612 lines
20 KiB
Markdown
612 lines
20 KiB
Markdown
# 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 视图用红点叠加所有会话内的死亡位置。
|