chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,611 @@
# 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.3sunscaledTime
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 VCamPriority 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: 播放胜利 Sting3s→ 之后淡入区域 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 UIP1
```
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<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 可视化):
```csharp
["deathX"] = _playerTransform.position.x,
["deathY"] = _playerTransform.position.y,
```
**本地 Debug 可视化**(编辑器专用):`AnalyticsDebugOverlay` 在 Scene 视图用红点叠加所有会话内的死亡位置。