Files
zeling_v2/Docs/Verification/Phase1_Verification_Guide.md

1096 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 → Tags and Layers`
2. 在 User Layer 中创建以下 Layer名称必须完全一致区分大小写
| Layer 名称 | 用途 |
|-----------|------|
| `Player` | 玩家主物体Rigidbody2D 所在) |
| `Enemy` | 敌人主物体Rigidbody2D/导航代理所在) |
| `Ground` | 地面、墙体、平台等场景碰撞体 |
| `PlayerHitBox` | 玩家攻击判定触发器 |
| `PlayerHurtBox` | 玩家受击判定触发器 |
| `EnemyHitBox` | 敌人攻击判定触发器 |
| `EnemyHurtBox` | 敌人受击判定触发器 |
| `TriggerZone` | 存档点、相机区、剧情触发区 |
3. 给对象分配 LayerScene 中逐个核对):
| 对象 | Layer |
|------|-------|
| Player 根节点(含 Rigidbody2D | `Player` |
| Player/HitBox 子节点 | `PlayerHitBox` |
| Player/HurtBox 子节点 | `PlayerHurtBox` |
| Enemy 根节点(含 Rigidbody2D 或 NavAgent | `Enemy` |
| Enemy/HitBox 子节点(如果有) | `EnemyHitBox` |
| Enemy/HurtBox 子节点 | `EnemyHurtBox` |
| Tilemap Ground、静态障碍 | `Ground` |
| SavePoint、CameraTrigger、RoomTrigger | `TriggerZone` |
4. 打开菜单 `Edit → Project Settings → Physics 2D`
5. 滚动到底部 `Layer Collision Matrix`
6. 按 Unity `Layer Collision Matrix` 逐格设置(与你截图中的排列一致):
当前矩阵显示顺序(从上到下/从左到右)为:
| 序号 | Layer |
|------|-------|
| 0 | `Default` |
| 1 | `TransparentFX` |
| 2 | `Ignore Raycast` |
| 3 | `Player` |
| 4 | `Water` |
| 5 | `UI` |
| 6 | `Enemy` |
| 7 | `Ground` |
| 8 | `PlayerHitBox` |
| 9 | `PlayerHurtBox` |
| 10 | `EnemyHitBox` |
| 11 | `EnemyHurtBox` |
| 12 | `TriggerZone` |
在该排列下,只需要重点修改以下交叉格(其余保持默认即可):
| 行 Layer | 列 Layer | 设置 |
|---------|---------|------|
| `Player` | `Ground` | ✅ |
| `Enemy` | `Ground` | ✅ |
| `Player` | `Enemy` | ✅ |
| `PlayerHitBox` | `EnemyHurtBox` | ✅ |
| `EnemyHitBox` | `PlayerHurtBox` | ✅ |
| `TriggerZone` | `Player` | ✅ |
| `PlayerHitBox` | `Ground` | ❌ |
| `EnemyHitBox` | `Ground` | ❌ |
| `PlayerHurtBox` | `Ground` | ❌ |
| `EnemyHurtBox` | `Ground` | ❌ |
| `PlayerHitBox` | `PlayerHurtBox` | ❌ |
| `EnemyHitBox` | `EnemyHurtBox` | ❌ |
| `PlayerHitBox` | `Player` | ❌ |
| `EnemyHitBox` | `Enemy` | ❌ |
| `TriggerZone` | `Enemy` | ❌ |
定位技巧(与 Unity 面板一致):
- Unity 只显示下三角矩阵,`A × B``B × A` 是同一个格
- 找不到某一格时,优先在“行名较靠下”的那一行里找对应列
- 先设置 5 个核心开启项:`Player-Ground``Enemy-Ground``PlayerHitBox-EnemyHurtBox``EnemyHitBox-PlayerHurtBox``TriggerZone-Player`
7. 组件级别补充设置(和矩阵一起生效):
- HitBox 与 HurtBox 的 Collider2D 必须 `Is Trigger = true`
- Player/Enemy 主体 Collider2D 必须 `Is Trigger = false`
- 至少一方需要 Rigidbody2D 才会触发 Trigger 回调(本项目通常在主体根节点)
8. 快速验证:
- 玩家攻击敌人:应触发 `EVT_HitConfirmed`
- 敌人攻击玩家:玩家 HP 下降且 HUD 刷新
- 玩家接触 SavePoint应触发 `EVT_SavePointActivated`
### 1.6 本文统一测试场景与加载方式(必须按此执行)
为避免“服务层未加载”或“房间场景缺对象”的误判,本文所有验证默认使用以下场景组合:
1. 打开 `Assets/Scenes/Persistent.unity`
2. 使用 Additive 方式再打开 `Assets/Scenes/TestRoom.unity`
3. 在 Hierarchy 中确认 `Persistent``TestRoom` 两个场景同时处于已加载状态
4. 若当前只开了 `TestRoom.unity`,请先停止测试并改为上述双场景组合后再执行
> 例外:仅测试单纯美术或单房间静态内容时可只开 `TestRoom.unity`。但凡涉及输入、服务注册、事件总线、存档、死亡复活,必须使用双场景组合。
---
## 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/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. 编辑器扩展工具一览
当前验证会用到两类入口:
1. **BaseGames 自定义菜单工具**:会出现在 `BaseGames` 菜单下
2. **Unity / 第三方现成窗口**:仍然在各自原生菜单里,不会出现在 `BaseGames` 菜单下
| 工具 | 菜单路径 | 快捷键 | 用途 |
|------|---------|--------|------|
| **Event Bus Monitor** | `BaseGames → Tools → Event Bus Monitor` | `Ctrl+Shift+E` | Play 模式下实时查看所有 SO 事件触发记录 |
| **Create Event Channel Assets** | `BaseGames → Tools → Create Event Channel Assets` | — | 一键生成全局事件频道资产 |
| **Reimport Event Channel Assets** | `BaseGames → Tools → Reimport Event Channel Assets` | — | 批量重导入 `Assets/Data/Events` 下的事件资产 |
| **Validate Address Keys** | `BaseGames → Addressables → Validate Address Keys` | — | 手动校验 AddressKeys 常量与 Addressable 分组一致性 |
| **Scaffold Persistent Scene** | `BaseGames → Tools → Scaffold Persistent Scene` | — | 一键生成 Persistent 场景基础层级与核心组件骨架 |
| **Place Player / Enemy / Platform…** | `BaseGames → Scene → Place → …` | — | 单独放置玩家、敌人、地面、相机、存档点等场景对象(替代 Scaffold Test Room |
| **Apply Script Execution Order Preset** | `BaseGames → Tools → Apply Script Execution Order Preset` | — | 一键写入推荐的脚本执行顺序 |
| **Validate Script Execution Order Preset** | `BaseGames → Tools → Validate Script Execution Order Preset` | — | 校验当前执行顺序是否符合推荐值 |
| **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. 优先使用一键菜单:`BaseGames → Tools → Apply Script Execution Order Preset`
2. (可选)执行校验菜单:`BaseGames → Tools → Validate Script Execution Order Preset`
3. 若需人工复核,打开 `Edit → Project Settings → Script Execution Order`
4. 确认以下顺序存在(数字越小越先执行):
| 脚本 | ExecutionOrder |
|------|---------------|
| `GameServiceRegistrar` | -2000 |
| `GameManager` | -1000 |
| `SceneService` | -900 |
| `SaveManager` | -900 |
| `AudioManager` | -500 |
| `PlayerController` | -100 |
#### 步骤 2Play 模式验证
1. 按 [1.6 本文统一测试场景与加载方式](#16-本文统一测试场景与加载方式必须按此执行) 加载 `Persistent.unity + TestRoom.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. 菜单 `BaseGames → Addressables → Validate Address Keys`
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 缓冲窗口生效。
#### 步骤 0先进入正确场景必做
1. 按 [1.6 本文统一测试场景与加载方式](#16-本文统一测试场景与加载方式必须按此执行) 加载场景
2. 打开 `BaseGames → Tools → Event Bus Monitor`
3. 在 EventBusMonitorWindow 点击 **Clear** 清空历史记录
4. 在 Filter 输入框输入 `Pause`
#### 步骤 1InputReader 挂载与引用核查
1.`Persistent` 场景中选中 `InputReaderHolder`
2. 检查 `InputReaderBootstrap._inputReader`:必须已拖拽 `Assets/Data/Input/InputReader.asset`
3. 检查 `InputReader.asset`ScriptableObject中的 `_inputActions`:必须指向 `Assets/Settings/PlayerInputActions.inputactions`
4. 检查 `_onPauseRequested`:必须指向 `Assets/Data/Events/EVT_PauseRequested.asset`
#### 步骤 2Inspector 字段核查
选中 `InputBuffer` 组件(挂在 Player 上):
| 字段 | 预期值 |
|------|--------|
| `_inputReader` | 已拖拽 `InputReader.asset`(非 None|
| `_jumpBufferDuration` | **0.15** |
| `_attackBufferDuration` | **0.12** |
| `_dashBufferDuration` | **0.10** |
#### 步骤 3InputActions 配置验证
1. 双击 `Assets/Settings/PlayerInputActions.inputactions` 打开 Input System 编辑器
2. 确认以下 **Action Maps** 存在:
- `Gameplay`(含 Move / Jump / Attack / DownAttack / UpAttack / Parry / Dash / UseSpring / Switch* / SoulSkill / SpiritSkill* / Interact / Pause
- `UI`(含 Navigate / Submit / Cancel / Pause / Point
3. 检查 `Pause` Action 的 BindingGameplay 与 UI 均需存在):
- `Keyboard``Escape`
- `Gamepad``Start`
4. 检查 `Jump` Action 的 Binding
- `Keyboard``Space`
- `Gamepad``Button South`(南键)
#### 步骤 4Escape 事件链验证(本阶段主验证项)
1.**Play**
2. 按一次 `Escape`
3. 观察 Console必须出现一次链路日志
```
[InputReaderSO.HandlePause] PAUSE INPUT DETECTED!
[InputReaderSO.HandlePause] Invoking PauseEvent...
[InputReaderSO.HandlePause] Raising _onPauseRequested channel...
```
4. 观察 EventBusMonitorWindowFilter=Pause
- 应新增 1 条 `EVT_PauseRequested`
- `Subs` 应大于 0通常为 2
5. 再按一次 `Escape`
6. 再次确认仅新增 1 条 `EVT_PauseRequested`
> 通过标准:按 2 次 Escape新增 2 条 `EVT_PauseRequested`。若出现 4 条,表示重复绑定回归,需要排查 InputReaderSO 的重复 Bind。
#### 步骤 5缓冲窗口验证跳跃缓冲
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*