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,963 @@
# Phase 1 验证指南 v2.0
> **适用版本**Phase 0 + Phase 1 全部完成2026-05-08
> **Unity 版本**2022.3.62f1c1 LTS
> **目标读者**:在 Unity Editor 中逐步验证当前实现是否符合预期功能
---
## 目录
1. [验证前准备](#1-验证前准备)
2. [场景搭建参考](#2-场景搭建参考)
3. [编辑器扩展工具一览](#3-编辑器扩展工具一览)
4. [各系统验证步骤](#4-各系统验证步骤)
- [V1服务层启动顺序](#v1服务层启动顺序)
- [V2Addressables 完整性检查](#v2addressables-完整性检查)
- [V3SO 事件系统与 EventBusMonitorWindow](#v3so-事件系统与-eventbusmonitorwindow)
- [V4输入系统](#v4输入系统)
- [V5玩家移动与 Animancer FSM](#v5玩家移动与-animancer-fsm)
- [V6战斗管道8 步验证)](#v6战斗管道8-步验证)
- [V7敌人 AI 寻路Behavior Designer](#v7敌人-ai-寻路behavior-designer)
- [V8存档点读写](#v8存档点读写)
- [V9死亡与复活流程](#v9死亡与复活流程)
- [V10HUD 与 UI 管理](#v10hud-与-ui-管理)
- [V11音频钩子](#v11音频钩子)
- [V12VFX 反馈](#v12vfx-反馈)
- [V13相机系统](#v13相机系统)
5. [完整可玩流程验证](#5-完整可玩流程验证)
6. [常见问题排查](#6-常见问题排查)
---
## 1. 验证前准备
### 1.1 编译状态确认
打开项目后执行以下检查:
1. **等待编译完成**Unity 左下角进度条消失,状态栏无旋转图标
2. **Console 检查**:菜单 `Window → General → Console`
- 点击 Console 右上角三个图标Error / Warning / Log全部开启
- 确认 **红色 Error 数量 = 0**(黄色 Warning 可容忍,但优先排查)
3. **Assembly 引用方向验证**(通用做法):
- 某些 Unity 版本不会显示 `Assembly Version Validation`,这属于正常情况
- 直接以“能否正常编译 + 是否有程序集引用错误”作为判定标准:
1. 菜单 `Assets → Open C# Project`
2. 在 IDERider/Visual Studio执行一次 Rebuild
3. 回到 Unity Console确认没有 asmdef/程序集循环引用相关错误
### 1.2 Addressables 构建
> 若跳过此步骤,运行时会出现 `InvalidKeyException` 或资产加载失败。
**步骤**
1. 菜单 `Window → Asset Management → Addressables → Groups`
2. 在弹出窗口中,点击左上角 **Build** 下拉菜单
3. 选择 `New Build → Default Build Script`
4. 等待右下角进度条完成
5. Console 中确认无 `[AddressKeyValidator] ❌` 错误(见 [V2](#v2addressables-完整性检查)
### 1.3 NavSurface 烘焙
> PathBerserker2d 的寻路完全依赖烘焙结果,未烘焙时敌人原地站立无响应。
**步骤**
1. 在测试房间场景中选中挂有 `NavSurface` 组件的 GameObject
2. 在 Inspector 中找到 NavSurface 组件
3. 点击组件右上角的 **Bake** 按钮
4. 成功后 Scene 视图中地面会显示**蓝绿色半透明网格** Gizmo
> **提示**:若 Scene 视图看不到 Gizmo点击 Scene 视图右上角 `Gizmos` 下拉 → 确认 `PathBerserker2d` 相关项已勾选
### 1.4 SO 事件频道资产确认
1. 在 Project 窗口导航到 `Assets/Data/Events/`
2. 确认存在若干 `.asset` 文件(以 `EVT_` 开头命名)
3. 若目录为空:菜单 `BaseGames → Tools → Create Event Channel Assets`(一键生成全部全局频道资产)
4. 若部分资产 Inspector 显示 `Script = None (Mono Script)`
- 先执行 `BaseGames → Tools → Reimport Event Channel Assets`
- 再执行 `BaseGames → Tools → Create Event Channel Assets`
- 若仍异常,右键 `Assets/Data/Events` 执行 Reimport 后重试
### 1.5 Physics 2D 碰撞矩阵设置
> HitBox / HurtBox 依赖 Layer 碰撞矩阵,未配置时攻击不触发伤害。
**步骤**
1. 菜单 `Edit → Project Settings → Physics 2D`
2. 滚动到底部 **Layer Collision Matrix** 区域
3. 确认以下组合已勾选(绿色):
| 行 Layer | 列 Layer | 说明 |
|---------|---------|------|
| `PlayerHitBox` | `EnemyHurtBox` | 玩家攻击打敌人 |
| `EnemyHitBox` | `PlayerHurtBox` | 敌人攻击打玩家 |
| `Player` | `Ground` | 玩家落地检测 |
| `Enemy` | `Ground` | 敌人落地检测 |
---
## 2. 场景搭建参考
### 2.1 Persistent 场景 —— `Assets/Scenes/Persistent.unity`
此场景全程常驻(`DontDestroyOnLoad`负责全局服务层。Hierarchy 层级参考:
```
[Persistent]
├── [Services]
│ ├── GameServiceRegistrar 组件: GameServiceRegistrar
│ │ DefaultExecutionOrder: -2000
│ │ Inspector 字段(必须全部拖拽赋值):
│ │ _deathRespawnService → DeathRespawnService.cs 所在 GameObject
│ │ _sceneService → SceneService.cs 所在 GameObject
│ │ _eventChannelRegistry → EventChannelRegistry.cs 所在 GameObject
│ │
│ ├── GameManager 组件: GameManager
│ │ DefaultExecutionOrder: -1000
│ │ Inspector 字段Listen 频道,全部拖拽 .asset:
│ │ _onPlayerDied → Assets/Data/Events/EVT_PlayerDied.asset
│ │ _onPauseRequested → Assets/Data/Events/EVT_PauseRequested.asset
│ │ _onBossFightStarted → Assets/Data/Events/EVT_BossFightStarted.asset
│ │ _onBossFightEnded → Assets/Data/Events/EVT_BossFightEnded.asset
│ │ _onDeathScreenConfirmed → Assets/Data/Events/EVT_DeathScreenConfirmed.asset
│ │ Inspector 字段Raise 频道):
│ │ _onGameStateChanged → Assets/Data/Events/EVT_GameStateChanged.asset
│ │ _onPlayerRespawned → Assets/Data/Events/EVT_PlayerRespawned.asset
│ │ Inspector 字段(子系统引用):
│ │ _sceneLoader → SceneLoader GameObject
│ │ _objectPool → GlobalObjectPool GameObject
│ │ _settingsManager → SettingsManager GameObject
│ │
│ └── AudioManager 组件: AudioManager
│ DefaultExecutionOrder: -500
│ Inspector 字段:
│ _audioMixer → Assets/Audio/MainMixer.mixer
├── [Input]
│ └── InputReaderHolder
│ _inputReader → Assets/Data/Input/InputReader.assetInputReaderSO
├── [Camera]
│ └── CameraStateController 组件: CameraStateController
└── [UI]
└── UIRoot 组件: UIManager
_hudController → HUDController GameObject 引用
_deathScreenController → DeathScreenController GameObject 引用
```
### 2.2 测试房间场景 —— `Assets/Scenes/Rooms/TestRoom.unity`
```
[TestRoom]
├── [Environment]
│ ├── Ground
│ │ 组件: Tilemap, TilemapCollider2DUsedByComposite=true
│ │ 组件: CompositeCollider2DGeometry Type: Polygons
│ │ Layer: Ground
│ └── NavSurfaceRoot
│ 组件: NavSurfacePathBerserker2d← 已烘焙
├── [Player]
│ └── Player Layer: Player
│ 组件: PlayerController
│ _statsConfig → Assets/Data/Player/PlayerStats.asset
│ _formConfig → Assets/Data/Player/FormConfig.asset
│ 组件: InputBuffer
│ _inputReader → Assets/Data/Input/InputReader.asset
│ _jumpBufferDuration = 0.15
│ _attackBufferDuration = 0.12
│ _dashBufferDuration = 0.10
│ 组件: Rigidbody2D
│ Body Type: Dynamic | Gravity Scale: 2 | Constraints: Freeze Rotation Z
│ 组件: AnimancerComponentCulling Mode: Always Animate
│ ├── HitBox Layer: PlayerHitBox
│ │ 组件: BoxCollider2DIs Trigger: ✅)
│ └── HurtBox Layer: PlayerHurtBox
│ 组件: CapsuleCollider2DIs Trigger: ✅)
├── [Enemy]
│ └── BasicEnemy Layer: Enemy
│ 组件: EnemyBase
│ 组件: EnemyStats
│ _statsSO → Assets/Data/Enemies/BasicEnemyStats.asset
│ 组件: NavAgentComponentPathBerserker2d← 需指向已烘焙的 NavSurface
│ 组件: BehaviorTreeBehavior Designer RuntimeController
│ └── HurtBox Layer: EnemyHurtBox
│ 组件: CapsuleCollider2DIs Trigger: ✅)
├── [SavePoint]
│ └── SavePointObject
│ 组件: SavePoint
│ _onSavePointActivated → Assets/Data/Events/EVT_SavePointActivated.asset
│ 组件: BoxCollider2DIs Trigger: ✅)
├── [Camera]
│ └── RoomCamera
│ 组件: CinemachineVirtualCamera
│ Follow → Player Transform
│ ├── CinemachineConfiner2D Extension
│ │ Bounding Shape 2D → 下方 RoomBoundary Collider
│ └── RoomBoundary
│ 组件: PolygonCollider2D仅定义边界无物理响应
└── [UI]
├── HUD CanvasScreen Space - OverlaySort Order: 0
│ └── HUDRoot
│ 组件: HUDController
│ _onHPChanged → Assets/Data/Events/EVT_HPChanged.asset
│ _onGeoChanged → Assets/Data/Events/EVT_GeoChanged.asset
└── DeathScreen CanvasScreen Space - OverlaySort Order: 10
初始: SetActive(false)
└── DeathScreenRoot
组件: DeathScreenController
_onDeathScreenConfirmed → Assets/Data/Events/EVT_DeathScreenConfirmed.asset
```
> **关键原则**`GameManager` 和 `DeathScreenController` 必须引用**同一个** `EVT_DeathScreenConfirmed.asset`,否则事件链断裂。
---
## 3. 编辑器扩展工具一览
Phase 0/1 实现了以下 Editor-only 工具,验证过程中会频繁用到:
| 工具 | 菜单路径 | 快捷键 | 用途 |
|------|---------|--------|------|
| **Event Bus Monitor** | `BaseGames → Tools → Event Bus Monitor` | `Ctrl+Shift+E` | Play 模式下实时查看所有 SO 事件触发记录 |
| **Validate AddressKeys** | `Tools → Validate AddressKeys` | — | 手动校验 AddressKeys 常量与 Addressable 分组一致性 |
| **Animancer Window** | `Window → Animation → Animancer` | — | 查看当前播放的动画状态和混合树 |
| **Behavior Designer** | 选中敌人 → Inspector → Open | — | 实时观察 BT 节点执行路径 |
| **Audio Mixer** | `Window → Audio → Audio Mixer` | — | 查看实时音频电平 |
| **Addressables Groups** | `Window → Asset Management → Addressables → Groups` | — | 管理和构建 Addressable 资产 |
| **Physics 2D Settings** | `Edit → Project Settings → Physics 2D` | — | 配置碰撞矩阵 |
---
## 4. 各系统验证步骤
---
### V1服务层启动顺序
**验证目标**:各 Manager 按 ExecutionOrder 正确初始化ServiceLocator 无遗漏注册。
#### 步骤 1确认执行顺序配置
1. 菜单 `Edit → Project Settings → Script Execution Order`
2. 确认以下顺序存在(数字越小越先执行):
| 脚本 | ExecutionOrder |
|------|---------------|
| `GameServiceRegistrar` | -2000 |
| `GameManager` | -1000 |
| `SceneService` | -900 |
| `SaveManager` | -900 |
| `AudioManager` | -500 |
| `PlayerController` | -100 |
#### 步骤 2Play 模式验证
1. 打开包含 `Persistent.unity` 的场景
2.**Play**
3. 查看 Console预期输出顺序
```
[GameServiceRegistrar] Registering services...
[GameManager] Awake
[AudioManager] Registered as IAudioService
```
#### 步骤 3ServiceLocator 状态验证
在任意 MonoBehaviour 的 `Start()` 中临时添加以下代码Play 后观察 Console
```csharp
void Start()
{
Debug.Log($"IAudioService: {ServiceLocator.GetOrDefault<IAudioService>()?.GetType().Name}");
Debug.Log($"IDeathRespawnService: {ServiceLocator.GetOrDefault<IDeathRespawnService>()?.GetType().Name}");
Debug.Log($"ISceneService: {ServiceLocator.GetOrDefault<ISceneService>()?.GetType().Name}");
}
```
**预期输出**
```
IAudioService: AudioManager
IDeathRespawnService: DeathRespawnService
ISceneService: SceneService
```
---
### V2Addressables 完整性检查
**验证目标**:所有 `AddressKeys` 常量都能在 Addressable 分组中找到对应条目。
#### 步骤 1手动触发验证
1. 菜单 `Tools → Validate AddressKeys`
2. 查看 Console
**预期结果**
```
[AddressKeyValidator] ✅ 所有 AddressKey 均有效。
```
**若出现错误**
```
[AddressKeyValidator] ❌ 发现 N 个失效 Key
AddressKeys.PrefabPlayer = "PLY_Player" → 在 Addressable 中未找到
```
**修复方法**
1. 菜单 `Window → Asset Management → Addressables → Groups`
2. 在对应分组中找到 Prefab → 右键 → `Copy Address`
3. 将复制的地址更新到 `Assets/Scripts/Core/Assets/AddressKeys.cs` 对应常量中
#### 步骤 2自动触发验证
`AddressKeyImportWatcher` 会在 Addressable 分组 `.asset` 修改后自动运行验证。
修改任意 Addressable 分组后Console 应自动出现验证结果,无需手动触发。
---
### V3SO 事件系统与 EventBusMonitorWindow
**验证目标**:事件频道在 Play 模式下正确触发EventBusMonitorWindow 能追踪事件流。
#### 步骤 1打开 EventBusMonitorWindow
1. **菜单路径**`BaseGames → Tools → Event Bus Monitor`
2. **快捷键**`Ctrl+Shift+E`
3. 窗口会显示工具栏 + 列表区域
#### 步骤 2认识 UI 布局
```
┌─────────────────────────────────────────────────────────────────────┐
│ Filter: [____________] [Pause] [Auto Scroll] [Clear] │
├──────────┬────────┬────────────────────────┬──────────────┬─────────┤
│ Time │ Frame │ Channel │ Payload │ Subs │
├──────────┼────────┼────────────────────────┼──────────────┼─────────┤
│ 12.34s │ #512 │ EVT_PlayerDied │ <void> │ 2 │← 正常(白色)
│ 12.38s │ #514 │ EVT_GameStateChanged │ Dead │ 0 │← 警告(红色:无订阅者)
└──────────┴────────┴────────────────────────┴──────────────┴─────────┘
```
**列说明**
- **Time**:触发时的 `Time.realtimeSinceStartup`(秒)
- **Frame**`Time.frameCount`(帧号)
- **Channel**SO 资产名称(即 `.asset` 文件名)
- **Payload**:事件负载的 `ToString()` 结果
- **Subs**:触发时的订阅者数量(**0 = 红色警告**,表示事件无人监听)
#### 步骤 3过滤器使用
- **Filter 输入框**:输入关键字(如 `Player`),只显示含此字符串的频道记录
- **Pause 按钮**:暂停捕获新记录(方便逐条查看已记录的事件)
- **Clear 按钮**:清空所有记录(建议每次新测试前清空)
- **Auto Scroll**:保持滚动到最新记录(追踪实时事件时开启)
#### 步骤 4验证事件链连通性
1.**Play**,打开 EventBusMonitorWindow
2. 在 Filter 框输入 `Player`
3. 触发各操作,确认对应事件出现在列表中,且 **Subs 列 > 0**
| 操作 | 预期出现的频道 | 最低 Subs 数 |
|------|-------------|------------|
| 玩家受伤 | `EVT_HPChanged` | 1HUDController 订阅)|
| 玩家死亡 | `EVT_PlayerDied` | 1GameManager 订阅)|
| 激活存档点 | `EVT_SavePointActivated` | 1GameManager 订阅)|
| 命中敌人 | `EVT_HitConfirmed` | 1CombatSFXController 等)|
> **Subs = 0 时**(红色行)意味着该事件频道有触发但无人响应——通常是 Inspector 字段未拖拽赋值,立即检查相关组件。
---
### V4输入系统
**验证目标**:物理按键正确映射到 InputReaderSO 事件InputBuffer 缓冲窗口生效。
#### 步骤 1Inspector 字段核查
选中 `InputBuffer` 组件(挂在 Player 上):
| 字段 | 预期值 |
|------|--------|
| `_inputReader` | 已拖拽 `InputReader.asset`(非 None|
| `_jumpBufferDuration` | **0.15** |
| `_attackBufferDuration` | **0.12** |
| `_dashBufferDuration` | **0.10** |
#### 步骤 2InputActions 配置验证
1. 双击 `Assets/Settings/PlayerInputActions.inputactions` 打开 Input System 编辑器
2. 确认以下 **Action Maps** 存在:
- `Gameplay`(含 Move / Jump / Attack / Dash / Parry 等 Actions
- `UI`(含 Navigate / Submit / Cancel 等)
- `Cutscene`(含 Skip 等)
3. 检查 `Jump` Action 的 Binding
- `Keyboard``Space`
- `Gamepad``Button South`(南键)
#### 步骤 3缓冲窗口验证跳跃缓冲
1. 让玩家跳跃到空中
2. **在落地前约 100150ms**(目测约 35 帧前)按下跳跃键
3. 落地后玩家应立即弹起(缓冲生效),而非落地后需重新按键
> **缓冲失效排查**:确认 `InputBuffer` 组件未被 `enabled = false`,且 `Update()` 正常执行。
---
### V5玩家移动与 Animancer FSM
**验证目标**Animancer FSM 在五个基础状态间正确切换,无动画卡顿或状态锁死。
#### 步骤 1打开 Animancer 调试窗口
1. **菜单路径**`Window → Animation → Animancer`
2. 窗口打开后显示当前所有 Animancer 实例
3. **Play 模式下**选中 Player GameObject
4. 窗口中出现该对象的 Animancer 状态树:
```
[AnimancerComponent] (Player)
└── Current: IdleState → 播放 "Idle" clip
NormalizedTime: 0.00 → 0.99 → 循环
Speed: 1.0
```
**Animancer 窗口关键指标**
- **Current**:当前激活的状态名称
- **NormalizedTime**动画播放进度0 = 开始1 = 结束);循环动画会在 0~1 之间反复
- **Speed**播放速度倍率1.0 = 正常)
- **Weight**:混合权重(单状态时 = 1.0
#### 步骤 2状态切换测试
按下表逐步操作并观察 Animancer 窗口中 **Current** 一行的变化:
| 操作 | 预期 Current State | Animancer 窗口显示 |
|------|-------------------|-------------------|
| 不操作 | `IdleState` | "Idle" clipNormalizedTime 循环 |
| 按住 A/D移动 | `RunState` | "Run" clipNormalizedTime 循环 |
| 按空格(跳跃) | `JumpState` | "Jump" clipNormalizedTime 0→1非循环 |
| 跳跃下落阶段 | `FallState` | "Fall" clipNormalizedTime 循环 |
| 落地 | `IdleState``RunState` | 对应 clip 恢复 |
| 按攻击键 | `AttackState` | "Attack" clip播放完毕后自动返回 |
#### 步骤 3PlayerController 字段核查
选中 Player 上的 `PlayerController` 组件,确认:
| 字段 | 预期值 |
|------|--------|
| `_statsConfig` | 已拖拽 `PlayerStats.asset`PlayerStatsSO|
| `_formConfig` | 已拖拽 `FormConfig.asset`FormConfigSO|
---
### V6战斗管道8 步验证)
**验证目标**:完整验证 HurtBox 的 8 步伤害处理流水线。
#### 步骤 1Physics 2D Layer 矩阵确认(战斗前必做)
1. 菜单 `Edit → Project Settings → Physics 2D`
2. 找到 **Layer Collision Matrix**(矩阵表格)
3. 确认 `PlayerHitBox` × `EnemyHurtBox` 格子已勾选(显示小方块)
#### 步骤 2基础命中验证
1. Play 后将玩家移动到敌人旁,按攻击键
2. **预期 Console 输出**
```
[HurtBox] Received: Amount=3, Type=Normal, FinalDamage=2
[EnemyBase] TakeDamage: HP 10 → 8
```
3.**EventBusMonitorWindow** 中过滤 `HitConfirmed`
- 确认 `EVT_HitConfirmed` 出现Subs ≥ 1
#### 步骤 3无敌帧验证
1. 选中 Player 上的 `HurtBox` 组件
2. Inspector 中临时将 `_isHurtBoxInvincible` 勾选为 **true**
3. 让敌人攻击玩家
4. **预期**Console 无伤害日志HP 不变
5. 验证后**取消勾选**
#### 步骤 4防御计算验证
1. 选中敌人 `EnemyStats` → Inspector 将 `Defense` 临时改为 **2**
2. 攻击基础伤害为 3 时:`FinalDamage = Max(1, 3-2) = 1`
3. Console 确认 `FinalDamage=1`(而非 3
4. 恢复 Defense 值
#### 步骤 5事件广播确认
1. 攻击命中后EventBusMonitorWindow 中确认:
- `EVT_HitConfirmed` 有记录Subs ≥ 1
- `EVT_HPChanged` 有记录(敌人 HP 变化广播)
---
### V7敌人 AI 寻路Behavior Designer
**验证目标**:敌人在 NavSurface 上正确执行巡逻 → 追击 → 攻击行为树。
#### 步骤 1打开 Behavior Tree 调试窗口
1. **Play 模式下**选中敌人 GameObject
2. 在 Inspector 中找到 **BehaviorTree**Behavior Designer RuntimeController组件
3. 点击组件中的 **`Open Behavior Designer`** 按钮
4. Behavior Designer 窗口打开,显示当前 BT 节点图
**节点颜色含义**
| 颜色 | 含义 |
|------|------|
| 🟢 绿色 | 当前正在执行的节点 |
| ⬜ 灰色 | 未执行 / 条件未满足 |
| 🔴 红色 | 条件不满足Conditional 类节点返回 Failure|
| 🔵 蓝色 | 上一次执行成功(已完成) |
#### 步骤 2Variable Monitor 面板Behavior Designer 内置)
1. 在 Behavior Designer 窗口中,点击左侧工具栏的 **Variables** 面板(或顶部菜单 View → Variables
2. 可以看到 BT 黑板中定义的变量,如:
- `PlayerTransform`SharedTransform追击目标
- `IsPlayerInRange`SharedBool是否检测到玩家
- `PatrolIndex`SharedInt当前巡逻点索引
3. **Play 模式下**让玩家靠近敌人 → 观察 `IsPlayerInRange` 实时变为 `true`
#### 步骤 3巡逻状态验证
1. 将玩家放在敌人**视野范围之外**(距离 > `BD_IsPlayerInRange` 检测半径)
2. 观察 Behavior Designer 窗口:
- `BD_Patrol` 节点高亮 **绿色**
- `BD_IsPlayerInRange` 节点为**红色**(条件不满足)
3. 场景中敌人在 Waypoints 之间往返移动
**BD_Patrol 组件 Inspector 字段核查**(选中敌人 BT 中的 BD_Patrol 任务节点):
- `Waypoints`Transform 数组(至少 2 个),每个指向场景内的巡逻点 GameObject
- `MoveSpeed`:巡逻速度(浮点数)
#### 步骤 4追击状态验证
1. 将玩家移入检测范围(靠近敌人)
2. **预期**
- Behavior Designer 中 `BD_IsPlayerInRange` 变**绿色**Subs 满足)
- `BD_Patrol` 停止
- `BD_MoveToPlayer` 开始执行(绿色)
- Variables 面板中 `IsPlayerInRange = true`
3. 敌人转向并向玩家寻路
#### 步骤 5NavSurface Gizmo 确认
1. Scene 视图确认地面有**蓝绿色半透明网格**(已烘焙)
2. 若没有:选中 NavSurface 组件 → 点击 `Bake`
---
### V8存档点读写
**验证目标**:激活存档点后数据正确写入磁盘,重新 Play 后玩家从存档位置复位。
#### 步骤 1确认存档路径
```
Windows: C:\Users\{用户名}\AppData\LocalLow\{公司名}\{产品名}\Saves\save_slot0.json
```
**快速定位**:在任意 MonoBehaviour 中临时添加:
```csharp
void Start() => Debug.Log(Application.persistentDataPath);
```
Play 后 Console 中点击该日志 → 直接跳转到文件夹。
#### 步骤 2激活存档点
1. Play 后将玩家移动到 `SavePoint` 碰撞体范围内
2.`E` 键交互(具体按键见 `PlayerInputActions.inputactions``Interact` Action
3. **Console 预期**
```
[SaveManager] SaveAsync() started...
[LocalFileStorage] WriteAsync: save_slot0.json
[SaveManager] SaveAsync() completed. Version: 2.1
```
4. EventBusMonitorWindow 中确认 `EVT_SavePointActivated` 有记录Subs ≥ 1
#### 步骤 3验证存档 JSON 结构
1. 不退出 Play用文本编辑器打开 `save_slot0.json`
2. 确认关键字段已正确写入:
```json
{
"Meta": {
"Version": "2.1",
"SlotIndex": 0,
"LastSaved": "2026-05-08T...",
"SavePointId": "SavePoint_TestRoom_01",
"Checksum": "..."
},
"Player": {
"PosX": 3.5,
"PosY": -1.2,
"Scene": "TestRoom",
"CurrentHP": 5
}
}
```
#### 步骤 4验证加载
1. 退出 **Play 模式**
2. 重新按 **Play**
3. **预期**
- `SaveManager.HasSave()` 返回 `true`
- Console`[SaveManager] LoadAsync() completed. Restored player to (3.5, -1.2) in TestRoom`
- 玩家出生在步骤 2 中存档的位置
---
### V9死亡与复活流程
**验证目标**:完整死亡事件链按顺序执行,状态机转换正确,复活后状态恢复。
#### 步骤 0先完成一次存档必要前提
按 [V8 步骤 2](#步骤-2激活存档点) 完成存档后继续。
#### 步骤 1触发死亡
方法 AInspector 直接触发,最可控):
1. Play 模式下选中 `Assets/Data/Events/EVT_PlayerDied.asset`
2. 在 Inspector 中点击 **`Raise Event`** 按钮(若有 Editor 扩展)
3. 或在 `PlayerStats` 组件中将 `_currentHP` 改为 **0**,触发死亡逻辑
方法 B战斗死亡
- 关闭无敌帧,让敌人反复攻击直至 HP = 0
#### 步骤 2观察事件链配合 EventBusMonitorWindow
点击 **Clear** 后触发死亡,预期按时间顺序出现:
```
EVT_PlayerDied <void> Subs: 1 ← GameManager 接收
EVT_GameStateChanged Dead Subs: ≥1 ← 各 UI 组件订阅
```
同时:
- `DeathScreen Canvas` 在 Hierarchy 中变为 **Active**
- Game 视图出现死亡界面
#### 步骤 3确认 GameStateMachine 状态
选中 `GameManager` 组件 → 在 Inspector 的 Debug 视图中(点击右上角 Normal → Debug查看
- `_fsm``CurrentStateId` 字段值 = `"Dead"`
#### 步骤 4确认按钮响应与复活
1.`Space` / `Enter` 键(视 DeathScreenController 配置)
2. **预期 Console**
```
[DeathScreenController] Confirmed → raising EVT_DeathScreenConfirmed
[GameManager] DeathFlow() resumed after confirmation
[GameManager] Raising EVT_PlayerRespawned
[SceneLoader] ReloadFromSave() started
[GameManager] TransitionTo: Dead → Gameplay
```
3. EventBusMonitorWindow 依次出现:
```
EVT_DeathScreenConfirmed <void> Subs: 1
EVT_PlayerRespawned <void> Subs: ≥1
```
#### 步骤 5复活后状态验证
| 检查项 | 预期值 |
|--------|--------|
| `GameManager._fsm.CurrentStateId` | `Gameplay` |
| 玩家 HP | 已从存档恢复 |
| 玩家位置 | 在存档点坐标(误差 < 0.1|
| DeathScreen Canvas | `SetActive(false)`,界面隐藏 |
---
### V10HUD 与 UI 管理
**验证目标**HUDController 实时同步事件数据UIManager 面板栈管理正常。
#### 步骤 1HP 显示同步
1. Play 模式下,选中 Player → `PlayerStats` 组件
2. Inspector 中修改 `_currentHP`(如 5 → 3
3. 同时观察 Game 视图 HUD 的 HP 条 / 数字
4. **同步排查**EventBusMonitorWindow 过滤 `HP` → 确认 `EVT_HPChanged` 有记录
#### 步骤 2UIManager 面板栈验证
在测试脚本中:
```csharp
IEnumerator Start()
{
var ui = FindObjectOfType<UIManager>();
var deathCanvas = FindObjectOfType<DeathScreenController>().gameObject;
ui.OpenPanel(deathCanvas);
Debug.Log($"DeathScreen Active: {deathCanvas.activeSelf}"); // 预期: True
yield return new WaitForSeconds(1f);
ui.CloseTopPanel();
Debug.Log($"DeathScreen Active: {deathCanvas.activeSelf}"); // 预期: False
}
```
---
### V11音频钩子
**验证目标**AudioManager 已注册为 IAudioService事件链连通性正确。
#### 步骤 1AudioMixer 配置核查
1. 菜单 `Window → Audio → Audio Mixer`
2. 确认存在以下 Group 层级:
```
Master
├── BGM
└── SFX
├── Combat
└── Ambient
```
3. 确认各 Group 的 Volume 参数已 **Expose to Script**(右键 Volume → Expose to Script → 在 Inspector 顶部 Exposed Parameters 中命名)
4.`AudioMixerKeys.cs` 中确认常量名与 Exposed Parameter 名称**完全一致**(区分大小写)
#### 步骤 2ServiceLocator 注册验证
```csharp
void Start()
{
var audio = ServiceLocator.Get<IAudioService>();
Debug.Log($"IAudioService 实现类型: {audio.GetType().Name}");
// 预期: AudioManager
}
```
#### 步骤 3Phase 1 桩实现验证(不验证真实音效)
1. 玩家攻击命中敌人
2. **预期 Console**
```
[AudioManager] PlaySFX("hit_normal") — stub, Phase 2 will implement
```
这证明事件链从 `EVT_HitConfirmed``CombatSFXController``AudioManager.PlaySFX()` 已连通,只是音频播放留待 Phase 2 实现。
---
### V12VFX 反馈
**验证目标**:命中特效在正确位置生成并回池,受击白闪正常显示。
#### 步骤 1HitFX 生成位置验证
1. Play 并攻击敌人
2. 观察命中点:特效应在**敌人碰撞体位置**出现,而非世界原点 (0, 0, 0)
3. 若特效出现在原点:检查 `HitBox` 传给 `HitFXSpawner` 的坐标是否为 `contactPoint.point`
#### 步骤 2VFXPool 对象池验证
1. Play 模式下在 Hierarchy 展开 `VFXPool` 相关 GameObject
2. 触发多次攻击命中,观察:
- 特效播放时对应 GameObject 变为 **Active**(白色显示)
- 特效播放完毕后变为 **Inactive**(灰色),**而非被 Destroy**
> 若 VFX Prefab 不断被 Destroy说明 `PooledObject.ReturnToPool()` 未正确调用。
#### 步骤 3HurtFlash 白闪验证
1. 让敌人攻击玩家
2. 玩家 SpriteRenderer 应短暂(约 0.10.2 秒)变为**纯白色**,然后恢复
3. 若不变白:确认 `HurtFlashController` 挂在 Player 上,且 Sprite 材质支持 `_FlashAmount` Shader 参数
---
### V13相机系统
**验证目标**:相机锁定在房间边界内,像素对齐无亚像素抖动。
#### 步骤 1Cinemachine 配置核查
选中 `RoomCamera``CinemachineVirtualCamera` 组件,确认:
- `Follow`:已拖拽 Player Transform
- Extensions 列表中有 `CinemachineConfiner2D`
- `CinemachineConfiner2D.Bounding Shape 2D`:已指向房间边界 PolygonCollider2D
#### 步骤 2边界限制验证
1. Play 后将玩家移动到房间最左/右/上/下边缘
2. **预期**:相机到达边界后停止跟随(不超出房间范围)
3. **若超出边界**:检查 `CinemachineConfiner2D` 的 Bounding Shape 2D 字段是否已赋值
#### 步骤 3像素对齐验证
1. Game 视图右上角选择目标分辨率
2. Play 后**缓慢**移动玩家
3. 观察背景图块正确表现是平滑移动无1像素跳动或闪烁
**Pixel Perfect Camera 检查**
- `Assets Pixels Per Unit`:与 Sprite 资产 PPU 一致(如 16 或 32
- `Reference Resolution`:目标分辨率(如 320×180
#### 步骤 4CameraBlendProfileSO 混合验证
1. 场景中设置两个 `CameraTriggerZone`(对应两台 RoomCamera
2. 玩家穿越触发区边界
3. **预期**:约 0.5 秒内平滑过渡(而非瞬间切换)
4. 过渡曲线由 `CameraBlendProfileSO.ToBlendDefinition()` 返回的 blend struct 配置
---
## 5. 完整可玩流程验证
完成各系统单独验证后,执行以下端对端流程确认所有系统**联动**正常:
```
操作序列 对应检查点
─────────────────────────────────────────────────────────────────
1. 打开 Persistent.unity + TestRoom.unityAdditive 加载)
2. 按 Play
✅ 检查点1Console 无红色 Error
EventBusMonitorWindow 无 Subs=0 的红色行
3. 将玩家移至距出生点较远的位置
4. 与 SavePoint 交互(按 E 键)
✅ 检查点2Console 打印 SaveAsync() completed
EventBusMonitorWindow: EVT_SavePointActivated Subs≥1
save_slot0.json 中 Player.PosX / PosY 已更新
5. 将玩家移至敌人视野范围内(靠近约 5 格)
✅ 检查点3Behavior Designer: BD_IsPlayerInRange 高亮绿色
敌人开始向玩家移动BD_MoveToPlayer 绿色)
Variables 面板: IsPlayerInRange = true
6. 攻击敌人 3 次
✅ 检查点4Console 打印 3 条 DamageInfo
EventBusMonitorWindow: EVT_HitConfirmed 有 3 条记录
HitFX 特效在敌人位置出现(非世界原点)
Animancer 窗口: AttackState 快速切入切出
7. 让玩家 HP 降至 0
✅ 检查点5DeathScreen Canvas 在 Hierarchy 变为 Active
Game 视图显示死亡界面
EventBusMonitorWindow: EVT_PlayerDied → EVT_GameStateChanged(Dead)
GameManager._fsm.CurrentStateId = "Dead"
8. 按确认键Space
✅ 检查点6EventBusMonitorWindow: EVT_DeathScreenConfirmed → EVT_PlayerRespawned
Console: SceneLoader.ReloadFromSave() 执行
玩家复活在步骤 4 存档坐标(误差 < 0.1
GameManager._fsm.CurrentStateId = "Gameplay"
DeathScreen Canvas 变为 Inactive
9. 退出 Play 模式,重新按 Play
✅ 检查点7玩家出生在步骤 4 存档位置(非场景初始位置)
HUD 显示与存档一致的 HP 和 Geo 值
```
**7 个检查点全部通过** = Phase 1 功能验证完成,可以开始 Phase 2 开发。
---
## 6. 常见问题排查
### ❌ `[ServiceLocator] Service of type X not registered`
**排查步骤**
1. 选中 `GameServiceRegistrar` → Inspector 检查 `_deathRespawnService``_sceneService``_eventChannelRegistry` 字段
2. 若任一字段为 **None** → 将对应 GameObject 拖拽赋值
3. 确认 `GameServiceRegistrar.ExecutionOrder = -2000``Edit → Project Settings → Script Execution Order`
---
### ❌ EventBusMonitorWindow 中频道 Subs = 0红色行
**排查步骤**
1. 记录红色行的 Channel 名(如 `EVT_PlayerDied`
2. 找到应订阅此频道的组件(`EVT_PlayerDied` 应由 `GameManager` 订阅)
3. 选中该组件 → Inspector 检查对应字段是否指向**同一个** `.asset`(而非 None 或不同资产)
4. 若为 None → 拖拽 `Assets/Data/Events/EVT_{Name}.asset` 赋值
---
### ❌ 玩家动画卡死在某个状态
**排查步骤**
1. `Window → Animation → Animancer` → 观察 Current State 名称和 NormalizedTime
2. 若 NormalizedTime 停滞:检查 `PlayerAnimationConfigSO` 中对应 AnimationClip 字段是否已赋值
3. Console 检查是否有与 `PlayerController` 相关的 `NullReferenceException`
---
### ❌ 敌人原地站立不移动
**排查步骤**
1. Scene 视图确认地面有蓝绿色网格 Gizmo
2. 若无 → NavSurface 组件 → 点击 `Bake`
3. 选中敌人 → `NavAgentComponent``NavSurface` 字段是否指向已烘焙的 NavSurface
---
### ❌ 攻击命中后敌人 HP 不减少
**排查步骤**
1. `Edit → Project Settings → Physics 2D` → 确认 `PlayerHitBox``EnemyHurtBox` 碰撞已勾选
2. 选中 Player HitBox 子对象 → Collider2D **Is Trigger = ✅**
3. 选中 Enemy HurtBox 子对象 → Collider2D **Is Trigger = ✅**
4. 确认 Layer 设置Player HitBox → `PlayerHitBox`Enemy HurtBox → `EnemyHurtBox`
---
### ❌ 存档后重新 Play 玩家仍在场景初始位置
**排查步骤**
1. 临时添加 `Debug.Log(Application.persistentDataPath)` → 打开文件夹确认 `save_slot0.json` 存在
2. Console 确认是否有 `[SaveManager] LoadAsync() completed` 日志
3. 若无日志:检查启动流程中是否调用了 `SaveManager.LoadAsync()`
4. 确认 `SaveManager.HasSave()` 返回 `true`
---
### ❌ 死亡界面出现但按确认键无响应
**排查步骤**
1. 选中 `DeathScreenController` → Inspector 中 `_onDeathScreenConfirmed` 字段 → **双击**该资产Project 窗口高亮对应 `.asset`,记录路径
2. 选中 `GameManager` → 同样双击 `_onDeathScreenConfirmed` 字段的资产
3. 若两者高亮的是**不同文件** → 统一引用同一个 `.asset`
---
### ❌ VFX 特效出现在世界原点 (0, 0, 0)
**排查步骤**
1. 检查 `HitBox.OnTriggerEnter2D` 回调,确认传给 `HitFXSpawner` 的位置参数为:
```csharp
other.ClosestPoint(transform.position) // 推荐
// 或
other.transform.position
```
而非 `Vector2.zero` 或 `transform.position`HitBox 自身位置)
---
*文档版本2.0 | 对应开发进度Phase 0 + Phase 1 完成 | 更新日期2026-05-08*