# 11 · 游戏管理器与应用生命周期 > **命名空间** `BaseGames.Core` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.World`(SaveSystem)· `BaseGames.UI`(LoadingOverlay) --- ## 目录 1. [职责概述](#1-职责概述) 2. [GameState 枚举](#2-gamestate-枚举) 3. [状态转换规则](#3-状态转换规则) 4. [初始化序列](#4-初始化序列) 5. [死亡与复活流程](#5-死亡与复活流程) 6. [Boss 战管理](#6-boss-战管理) 7. [暂停管理](#7-暂停管理) 8. [场景加载编排](#8-场景加载编排) 9. [快速旅行(Bench 传送)](#9-快速旅行bench-传送) 10. [事件频道](#10-事件频道) 11. [设计约束](#11-设计约束) --- ## 1. 职责概述 `GameManager` 是游戏**应用生命周期编排器**,是唯一持有全局 `GameState` 的对象: | 职责 | 说明 | |------|------| | 管理 `GameState` | 定义合法状态转换,广播 `OnGameStateChanged` | | 死亡复活编排 | 协调 SaveSystem + SceneLoader + UIManager 完成完整复活流程 | | 暂停 / 恢复 | 控制 `Time.timeScale`,切换输入 Action Map | | Boss 战切换 | 触发 BossFight 状态,联动 BGM / 镜头 / UI | | 快速旅行编排 | Bench 选择目标 → 加载过渡 → 传送 | | 应用启动初始化 | 按序初始化各子系统(见 §4) | **GameManager 不是服务定位器**:不暴露子系统引用(`GameManager.AudioManager`、`GameManager.UIManager` 等),子系统通过事件频道通信,GameManager 仅协调时序。 --- ## 2. GameState 枚举 ```csharp namespace BaseGames.Core { public enum GameState { Initializing, // 应用启动,资源初始化中 MainMenu, // 主菜单 LoadingScene, // 场景异步加载中(房间切换 / 死亡后重载 / 快速旅行) Gameplay, // 正常游玩 BossFight, // Boss 战(Gameplay 子状态,BGM/镜头特殊处理) Paused, // 暂停(Time.timeScale = 0) Dead, // 玩家死亡动画 + 死亡画面播放中 Cutscene, // 过场动画(P1) } } ``` --- ## 3. 状态转换规则 **合法转换表**(✅ 允许,❌ 禁止): | 当前 → 目标 | MainMenu | LoadingScene | Gameplay | BossFight | Paused | Dead | Cutscene | |-----------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | **Initializing** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | **MainMenu** | — | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | **LoadingScene** | ✅ | — | ✅ | ❌ | ❌ | ❌ | ❌ | | **Gameplay** | ❌ | ✅ | — | ✅ | ✅ | ✅ | ✅ | | **BossFight** | ❌ | ✅ | ✅ | — | ✅ | ✅ | ❌ | | **Paused** | ✅ | ❌ | ✅ | ✅ | — | ❌ | ❌ | | **Dead** | ❌ | ✅ | ❌ | ❌ | ❌ | — | ❌ | | **Cutscene** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | — | ```csharp public void TransitionTo(GameState newState) { if (!IsValidTransition(_currentState, newState)) { Debug.LogWarning($"[GameManager] 非法状态转换: {_currentState} → {newState}"); return; } _currentState = newState; _onGameStateChanged.Raise(newState); // 广播给 UIManager / AudioManager / 等 } ``` --- ## 4. 初始化序列 `GameManager` 使用 `DefaultExecutionOrder(-1000)`,在所有其他 `Awake()` 之前执行: ``` Awake(): 1. 设置 DontDestroyOnLoad(gameObject)(Persistent 场景随 GameManager 常驻) 2. Application.targetFrameRate = 60 3. Physics2D.simulationMode = Script(手动控制物理步进,与 FixedUpdate 解耦) 4. SaveManager.Initialize() (读取或创建存档文件) 5. AudioManager.Initialize() (加载 AudioMixer 快照) 6. ObjectPoolManager.Initialize() (预热常用对象池) 7. SettingsManager.Initialize() (读取设置,应用音量/分辨率等) 8. TransitionTo(GameState.Initializing) Start(): 9. 等待首帧渲染完成(yield return null) 10. TransitionTo(GameState.MainMenu) → UIManager 收到事件:显示 MainMenuPanel → AudioManager 收到事件:播放主菜单 BGM ``` --- ## 5. 死亡与复活流程 死亡复活是跨系统编排最复杂的流程,由 `GameManager` 通过 **Coroutine** 统一控制时序。 ### 触发链 ``` 1. PlayerHurtState 检测 HP <= 0 → OnPlayerDied.Raise() 2. GameManager.OnPlayerDied() 响应 → TransitionTo(GameState.Dead) → 禁用输入(InputReaderSO.EnableGameplayInput(false)) ├─ UIManager: DeathScreenPanel 开始渐入(0.8s) ├─ AudioManager: BGM 渐出(1.0s) └─ (Geo 掉落,若启用 Shade 机制→P1) 3. 等待死亡动画播放完成(yield WaitForSecondsRealtime(1.2f)) 4. DeathScreen 完全可见(WaitForSecondsRealtime(0.8f)) 5. 等待玩家按键确认(或 WaitForSecondsRealtime(3f) 超时自动继续) 6. LoadingOverlay.FadeIn()(0.3s,unscaledTime) 7. SceneLoader.LoadCheckpointScene() → 读取 SaveData.currentCheckpointScene → 异步 Unload 当前房间,Additive Load 存档点房间 → TransitionTo(GameState.LoadingScene) 8. 场景加载完成 → SaveSystem.RestoreGameState()(还原 HP、Soul、Geo、玩家坐标到存档点值) → DeathScreenPanel.SetActive(false) → LoadingOverlay.FadeOut()(0.3s) → InputReaderSO.EnableGameplayInput(true) → TransitionTo(GameState.Gameplay) → OnPlayerRespawned.Raise() ├─ PlayerController: 播放复活站起动画 └─ AudioManager: 恢复区域 BGM(淡入 1.0s) ``` ### 关键时序图 ``` 0.0s │ OnPlayerDied → TransitionTo(Dead),死亡动画开始 │ 1.2s │ 死亡动画结束,DeathScreen 渐入中 │ 2.0s │ DeathScreen 全黑可见,等待输入 │ ~2.xs│ 玩家按键(或 3s 超时) │ +0.0s│ LoadingOverlay 开始淡入 +0.3s│ 全黑,场景开始异步加载(TransitionTo LoadingScene) │ async │ 场景卸载 + 加载中... │ done │ 还原存档状态 │ LoadingOverlay 淡出(0.3s) │ TransitionTo(Gameplay) │ OnPlayerRespawned.Raise() ``` --- ## 6. Boss 战管理 ### 入场流程 `BossTrigger` 挂载在 Boss 房间入口触发器上: ```csharp // BossTrigger.cs(挂载在 Boss 房间入口 Trigger Collider) void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; // 1. 锁门:激活 Boss 房间入口碰撞体,阻止玩家退出 _entranceDoor.SetActive(true); // 2. 通知 BossEntity 激活(BT 开始运行) _bossEntity.ActivateBoss(_bossMaxHP); // 3. 广播 Boss 相关信息 _onBossNameSet.Raise(_bossDisplayName); _onBossHPMaxSet.Raise(_bossMaxHP); _onBossFightToggled.Raise(true); // 4. GameManager 状态切换 GameManager.Instance.TransitionTo(GameState.BossFight); // → AudioManager: BGM 切换到 Boss 主题(淡出旧 BGM 1s,淡入新 BGM) // → CameraStateController: 切换到 Boss VCam(Priority 40 → 50) // → UIManager: BossHPBar 滑入 } ``` ### 击败流程 `BossBase.OnBossDefeated()` 协程: ``` 1. 播放 Boss 死亡动画(BossDeathAnimancerState) 2. yield WaitForSeconds(死亡动画时长) 3. 解锁入口门(entranceDoor.SetActive(false)) 4. 掉落奖励(大量 Geo Collectible 爆散 + AbilityUnlock 物件生成) 5. SaveData 记录 defeatedBosses[bossId] = true → SaveManager.Save() 6. OnBossFightToggled.Raise(false) → UIManager BossHPBar 淡出 7. 激活该 Boss 房间内的 SavePoint(若存在) 8. GameManager.Instance.TransitionTo(GameState.Gameplay) → AudioManager: 播放胜利 Sting(3s)→ 之后淡入区域 BGM → CameraStateController: 恢复普通 VCam ``` ### Boss 阶段切换 ```csharp // BossBase(继承 EnemyBase)内部监听 HP 变化: void OnHPChanged(int newHP) { if (_currentPhase >= _phaseConfig.phases.Length - 1) return; if ((float)newHP / _maxHP <= _phaseConfig.phases[_currentPhase + 1].hpThreshold) { _currentPhase++; StartCoroutine(PhaseTransition()); _onBossPhaseChanged.Raise(_currentPhase); } } IEnumerator PhaseTransition() { // BT 暂停 → 播放阶段过渡动画 → 数值重置(速度/伤害提升)→ BT 恢复 _behaviorTree.DisableBehavior(); yield return PlayAnimationAndWait(_phaseTransitionClip); ApplyPhaseStats(_phaseConfig.phases[_currentPhase]); _behaviorTree.EnableBehavior(); } ``` --- ## 7. 暂停管理 ```csharp // GameManager.cs public void PauseGame() { if (_currentState != GameState.Gameplay && _currentState != GameState.BossFight) return; _stateBeforePause = _currentState; Time.timeScale = 0f; TransitionTo(GameState.Paused); _inputReader.EnableUIInput(); // 切换到 UI Action Map,菜单按钮可响应 _inputReader.EnableGameplayInput(false); } public void ResumeGame() { if (_currentState != GameState.Paused) return; Time.timeScale = 1f; TransitionTo(_stateBeforePause); _inputReader.EnableGameplayInput(true); _inputReader.EnableUIInput(false); } ``` **注意**:`MMF_Player.TimescaleIndependent = true` 已在 Feel 设置中为所有菜单动画启用,保证暂停时 UI 动画(菜单弹入、按钮 hover 等)不受 `timeScale = 0` 影响(使用 `Time.unscaledDeltaTime`)。 --- ## 8. 场景加载编排 `SceneLoader` 组件(`BaseGames.Core`)封装 Addressables 异步加载细节,GameManager 只调用高层接口: ```csharp // SceneLoader.cs public class SceneLoader : MonoBehaviour { // 房间切换(RoomTransition 调用) public IEnumerator LoadRoom(string roomAddress, Vector2 spawnPos, RoomTransition.Side spawnSide); // 死亡复活(加载存档点所在场景) public IEnumerator LoadCheckpointScene(); // 快速旅行(加载目标 Bench 所在场景) public IEnumerator LoadFastTravelTarget(string roomAddress, string fastTravelId); // 返回主菜单(卸载全部游戏场景) public IEnumerator UnloadAllAndLoadMainMenu(); } ``` 所有场景通过 **Addressables** 加载,不使用 `SceneManager.LoadSceneAsync(string)`: ```csharp // 内部实现示例 async Task LoadRoomInternal(string roomAddress) { // 卸载旧房间 if (_currentRoomHandle.IsValid()) await Addressables.UnloadSceneAsync(_currentRoomHandle).Task; // 加载新房间(Additive) _currentRoomHandle = Addressables.LoadSceneAsync( roomAddress, LoadSceneMode.Additive); await _currentRoomHandle.Task; } ``` ### Additive 多场景规则 | 规则 | 说明 | |------|------| | `Persistent` 场景永不卸载 | GameManager / AudioManager / UIManager 所在场景 | | 同时最多 1 个房间场景(MVP)| 每次加载新房间前 Unload 旧房间 | | 预加载相邻房间(P1)| 在玩家靠近出口时异步预加载下一个房间(`allowSceneActivation = false`)| | Unload 触发资源回收 | 离开房间时所有 Enemy / Collectible 实例随场景卸载,下次进入重新生成 | --- ## 9. 快速旅行(Bench 传送) Bench(长椅)兼具存档点与快速旅行节点功能: ### 激活流程 ```csharp // SavePoint.cs 的 OnInteract(): void OnInteract() { // 1. 存档 SaveManager.SetCheckpoint(transform.position, SceneManager.GetActiveScene().name); SaveManager.UnlockFastTravel(_fastTravelId); // 记录已解锁节点 SaveManager.Save(); OnSavePointActivated.Raise(); // 2. HP 全恢复(Hollow Knight 风格:坐 Bench 回血) _onPlayerHPChanged.Raise(playerStats.MaxHP); // 3. 若有已解锁的其他传送点,打开 FastTravel UI if (SaveManager.GetUnlockedFastTravelCount() > 1) UIManager.OpenFastTravelPanel(_fastTravelId); } ``` ### FastTravel UI(P1) ``` FastTravelPanel ├── RegionTabs (按区域分页) └── FastTravelPointList ├── FTPoint [森林长椅 01] ← 已解锁,当前位置(灰色不可选) ├── FTPoint [洞窟长椅 01] ← 已解锁 └── FTPoint [废墟长椅 01] ← 未解锁(不显示 / 灰色) ``` 传送流程:选择目标 → `LoadingOverlay.FadeIn()` → `SceneLoader.LoadFastTravelTarget()` → 玩家在目标 Bench 位置生成 → `FadeOut()`。 --- ## 10. 事件频道 新增 / 补充频道(`Assets/ScriptableObjects/Events/Game/`): | 资产名 | 类型 | 触发时机 | |--------|------|---------| | `OnGameStateChanged.asset` | `GameStateEventChannelSO` | 每次 TransitionTo() 成功时 | | `OnPlayerRespawned.asset` | `VoidEventChannelSO` | 复活流程完成,玩家已在场景中 | | `OnBossDefeated.asset` | `StringEventChannelSO` | Boss 击败,传递 bossId | | `OnBossPhaseChanged.asset` | `IntEventChannelSO` | Boss 阶段切换,传递新阶段索引 | | `OnFastTravelRequested.asset` | `StringEventChannelSO` | 玩家选择目标,传递目标 FastTravelId | | `OnQuitToMainMenu.asset` | `VoidEventChannelSO` | 确认退出到主菜单 | > `OnPlayerDied.asset`、`OnBossFightToggled.asset`、`OnSavePointActivated.asset` 已在 00_Overview.md 中定义,本文引用即可。 --- ## 11. 设计约束 1. **单例但不是服务定位器** `GameManager.Instance` 只暴露状态转换方法和协程入口,不暴露子系统引用。子系统间通信始终通过事件频道。 2. **禁止在 GameManager 中写业务逻辑** HP 计算、战斗判定、音频播放均不属于 GameManager 职责。GameManager 只关心"什么时候做什么",不关心"怎么做"。 3. **协程编排** 死亡复活、Boss 入场等复杂流程通过 `IEnumerator` Coroutine 实现,保证时序可控,避免回调嵌套地狱。所有 Coroutine 都在 `GameManager` 的 Persistent GameObject 上运行(不受场景卸载影响)。 4. **失去焦点自动暂停** ```csharp void OnApplicationFocus(bool hasFocus) { if (!hasFocus && _currentState == GameState.Gameplay) PauseGame(); } ``` 5. **退出确认** 从 Pause 菜单选择"退出到主菜单"时,弹出确认对话框("进度将丢失,是否确认?"),防止误操作。 --- ## 12. 崩溃恢复机制 游戏意外崩溃或强制结束进程时,下次启动自动检测并提供恢复选项。 ### 12.1 崩溃哨兵文件(crash.lock) ``` 文件路径:{persistentDataPath}/crash.lock 生命周期: 游戏启动时:若 crash.lock 不存在 → 写入 crash.lock(记录启动时间戳) 游戏正常退出(OnApplicationQuit):删除 crash.lock 结论:crash.lock 存在 = 上次会话非正常结束(崩溃 / 强杀) ``` ```csharp // CrashGuard.cs(挂载于 Persistent 场景 Bootstrap) public static class CrashGuard { static readonly string LockPath = Path.Combine(Application.persistentDataPath, "crash.lock"); public static bool WasPreviousCrash => File.Exists(LockPath); public static void OnGameStarted() { // 写入时间戳(方便诊断) File.WriteAllText(LockPath, DateTime.UtcNow.ToString("o"), System.Text.Encoding.UTF8); } public static void OnGameExitedCleanly() { if (File.Exists(LockPath)) File.Delete(LockPath); } } ``` ### 12.2 启动时崩溃检测 在 `GameManager.InitializeAsync()` 开始时检测: ```csharp async UniTask InitializeAsync() { // 崩溃恢复检测(必须在 SaveManager 初始化前) if (CrashGuard.WasPreviousCrash) { Debug.LogWarning("[GameManager] 检测到上次会话崩溃"); await HandleCrashRecoveryAsync(); } CrashGuard.OnGameStarted(); // 正常初始化流程... await _saveManager.InitializeAsync(); // ... } async UniTask HandleCrashRecoveryAsync() { // 尝试识别有备份的存档槽 int recoverySlot = _saveManager.FindSlotWithValidBackup(); if (recoverySlot < 0) return; // 无备份,忽略 // 显示恢复对话框(等待玩家确认,不阻塞 Update) bool accepted = await _uiManager.ShowCrashRecoveryDialogAsync(recoverySlot); if (accepted) await _saveManager.RestoreFromBackupAsync(recoverySlot); } ``` ### 12.3 自动检查点(5 分钟) **防止大量进度因崩溃丢失**:每隔 5 分钟,在后台静默写入一次检查点文件(不覆盖主存档): ```csharp // 在 GameManager.Update 或 LateUpdate 中(仅 Gameplay 状态) float _checkpointTimer; const float CHECKPOINT_INTERVAL = 300f; // 5 分钟 void UpdateCheckpoint() { if (_currentState != GameState.Gameplay) return; _checkpointTimer += Time.unscaledDeltaTime; if (_checkpointTimer < CHECKPOINT_INTERVAL) return; _checkpointTimer = 0f; // 后台写入,不影响帧率 _saveManager.WriteCheckpointAsync().Forget(); } ``` 检查点路径:`{persistentDataPath}/Saves/Slot{n}_checkpoint.json` 崩溃恢复对话框同时提供"使用检查点"选项(若检查点比备份更新)。 ### 12.4 OnApplicationQuit 清理 ```csharp // GameManager void OnApplicationQuit() { CrashGuard.OnGameExitedCleanly(); // 其他清理... } ``` --- ## 13. 遥测与分析钩子 > **优先级 P2**,发布后用于平衡调整与 Bug 复现。 > 所有分析代码必须包裹在 `#if UNITY_ANALYTICS || DEVELOPMENT_BUILD` 中,发行版无分析时零开销。 ### 13.1 关键事件列表 | 事件名 | 触发时机 | 关键参数 | |--------|---------|---------| | `room_entered` | 进入新房间 | `regionId`, `roomId`, `playerHP`, `playerGeo` | | `death_occurred` | 玩家死亡 | `causeOfDeath`, `regionId`, `roomId`, `totalDeaths` | | `boss_encounter` | 进入 Boss 房间 | `bossId`, `playerHP`, `equippedCharms[]` | | `boss_defeated` | Boss 死亡 | `bossId`, `attemptCount`, `timeElapsed`, `playerHP` | | `ability_unlocked` | 获取新能力 | `abilityType`, `regionId`, `playtimeMinutes` | | `session_start` | 游戏启动/读档 | `saveSlot`, `regionId`, `totalPlaytime` | | `session_end` | 退出游戏(正常)| `saveSlot`, `totalPlaytime`, `sessionLength` | ### 13.2 AnalyticsManager ```csharp #if UNITY_ANALYTICS || DEVELOPMENT_BUILD public class AnalyticsManager : MonoBehaviour { // 订阅全局事件,被动采集(零侵入业务逻辑) [SerializeField] StringEventChannelSO _onRoomEntered; [SerializeField] VoidEventChannelSO _onPlayerDied; [SerializeField] StringEventChannelSO _onBossDefeated; [SerializeField] StringEventChannelSO _onAbilityUnlocked; void OnEnable() { _onRoomEntered.OnEventRaised += TrackRoomEntered; _onPlayerDied.OnEventRaised += TrackDeath; _onBossDefeated.OnEventRaised += TrackBossDefeated; _onAbilityUnlocked.OnEventRaised += TrackAbilityUnlocked; } void OnDisable() { _onRoomEntered.OnEventRaised -= TrackRoomEntered; _onPlayerDied.OnEventRaised -= TrackDeath; _onBossDefeated.OnEventRaised -= TrackBossDefeated; _onAbilityUnlocked.OnEventRaised -= TrackAbilityUnlocked; } void TrackRoomEntered(string roomId) { SendEvent("room_entered", new Dictionary { ["roomId"] = roomId, ["regionId"] = _worldState.CurrentRegionId, ["playerHP"] = _playerStats.CurrentHP, ["playerGeo"] = _playerStats.CurrentGeo }); } void SendEvent(string eventName, Dictionary parameters) { // P2: 接入 Unity Analytics / 自定义后端 #if DEVELOPMENT_BUILD Debug.Log($"[Analytics] {eventName}: {JsonConvert.SerializeObject(parameters)}"); #endif // Unity.Services.Analytics.AnalyticsService.Instance.RecordEvent(...) } } #endif ``` ### 13.3 死亡热图数据 `death_occurred` 事件额外记录玩家世界坐标,方便在地图上生成**死亡热图**(配合外部分析工具或本地 Debug 可视化): ```csharp ["deathX"] = _playerTransform.position.x, ["deathY"] = _playerTransform.position.y, ``` **本地 Debug 可视化**(编辑器专用):`AnalyticsDebugOverlay` 在 Scene 视图用红点叠加所有会话内的死亡位置。