# 03 · Core 核心模块 > **命名空间** `BaseGames.Core` > **程序集** `BaseGames.Core` > **路径** `Assets/Scripts/Core/` > **依赖** `BaseGames.Core.Events`、`Newtonsoft.Json` --- ## 目录 1. [GameManager](#1-gamemanager) 2. [GameState 枚举与合法转换表](#2-gamestate-枚举与合法转换表) 3. [SceneLoader](#3-sceneloader) 4. [SceneLoadRequest 数据结构](#4-sceneloadrequest-数据结构) 5. [GlobalObjectPool(引用 13_AssetPoolModule)](#5-globalobjectpool) 6. [SettingsManager](#6-settingsmanager) 7. [GlobalSettingsSO](#7-globalsettingsso) 8. [死亡复活流程(时序)](#8-死亡复活流程时序) 9. [Boss 战切换流程(时序)](#9-boss-战切换流程时序) 10. [初始化序列(ExecutionOrder)](#10-初始化序列) 11. [ServiceLocator — 轻量依赖注入](#11-servicelocator--轻量依赖注入) 12. [DeathRespawnService — 死亡/复活服务拆分](#12-deathrespawnservice--死亡复活服务拆分) 13. [SceneService — 场景管理服务拆分](#13-sceneservice--场景管理服务拆分) --- ## 1. GameManager ``` 路径: Assets/Scripts/Core/GameManager.cs 程序集: BaseGames.Core [DefaultExecutionOrder(-1000)] ``` ### 字段 ```csharp [Header("Event Channels - Listen")] [SerializeField] private VoidEventChannelSO _onPlayerDied; [SerializeField] private VoidEventChannelSO _onSavePointActivated; [SerializeField] private StringEventChannelSO _onBossFightStarted; [SerializeField] private BoolEventChannelSO _onBossFightEnded; [SerializeField] private VoidEventChannelSO _onPauseRequested; [SerializeField] private StringEventChannelSO _onSceneLoaded; [SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed; // DeathScreenController 按钮点击 [Header("Event Channels - Raise")] [SerializeField] private GameStateEventChannelSO _onGameStateChanged; [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; [SerializeField] private VoidEventChannelSO _onPlayerRespawned; [Header("Sub Managers(同 Persistent 场景内引用)")] [SerializeField] private SceneLoader _sceneLoader; [SerializeField] private GlobalObjectPool _objectPool; // ⚠️ 权威类名为 GlobalObjectPool(见 13_AssetPoolModule §3) [SerializeField] private SettingsManager _settingsManager; // SaveManager 是独立单例,不通过 GameManager 暴露 private GameState _currentState = GameState.Initializing; private string _lastSavePointId; private string _currentSceneName; ``` ### 公开接口 ```csharp // 状态管理 public GameState CurrentState => _currentState; public void TransitionTo(GameState newState); // 内含合法性检查,发布 _onGameStateChanged // 场景控制(通过 SceneLoader 编排) public void LoadRoom(string roomSceneName, string entryTransitionId = null); public void ReturnToMainMenu(); // 暂停 public void Pause(); public void Resume(); // 复活(由死亡流程 Coroutine 最终调用) internal void CompleteRespawn(); ``` ### 私有/内部方法 ```csharp // 生命周期 private void Awake(); // 初始化序列(见 §10) private void Start(); // 首帧后跳转 MainMenu // 事件处理 private void HandlePlayerDied(); // → TransitionTo(Dead) → 启动 DeathCoroutine private void HandleSavePointActivated(string saveId); // → SaveManager.SaveAsync() private void HandleBossFightStarted(string bossId); // → TransitionTo(BossFight) private void HandleBossFightEnded(bool victory); // → 根据结果决策 private void HandlePauseRequested(); // → Pause() / Resume() 切换 // Coroutines private IEnumerator DeathSequenceCoroutine(); // 死亡动画 → 显示 DeathScreen → 等待输入 private IEnumerator RespawnCoroutine(); // 淡出 → LoadLastSaveScene → 淡入 → 初始化 private IEnumerator LoadSceneCoroutine(SceneLoadRequest request); ``` --- ## 2. GameState 插件化状态机 > **设计动机**:原 `enum GameState` 在编译期固定,DLC 若需引入新游戏模式必须修改核心枚举,违反开闭原则。本节将状态机改为 **`IGameState` 接口 + 运行时注册表**,内置 8 个状态保持不变,外部模块可通过 `RuntimeInitializeOnLoad` 注入新状态,无需重新编译核心程序集。 ### 2.1 GameStateId — 运行时可注册状态标识 ```csharp // 路径: Assets/Scripts/Core/GameStateId.cs namespace BaseGames.Core { /// /// 轻量状态标识符(值类型,无堆分配)。 /// 内置状态通过 静态字段访问, /// 扩展状态在 [RuntimeInitializeOnLoad] 中调用 Register() 注入。 /// public readonly struct GameStateId : System.IEquatable { public readonly int Value; private GameStateId(int v) => Value = v; private static int _nextId; private static readonly System.Collections.Generic.Dictionary _registry = new(); /// 注册状态;已注册则返回现有 ID(幂等)。 public static GameStateId Register(string name) { if (_registry.TryGetValue(name, out var existing)) return existing; var id = new GameStateId(_nextId++); _registry[name] = id; return id; } public static bool TryResolve(string name, out GameStateId id) => _registry.TryGetValue(name, out id); public static IEnumerable All => _registry.Values; public bool Equals(GameStateId other) => Value == other.Value; public override bool Equals(object obj) => obj is GameStateId g && Equals(g); public override int GetHashCode() => Value; public static bool operator ==(GameStateId a, GameStateId b) => a.Value == b.Value; public static bool operator !=(GameStateId a, GameStateId b) => a.Value != b.Value; public override string ToString() { foreach (var kv in _registry) if (kv.Value == this) return kv.Key; return $"GameState({Value})"; } } /// 内置状态常量(静态初始化顺序确保在任何 Awake 前完成)。 public static class GameStates { public static readonly GameStateId Initializing = GameStateId.Register("Initializing"); public static readonly GameStateId MainMenu = GameStateId.Register("MainMenu"); public static readonly GameStateId LoadingScene = GameStateId.Register("LoadingScene"); public static readonly GameStateId Gameplay = GameStateId.Register("Gameplay"); public static readonly GameStateId BossFight = GameStateId.Register("BossFight"); public static readonly GameStateId Paused = GameStateId.Register("Paused"); public static readonly GameStateId Dead = GameStateId.Register("Dead"); public static readonly GameStateId Cutscene = GameStateId.Register("Cutscene"); } } ``` ### 2.2 IGameState — 状态行为接口 ```csharp // 路径: Assets/Scripts/Core/IGameState.cs namespace BaseGames.Core { /// /// 可插件化的游戏状态行为接口。 /// 每个状态封装自身的进入/退出/Tick 逻辑,不再依赖 GameManager 中的 switch 分支。 /// public interface IGameState { GameStateId Id { get; } /// 合法的后继状态集合。GameManager.TransitionTo() 据此校验。 IReadOnlyCollection ValidNextStates { get; } void OnEnter(GameStateId previousState); void OnExit(GameStateId nextState); /// 每帧由 GameManager.Update() 调用(可为空实现)。 void Tick(float deltaTime) { } } } ``` ### 2.3 GameStateMachine — 轻量状态机核心 ```csharp // 路径: Assets/Scripts/Core/GameStateMachine.cs namespace BaseGames.Core { /// /// 状态机核心,持有状态注册表与当前状态。 /// GameManager 持有一个实例;状态对象在 Awake 注入(内置 + 可选 DLC 追加)。 /// public class GameStateMachine { private readonly Dictionary _states = new(); private IGameState _current; public GameStateId CurrentStateId => _current?.Id ?? default; /// 注册状态实现(同 Id 注册多次以最后一次为准)。 public void Register(IGameState state) => _states[state.Id] = state; public bool TransitionTo(GameStateId nextId, out string error) { if (!_states.TryGetValue(nextId, out var next)) { error = $"[GameStateMachine] 未知状态 '{nextId}'"; return false; } if (_current != null && !_current.ValidNextStates.Contains(nextId)) { error = $"[GameStateMachine] 非法转换 {_current.Id} → {nextId}"; return false; } var prev = _current?.Id ?? default; _current?.OnExit(nextId); _current = next; _current.OnEnter(prev); error = null; return true; } public void Tick(float deltaTime) => _current?.Tick(deltaTime); } } ``` ### 2.4 GameManager 集成 ```csharp // GameManager.cs — 关键修改(其余字段不变) public class GameManager : MonoBehaviour { // 状态机实例(不再使用 enum currentState) private readonly GameStateMachine _fsm = new(); private void Awake() { // 注册内置状态 _fsm.Register(new InitializingState()); _fsm.Register(new MainMenuState(_onMainMenuEntered)); _fsm.Register(new LoadingSceneState(_onLoadingStarted)); _fsm.Register(new GameplayState(_inputReader, _onGameplayStarted)); _fsm.Register(new BossFightState(_onBossFightEntered)); _fsm.Register(new PausedState(_onGamePaused)); _fsm.Register(new DeadState(_onPlayerDied)); _fsm.Register(new CutsceneState(_onCutsceneStarted)); // DLC / 模块注入(可选;通过 [RuntimeInitializeOnLoad] 在 Awake 前注入) foreach (var ext in _externalStateFactories) _fsm.Register(ext.Create()); _fsm.TransitionTo(GameStates.Initializing, out _); } // 对外仍使用 GameStateId(或兼容属性) public GameStateId CurrentState => _fsm.CurrentStateId; public void TransitionTo(GameStateId nextState) { if (!_fsm.TransitionTo(nextState, out var err)) Debug.LogWarning(err); else _onGameStateChanged.Raise(nextState); } private void Update() => _fsm.Tick(Time.deltaTime); // ── DLC 扩展点 ──────────────────────────────────────────────────────── // 外部模块在 [RuntimeInitializeOnLoad(RuntimeInitializeLoadType.BeforeSceneLoad)] // 调用 GameManager.RegisterStateFactory(factory) 注入新状态 private static readonly List _externalStateFactories = new(); public static void RegisterStateFactory(IGameStateFactory factory) => _externalStateFactories.Add(factory); } /// DLC 模块实现此接口以注入新游戏状态。 public interface IGameStateFactory { IGameState Create(); } ``` ### 2.5 内置状态示例 ```csharp // 路径: Assets/Scripts/Core/States/GameplayState.cs public class GameplayState : IGameState { private readonly VoidEventChannelSO _onEnterChannel; private readonly InputReaderSO _inputReader; public GameplayState(InputReaderSO input, VoidEventChannelSO onEnter) { _inputReader = input; _onEnterChannel = onEnter; } public GameStateId Id => GameStates.Gameplay; public IReadOnlyCollection ValidNextStates { get; } = new HashSet { GameStates.LoadingScene, GameStates.BossFight, GameStates.Paused, GameStates.Dead, GameStates.Cutscene, }; public void OnEnter(GameStateId prev) { _inputReader.EnableGameplayMap(); _onEnterChannel.Raise(); } public void OnExit(GameStateId next) { if (next == GameStates.Paused) return; // 暂停不禁用 Gameplay map _inputReader.DisableGameplayMap(); } } ``` **合法转换一览(同前)**: ``` Initializing → MainMenu MainMenu → LoadingScene LoadingScene → MainMenu | Gameplay Gameplay → LoadingScene | BossFight | Paused | Dead | Cutscene BossFight → LoadingScene | Gameplay | Paused | Dead Paused → Gameplay | BossFight | MainMenu Dead → LoadingScene Cutscene → Gameplay ``` > **向后兼容**:现有订阅 `GameStateEventChannelSO`(泛型参数改为 `GameStateId`)的代码,将事件类型从 `GameState`(enum)替换为 `GameStateId`(struct)即可,API 形态不变。`GameStateId == GameStateId` 值比较性能与 enum 相同(int 比较)。 ``` 路径: Assets/Scripts/Core/SceneLoader.cs 程序集: BaseGames.Core ``` ### 职责 - 监听 `EVT_SceneLoadRequest` 频道,执行异步加载 - 管理 Persistent 场景常驻(加法加载/卸载) - 加载完成后发布 `EVT_SceneLoaded` ### 字段 ```csharp [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; [SerializeField] private StringEventChannelSO _onSceneLoaded; [SerializeField] private VoidEventChannelSO _onFadeInRequest; // 触发 UI 淡入 [SerializeField] private VoidEventChannelSO _onFadeOutRequest; // 触发 UI 淡出 [SerializeField] private float _fadeDuration = 0.3f; private string _currentRoomScene; // 当前已加载的房间场景名 ``` ### 接口 ```csharp // 由 GameManager 或 RoomTransition 触发 public void RequestLoad(SceneLoadRequest request); // 内部流程 private IEnumerator LoadSceneCoroutine(SceneLoadRequest request); // 1. Raise FadeOut // 2. yield LoadSceneAsync(房间场景) // 3. 卸载旧房间场景(保留 Persistent) // 4. 激活新场景 // 5. Raise SceneLoaded(供 GameManager 切状态、MapManager 更新) // 6. Raise FadeIn ``` --- ## 4. SceneLoadRequest 数据结构 ```csharp // 路径: Assets/Scripts/Core/SceneLoadRequest.cs namespace BaseGames.Core { [System.Serializable] public struct SceneLoadRequest { public string SceneName; // Addressable 场景键(AddressKeys 常量) public string EntryTransitionId; // 玩家出生点 ID(RoomTransition 与之匹配) public bool ShowLoadingScreen; // 跨大区域时 true public bool IsRespawn; // 死亡复活时 true,不执行过渡动画 } } ``` --- ## 5. GlobalObjectPool > ⚠️ **权威实现见 [`13_AssetPoolModule.md §3`](../Architecture/13_AssetPoolModule.md)** > 类名为 `GlobalObjectPool`(命名空间 `BaseGames.Core`)。 > 本模块不重复定义,以下仅列出与 CoreModule 相关的调用入口摘要。 ```csharp // 使用方式(CoreModule 内部调用) GlobalObjectPool.Instance.WarmupAsync(); // SceneLoader 步骤 4 调用 GlobalObjectPool.Instance.Spawn(addressKey, pos, rot); // 取出实例 GlobalObjectPool.Instance.Despawn(addressKey, instance); // 归还实例 GlobalObjectPool.Instance.ClearPool(addressKey); // 场景卸载时清除指定池 ``` --- ## 6. SettingsManager ``` 路径: Assets/Scripts/Core/SettingsManager.cs 程序集: BaseGames.Core [DefaultExecutionOrder(-800)] ``` ### 职责 - 从 `GlobalSettingsSO` 读取默认值,从 PlayerPrefs / 独立设置文件加载用户覆盖 - 应用设置到 AudioMixer、Screen、InputSystem - 提供运行时修改接口(供 SettingsPanel 调用) ### 字段 ```csharp [SerializeField] private GlobalSettingsSO _defaultSettings; [SerializeField] private FloatEventChannelSO _onMasterVolumeChanged; // 同步 HUD private GlobalSettingsData _current; // 运行时值(从文件读取) ``` ### 接口 ```csharp public void Initialize(); // GameManager.Awake 调用 // 音量(0-1 线性;内部调用 AudioManager.Instance.SetVolume(AudioMixerKeys.*, value) 写入 AudioMixer) public void SetMasterVolume(float value) => AudioManager.Instance.SetVolume(AudioMixerKeys.Master, value); public void SetBGMVolume(float value) => AudioManager.Instance.SetVolume(AudioMixerKeys.BGM, value); public void SetSFXVolume(float value) => AudioManager.Instance.SetVolume(AudioMixerKeys.SFX, value); public void SetAmbientVolume(float value) => AudioManager.Instance.SetVolume(AudioMixerKeys.Ambient, value); // 画面 public void SetResolution(int width, int height, FullScreenMode mode); public void SetVSync(bool enabled); public void SetTargetFrameRate(int fps); // 仅非 VSync 时生效 // 语言 public void SetLanguage(string localeCode); // 调用 LocalizationSettings.SelectedLocale // 保存(写入独立设置文件) public void Save(); // 当前值访问 public GlobalSettingsData Current => _current; ``` --- ## 7. GlobalSettingsSO ``` 路径: Assets/Scripts/Core/GlobalSettingsSO.cs 资产: Assets/Data/Settings/SET_GlobalSettings.asset ``` ```csharp [CreateAssetMenu(menuName = "Settings/GlobalSettings")] public class GlobalSettingsSO : ScriptableObject { [Header("Audio")] public float DefaultMasterVolume = 1f; public float DefaultBGMVolume = 0.8f; public float DefaultSFXVolume = 1f; public float DefaultAmbientVolume = 0.8f; // ⚠️ 与 AudioMixerKeys.Ambient 对应 [Header("Display")] public int DefaultTargetFPS = 60; public bool DefaultVSync = false; [Header("Language")] public string DefaultLocaleCode = "zh-CN"; [Header("Accessibility")] public bool DefaultHighContrast = false; public bool DefaultScreenShake = true; } [System.Serializable] public class GlobalSettingsData { public float MasterVolume; public float BGMVolume; public float SFXVolume; public float AmbientVolume; // ⚠️ 与 AudioMixerKeys.Ambient 对应 public int TargetFPS; public bool VSync; public string LocaleCode; public bool HighContrast; public bool ScreenShake; } ``` --- ## 8. 死亡复活流程(时序) ``` [PlayerStats] HP ≤ 0 → Raise EVT_PlayerDied [GameManager] HandlePlayerDied() → TransitionTo(GameState.Dead) → Raise EVT_GameStateChanged(Dead) ← [UIManager] 禁用 HUD 输入 ← [AudioManager] 播放死亡音乐/SFX → StartCoroutine(DeathSequenceCoroutine) 1. 等待死亡动画完成(约 1.5s) 2. (DeathScreenController 订阅 EVT_PlayerDied,延迟 1.5s 后自动显示死亡画面;无需 GameManager 直接调用 UI) 3. 等待玩家按下"重试"(监听 EVT_DeathScreenConfirmed) [GameManager] RespawnCoroutine() → TransitionTo(GameState.LoadingScene) → SceneLoader.RequestLoad(lastSaveScene, isRespawn: true) → 淡出 → 卸载当前房间场景 → 加载存档点对应场景 → SaveManager 恢复玩家状态(HP = MaxHP×0.67,灵泉=1) → 淡入 → TransitionTo(GameState.Gameplay) → Raise EVT_PlayerRespawned ``` --- ## 9. Boss 战切换流程(时序) ``` [BossOrchestrator] 玩家进入 Boss 房间触发器 → Raise EVT_BossFightStarted("boss_forest") [GameManager] HandleBossFightStarted(bossId) → TransitionTo(GameState.BossFight) ← [AudioManager] 切换 Boss BGM(AudioMixer 快照) ← [UIManager] 显示 Boss 血量条 ← [CinemachineCamera] 切换为 BossCamera 虚拟摄像机 [BossOrchestrator] Boss 死亡 → Raise EVT_BossFightEnded(victory: true) [GameManager] HandleBossFightEnded(true) → TransitionTo(GameState.Gameplay) ← [AudioManager] 恢复常规 BGM ← [UIManager] 隐藏 Boss 血量条 → 触发剧情 / 过场(如有) ``` --- ## 10. 初始化序列 ```csharp // GameManager.Awake() [DefaultExecutionOrder(-1000)] private void Awake() { DontDestroyOnLoad(gameObject); // Persistent 场景随 GameManager 常驻 Application.targetFrameRate = 60; Physics2D.simulationMode = SimulationMode2D.Script; // 手动物理步进 // 子系统初始化(按依赖顺序) _settingsManager.Initialize(); // 1. 读取设置文件,应用音量/分辨率 // SaveManager 自初始化(SaveManager.Awake 先于 GameManager.Start) // GlobalObjectPool 无 Initialize() 方法;预热由 SceneLoader.LoadSceneCoroutine 步骤 4 // 调用 GlobalObjectPool.Instance.WarmupAsync() 触发(见 13_AssetPoolModule §6) // DifficultyManager 自初始化(DefaultExecutionOrder: -900,早于本 Awake) // → 读取 SaveData.Meta.CurrentDifficulty // → Apply(level) → 广播 EVT_DifficultyChanged // → EnemyStats / PlayerStats / ShopController 等系统订阅此事件后注入难度系数 // → 详见 19_DifficultyModule.md TransitionTo(GameState.Initializing); } private IEnumerator Start() { yield return null; // 等待首帧渲染 TransitionTo(GameState.MainMenu); // → UIManager 收到 EVT_GameStateChanged(MainMenu) → 显示 MainMenuPanel // → AudioManager 收到 → 播放主菜单 BGM } ``` --- ## 11. ServiceLocator — 轻量依赖注入 > **P0 优化**:原项目中 `AudioManager.Instance`、`EventChannelRegistry.Instance`、`SaveManager` 等均为静态单例, > 在多场景 Play Mode 快速进入(Direct Play from Room scene)时若 Persistent 场景未加载, > `Instance` 为 null 导致崩溃;测试时也无法替换为 Mock 实现。 > 引入 `ServiceLocator` 替代所有硬编码 `Instance`,保留 Inspector 序列化注入路径(无需引入 Zenject/VContainer 等外部 DI 框架)。 ### 11.1 ServiceLocator 核心 ```csharp // 路径: Assets/Scripts/Core/ServiceLocator.cs namespace BaseGames.Core { /// /// 轻量服务定位器。 /// - 通过类型键注册/查找服务,支持接口类型注册(依赖倒置) /// - 在 Persistent 场景 Awake(ExecutionOrder -2000)批量注册全局服务 /// - 支持 Fallback:找不到服务时返回 NullObject 而非 null(防崩溃) /// - Editor 下 Play Mode "Enter Play Mode Without Domain Reload" 友好(静态字典不清空) /// public static class ServiceLocator { private static readonly Dictionary _services = new(); // ── 注册 ──────────────────────────────────────────────────────── /// 以接口类型 TInterface 注册实现 impl。 public static void Register(TInterface impl) => _services[typeof(TInterface)] = impl; /// 仅当尚未注册时才注册(防多场景重复注册同一服务)。 public static void RegisterIfAbsent(TInterface impl) { if (!_services.ContainsKey(typeof(TInterface))) _services[typeof(TInterface)] = impl; } // ── 查找 ──────────────────────────────────────────────────────── /// /// 查找服务。未注册时返回 default(T为class则为null)并输出LogError。 /// public static TInterface Get() { if (_services.TryGetValue(typeof(TInterface), out var svc) && svc is TInterface typed) return typed; // 未注册时抛出明确异常(带上下文),而非返回 null 造成延迟 NullReferenceException throw new InvalidOperationException( $"[ServiceLocator] Service '{typeof(TInterface).Name}' is not registered. " + "Ensure GameServiceRegistrar.Awake() has run before this call " + "(ExecutionOrder -2000). For optional services use GetOrDefault()."); } /// 安全版 Get:未注册时返回 fallback,不报错(适用于可选服务)。 public static TInterface GetOrDefault(TInterface fallback = default) => _services.TryGetValue(typeof(TInterface), out var svc) && svc is TInterface typed ? typed : fallback; // ── 测试支持 ───────────────────────────────────────────────────── /// 单元测试中替换服务实现(仅在 UNITY_EDITOR 或 TEST 编译条件下可用)。 [System.Diagnostics.Conditional("UNITY_EDITOR")] public static void OverrideForTest(TInterface mock) => _services[typeof(TInterface)] = mock; [System.Diagnostics.Conditional("UNITY_EDITOR")] public static void Reset() => _services.Clear(); } } ``` ### 11.2 全局服务接口清单 | 接口 | 实现类 | NullObject 实现 | |------|--------|----------------| | `IAudioService` | `AudioManager` | `NullAudioService` | | `ISaveService` | `SaveManager` | — | | `ISceneService` | `SceneService`(见 §13) | — | | `IDeathRespawnService` | `DeathRespawnService`(见 §12) | — | | `IEventChannelRegistry` | `EventChannelRegistry` | — | ### 11.3 服务注册器 ```csharp // 路径: Assets/Scripts/Core/GameServiceRegistrar.cs // Persistent 场景内挂载,最早执行(ExecutionOrder -2000) [DefaultExecutionOrder(-2000)] public class GameServiceRegistrar : MonoBehaviour { [Header("服务实现引用(Inspector 拖拽)")] [SerializeField] private AudioManager _audioManager; [SerializeField] private SaveManager _saveManager; [SerializeField] private SceneService _sceneService; [SerializeField] private DeathRespawnService _deathRespawnService; [SerializeField] private EventChannelRegistry _eventChannelRegistry; private void Awake() { ServiceLocator.Register(_audioManager); ServiceLocator.Register(_saveManager); ServiceLocator.Register(_sceneService); ServiceLocator.Register(_deathRespawnService); ServiceLocator.Register(_eventChannelRegistry); } } ``` ### 11.4 消费示例(替代旧 Singleton) ```csharp // ❌ 旧写法(直接单例,测试不友好) AudioManager.Instance.PlaySFX(clip); // ✅ 新写法(ServiceLocator,可 Mock) ServiceLocator.Get().PlaySFX(clip); // ✅ 可选服务(如 PlatformManager 仅 Steam 平台存在) ServiceLocator.GetOrDefault(NullPlatformService.Instance) .UnlockAchievement("ACH_FirstKill"); ``` ### 11.5 IAudioService 接口 ```csharp // 路径: Assets/Scripts/Audio/IAudioService.cs namespace BaseGames.Audio { public interface IAudioService { void PlayBGM(AudioCueSO cue, float fadeInTime = 1.0f); void StopBGM(float fadeOutTime = 1.0f); void PlaySFX(AudioCueSO cue); void PlaySFXAtPosition(AudioCueSO cue, Vector2 position); void SetMixerVolume(string paramName, float linearVolume); // 0..1 → dB void TransitionToSnapshot(string snapshotName, float transitionTime); AudioCueSO GetCurrentBGM(); } /// 静默实现,用于测试和 BGM 静默场景。 public class NullAudioService : IAudioService { public static readonly NullAudioService Instance = new(); public void PlayBGM(AudioCueSO c, float t = 1f) { } public void StopBGM(float t = 1f) { } public void PlaySFX(AudioCueSO c) { } public void PlaySFXAtPosition(AudioCueSO c, Vector2 p) { } public void SetMixerVolume(string n, float v) { } public void TransitionToSnapshot(string n, float t) { } public AudioCueSO GetCurrentBGM() => null; } } ``` --- ## 12. DeathRespawnService — 死亡/复活服务拆分 > **P1 优化**:原 `GameManager` 同时管理状态机 + 死亡序列协程 + 场景切换,违反 SRP。 > 将死亡/复活流程提取为独立的 `DeathRespawnService`,GameManager 只负责 > 监听事件 → 委托给对应服务 → 进行状态转换。 ### 12.1 IDeathRespawnService 接口 ```csharp // 路径: Assets/Scripts/Core/IDeathRespawnService.cs namespace BaseGames.Core { public interface IDeathRespawnService { /// 玩家死亡时由 GameManager 调用,启动死亡演出流程。 UniTask StartDeathSequenceAsync(CancellationToken ct = default); /// DeathScreen 确认按钮点击后调用,执行复活流程。 UniTask StartRespawnAsync(CancellationToken ct = default); /// SteelSoul 模式:HP 归零后直接清档并返回主菜单。 UniTask StartGameOverAsync(CancellationToken ct = default); } } ``` ### 12.2 DeathRespawnService 实现 ```csharp // 路径: Assets/Scripts/Core/DeathRespawnService.cs namespace BaseGames.Core { /// /// 死亡 / 复活流程的独立服务。 /// 依赖:ISceneService(场景切换)、ISaveService(加载存档)、 /// GameStateMachine(状态转换通知)、LoadingOverlay(淡入淡出)。 /// 所有依赖由 ServiceLocator 在 Awake 时解析,不持有 GameManager 引用。 /// public class DeathRespawnService : MonoBehaviour, IDeathRespawnService { [Header("Config")] [SerializeField] private float _deathAnimDuration = 1.2f; // 死亡动画时长 [SerializeField] private float _deathScreenDelay = 0.5f; // 动画结束 → 死亡画面 [SerializeField] private float _respawnFadeDuration = 0.4f; // 淡出/淡入时长 [Header("Event Channels - Raise")] [SerializeField] private VoidEventChannelSO _onRespawnStarted; [SerializeField] private VoidEventChannelSO _onRespawnCompleted; [Header("Event Channels - Listen")] [SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed; // 内部等待 DeathScreen 用户确认 private UniTaskCompletionSource _deathConfirmTcs; private void OnEnable() => _onDeathScreenConfirmed.OnEventRaised += HandleDeathScreenConfirmed; private void OnDisable() => _onDeathScreenConfirmed.OnEventRaised -= HandleDeathScreenConfirmed; private void HandleDeathScreenConfirmed() => _deathConfirmTcs?.TrySetResult(); // ── IDeathRespawnService ────────────────────────────────────── public async UniTask StartDeathSequenceAsync(CancellationToken ct = default) { // 1. 等待死亡动画 await UniTask.Delay(TimeSpan.FromSeconds(_deathAnimDuration), cancellationToken: ct); // 2. 等待 DeathScreen 延迟 await UniTask.Delay(TimeSpan.FromSeconds(_deathScreenDelay), cancellationToken: ct); // 3. 等待玩家在死亡画面确认(UI → EVT_DeathScreenConfirmed) _deathConfirmTcs = new UniTaskCompletionSource(); await _deathConfirmTcs.Task.AttachExternalCancellation(ct); } public async UniTask StartRespawnAsync(CancellationToken ct = default) { var sceneService = ServiceLocator.Get(); var saveService = ServiceLocator.Get(); _onRespawnStarted.Raise(); // 1. 淡出 await ServiceLocator.Get().FadeOutAsync(_respawnFadeDuration, ct); // 2. 加载最后存档场景 var saveData = await saveService.LoadCurrentSlotAsync(ct); await sceneService.LoadSceneAsync(new SceneLoadRequest { SceneName = saveData.Player.Scene, EntryTransitionId = saveData.Meta.SavePointId, IsRespawn = true, }, ct); // 3. 淡入 await ServiceLocator.Get().FadeInAsync(_respawnFadeDuration, ct); _onRespawnCompleted.Raise(); } public async UniTask StartGameOverAsync(CancellationToken ct = default) { // SteelSoul:清档 → 返回主菜单 await ServiceLocator.Get().DeleteCurrentSlotAsync(ct); await ServiceLocator.Get().LoadMainMenuAsync(ct); } } } ``` ### 12.3 重构后 GameManager 变化 ```csharp // GameManager 精简为:事件监听 → 委托服务 → 状态转换 private async UniTaskVoid HandlePlayerDied() { _stateMachine.TransitionTo(GameStates.Dead); await ServiceLocator.Get().StartDeathSequenceAsync(_cts.Token); // DeathScreen 确认后触发复活 _stateMachine.TransitionTo(GameStates.Loading); await ServiceLocator.Get().StartRespawnAsync(_cts.Token); _stateMachine.TransitionTo(GameStates.Playing); } ``` > **协程 → UniTask 迁移**:`DeathRespawnService` 全程使用 UniTask,消除 `IEnumerator` 混用问题。 > `GameManager.Start()` 改为 `private async UniTaskVoid Start()`。 > `_cts = new CancellationTokenSource()` 在 `OnDestroy()` 时 `.Cancel()` + `.Dispose()`。 --- ## 13. SceneService — 场景管理服务拆分 > **P1 优化**:原 `GameManager.LoadSceneCoroutine` 内嵌了场景加载、LoadingOverlay 控制、 > Addressables 预热触发等逻辑。提取为独立的 `SceneService`,GameManager 调用 > `ServiceLocator.Get().LoadSceneAsync()` 即可。 ### 13.1 ISceneService 接口 ```csharp // 路径: Assets/Scripts/Core/ISceneService.cs namespace BaseGames.Core { public interface ISceneService { /// 通用场景加载(带 LoadingOverlay + Addressables 预热)。 UniTask LoadSceneAsync(SceneLoadRequest request, CancellationToken ct = default); /// 返回主菜单(清理所有 Additive 场景后加载 MainMenu)。 UniTask LoadMainMenuAsync(CancellationToken ct = default); /// 当前已加载场景名称(Persistent 除外)。 string CurrentSceneName { get; } } } ``` ### 13.2 SceneService 实现 ```csharp // 路径: Assets/Scripts/Core/SceneService.cs [DefaultExecutionOrder(-900)] public class SceneService : MonoBehaviour, ISceneService { [Header("Config")] [SerializeField] private WarmupManifestSO _warmupManifest; // 见 13_AssetPoolModule §12 [SerializeField] private LoadingOverlay _loadingOverlay; public string CurrentSceneName { get; private set; } public async UniTask LoadSceneAsync(SceneLoadRequest request, CancellationToken ct = default) { // 1. 淡出 await _loadingOverlay.FadeOutAsync(0.3f, ct); // 2. 卸载当前场景(保留 Persistent) if (!string.IsNullOrEmpty(CurrentSceneName)) await Addressables.UnloadSceneAsync(CurrentSceneName).ToUniTask(ct); // 3. 加载新场景 var handle = Addressables.LoadSceneAsync(request.SceneName, LoadSceneMode.Additive, activateOnLoad: false); await handle.ToUniTask(ct); // 4. Addressables 预热(见 13_AssetPoolModule §9) await ServiceLocator.Get() .WarmupFromManifestAsync(_warmupManifest, ct); // 5. 激活场景 await handle.Result.ActivateAsync().ToUniTask(ct); CurrentSceneName = request.SceneName; // 6. 淡入 await _loadingOverlay.FadeInAsync(0.3f, ct); } public async UniTask LoadMainMenuAsync(CancellationToken ct = default) => await LoadSceneAsync(new SceneLoadRequest { SceneName = AddressKeys.SceneMainMenu }, ct); } ```