20 KiB
11 · 游戏管理器与应用生命周期
命名空间
BaseGames.Core
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.World(SaveSystem)·BaseGames.UI(LoadingOverlay)
目录
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 枚举
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 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | — |
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 房间入口触发器上:
// 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 阶段切换
// 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. 暂停管理
// 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 只调用高层接口:
// 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):
// 内部实现示例
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(长椅)兼具存档点与快速旅行节点功能:
激活流程
// 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. 设计约束
-
单例但不是服务定位器
GameManager.Instance只暴露状态转换方法和协程入口,不暴露子系统引用。子系统间通信始终通过事件频道。 -
禁止在 GameManager 中写业务逻辑
HP 计算、战斗判定、音频播放均不属于 GameManager 职责。GameManager 只关心"什么时候做什么",不关心"怎么做"。
-
协程编排
死亡复活、Boss 入场等复杂流程通过
IEnumeratorCoroutine 实现,保证时序可控,避免回调嵌套地狱。所有 Coroutine 都在GameManager的 Persistent GameObject 上运行(不受场景卸载影响)。 -
失去焦点自动暂停
void OnApplicationFocus(bool hasFocus) { if (!hasFocus && _currentState == GameState.Gameplay) PauseGame(); } -
退出确认
从 Pause 菜单选择"退出到主菜单"时,弹出确认对话框("进度将丢失,是否确认?"),防止误操作。
12. 崩溃恢复机制
游戏意外崩溃或强制结束进程时,下次启动自动检测并提供恢复选项。
12.1 崩溃哨兵文件(crash.lock)
文件路径:{persistentDataPath}/crash.lock
生命周期:
游戏启动时:若 crash.lock 不存在 → 写入 crash.lock(记录启动时间戳)
游戏正常退出(OnApplicationQuit):删除 crash.lock
结论:crash.lock 存在 = 上次会话非正常结束(崩溃 / 强杀)
// 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() 开始时检测:
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 分钟,在后台静默写入一次检查点文件(不覆盖主存档):
// 在 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 清理
// 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
#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<string, object>
{
["roomId"] = roomId,
["regionId"] = _worldState.CurrentRegionId,
["playerHP"] = _playerStats.CurrentHP,
["playerGeo"] = _playerStats.CurrentGeo
});
}
void SendEvent(string eventName, Dictionary<string, object> 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 可视化):
["deathX"] = _playerTransform.position.x,
["deathY"] = _playerTransform.position.y,
本地 Debug 可视化(编辑器专用):AnalyticsDebugOverlay 在 Scene 视图用红点叠加所有会话内的死亡位置。