chore: initial commit
This commit is contained in:
611
Docs/Design/11_GameManager.md
Normal file
611
Docs/Design/11_GameManager.md
Normal 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.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<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 视图用红点叠加所有会话内的死亡位置。
|
||||
Reference in New Issue
Block a user