# 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);
}
```