# 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服务层启动顺序) - [V2:Addressables 完整性检查](#v2addressables-完整性检查) - [V3:SO 事件系统与 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死亡与复活流程) - [V10:HUD 与 UI 管理](#v10hud-与-ui-管理) - [V11:音频钩子](#v11音频钩子) - [V12:VFX 反馈](#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. 在 IDE(Rider/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. 给对象分配 Layer(Scene 中逐个核对): | 对象 | 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.asset(InputReaderSO) │ ├── [Camera] │ └── CameraStateController 组件: CameraStateController │ └── [UI] └── UIRoot 组件: UIManager _hudController → HUDController GameObject 引用 _deathScreenController → DeathScreenController GameObject 引用 ``` ### 2.2 测试房间场景 —— `Assets/Scenes/TestRoom.unity` ``` [TestRoom] ├── [Environment] │ ├── Ground │ │ 组件: Tilemap, TilemapCollider2D(UsedByComposite=true) │ │ 组件: CompositeCollider2D(Geometry Type: Polygons) │ │ Layer: Ground │ └── NavSurfaceRoot │ 组件: NavSurface(PathBerserker2d)← 已烘焙 │ ├── [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 │ 组件: AnimancerComponent(Culling Mode: Always Animate) │ ├── HitBox Layer: PlayerHitBox │ │ 组件: BoxCollider2D(Is Trigger: ✅) │ └── HurtBox Layer: PlayerHurtBox │ 组件: CapsuleCollider2D(Is Trigger: ✅) │ ├── [Enemy] │ └── BasicEnemy Layer: Enemy │ 组件: EnemyBase │ 组件: EnemyStats │ _statsSO → Assets/Data/Enemies/BasicEnemyStats.asset │ 组件: NavAgentComponent(PathBerserker2d)← 需指向已烘焙的 NavSurface │ 组件: BehaviorTree(Behavior Designer RuntimeController) │ └── HurtBox Layer: EnemyHurtBox │ 组件: CapsuleCollider2D(Is Trigger: ✅) │ ├── [SavePoint] │ └── SavePointObject │ 组件: SavePoint │ _onSavePointActivated → Assets/Data/Events/EVT_SavePointActivated.asset │ 组件: BoxCollider2D(Is Trigger: ✅) │ ├── [Camera] │ └── RoomCamera │ 组件: CinemachineVirtualCamera │ Follow → Player Transform │ ├── CinemachineConfiner2D Extension │ │ Bounding Shape 2D → 下方 RoomBoundary Collider │ └── RoomBoundary │ 组件: PolygonCollider2D(仅定义边界,无物理响应) │ └── [UI] ├── HUD Canvas(Screen Space - Overlay,Sort Order: 0) │ └── HUDRoot │ 组件: HUDController │ _onHPChanged → Assets/Data/Events/EVT_HPChanged.asset │ _onGeoChanged → Assets/Data/Events/EVT_GeoChanged.asset └── DeathScreen Canvas(Screen Space - Overlay,Sort 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 → Tools → Validate Address Keys` | — | 手动校验 AddressKeys 常量与 Addressable 分组一致性 | | **Scaffold Persistent Scene** | `BaseGames → Tools → Scaffold Persistent Scene` | — | 一键生成 Persistent 场景基础层级与核心组件骨架 | | **Scaffold Test Room** | `BaseGames → Tools → Scaffold Test Room` | — | 一键生成 TestRoom 基础层级、Player/Enemy/Camera/SavePoint 骨架 | | **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 | #### 步骤 2:Play 模式验证 1. 按 [1.6 本文统一测试场景与加载方式](#16-本文统一测试场景与加载方式必须按此执行) 加载 `Persistent.unity + TestRoom.unity` 2. 按 **Play** 3. 查看 Console,预期输出(顺序): ``` [GameServiceRegistrar] Registering services... [GameManager] Awake [AudioManager] Registered as IAudioService ``` #### 步骤 3:ServiceLocator 状态验证 在任意 MonoBehaviour 的 `Start()` 中临时添加以下代码,Play 后观察 Console: ```csharp void Start() { Debug.Log($"IAudioService: {ServiceLocator.GetOrDefault()?.GetType().Name}"); Debug.Log($"IDeathRespawnService: {ServiceLocator.GetOrDefault()?.GetType().Name}"); Debug.Log($"ISceneService: {ServiceLocator.GetOrDefault()?.GetType().Name}"); } ``` **预期输出**: ``` IAudioService: AudioManager IDeathRespawnService: DeathRespawnService ISceneService: SceneService ``` --- ### V2:Addressables 完整性检查 **验证目标**:所有 `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 应自动出现验证结果,无需手动触发。 --- ### V3:SO 事件系统与 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 │ │ 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` | 1(HUDController 订阅)| | 玩家死亡 | `EVT_PlayerDied` | 1(GameManager 订阅)| | 激活存档点 | `EVT_SavePointActivated` | 1(GameManager 订阅)| | 命中敌人 | `EVT_HitConfirmed` | 1(CombatSFXController 等)| > **Subs = 0 时**(红色行)意味着该事件频道有触发但无人响应——通常是 Inspector 字段未拖拽赋值,立即检查相关组件。 --- ### V4:输入系统 **验证目标**:物理按键正确映射到 InputReaderSO 事件,InputBuffer 缓冲窗口生效。 #### 步骤 0:先进入正确场景(必做) 1. 按 [1.6 本文统一测试场景与加载方式](#16-本文统一测试场景与加载方式必须按此执行) 加载场景 2. 打开 `BaseGames → Tools → Event Bus Monitor` 3. 在 EventBusMonitorWindow 点击 **Clear** 清空历史记录 4. 在 Filter 输入框输入 `Pause` #### 步骤 1:InputReader 挂载与引用核查 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` #### 步骤 2:Inspector 字段核查 选中 `InputBuffer` 组件(挂在 Player 上): | 字段 | 预期值 | |------|--------| | `_inputReader` | 已拖拽 `InputReader.asset`(非 None)| | `_jumpBufferDuration` | **0.15** | | `_attackBufferDuration` | **0.12** | | `_dashBufferDuration` | **0.10** | #### 步骤 3:InputActions 配置验证 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 的 Binding(Gameplay 与 UI 均需存在): - `Keyboard` → `Escape` - `Gamepad` → `Start` 4. 检查 `Jump` Action 的 Binding: - `Keyboard` → `Space` - `Gamepad` → `Button South`(南键) #### 步骤 4:Escape 事件链验证(本阶段主验证项) 1. 按 **Play** 2. 按一次 `Escape` 3. 观察 Console,必须出现一次链路日志: ``` [InputReaderSO.HandlePause] PAUSE INPUT DETECTED! [InputReaderSO.HandlePause] Invoking PauseEvent... [InputReaderSO.HandlePause] Raising _onPauseRequested channel... ``` 4. 观察 EventBusMonitorWindow(Filter=Pause): - 应新增 1 条 `EVT_PauseRequested` - `Subs` 应大于 0(通常为 2) 5. 再按一次 `Escape` 6. 再次确认仅新增 1 条 `EVT_PauseRequested` > 通过标准:按 2 次 Escape,新增 2 条 `EVT_PauseRequested`。若出现 4 条,表示重复绑定回归,需要排查 InputReaderSO 的重复 Bind。 #### 步骤 5:缓冲窗口验证(跳跃缓冲) 1. 让玩家跳跃到空中 2. **在落地前约 100–150ms**(目测约 3–5 帧前)按下跳跃键 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" clip,NormalizedTime 循环 | | 按住 A/D(移动) | `RunState` | "Run" clip,NormalizedTime 循环 | | 按空格(跳跃) | `JumpState` | "Jump" clip,NormalizedTime 0→1,非循环 | | 跳跃下落阶段 | `FallState` | "Fall" clip,NormalizedTime 循环 | | 落地 | `IdleState` 或 `RunState` | 对应 clip 恢复 | | 按攻击键 | `AttackState` | "Attack" clip,播放完毕后自动返回 | #### 步骤 3:PlayerController 字段核查 选中 Player 上的 `PlayerController` 组件,确认: | 字段 | 预期值 | |------|--------| | `_statsConfig` | 已拖拽 `PlayerStats.asset`(PlayerStatsSO)| | `_formConfig` | 已拖拽 `FormConfig.asset`(FormConfigSO)| --- ### V6:战斗管道(8 步验证) **验证目标**:完整验证 HurtBox 的 8 步伤害处理流水线。 #### 步骤 1:Physics 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)| | 🔵 蓝色 | 上一次执行成功(已完成) | #### 步骤 2:Variable 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. 敌人转向并向玩家寻路 #### 步骤 5:NavSurface 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:触发死亡 方法 A(Inspector 直接触发,最可控): 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 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 Subs: 1 EVT_PlayerRespawned Subs: ≥1 ``` #### 步骤 5:复活后状态验证 | 检查项 | 预期值 | |--------|--------| | `GameManager._fsm.CurrentStateId` | `Gameplay` | | 玩家 HP | 已从存档恢复 | | 玩家位置 | 在存档点坐标(误差 < 0.1)| | DeathScreen Canvas | `SetActive(false)`,界面隐藏 | --- ### V10:HUD 与 UI 管理 **验证目标**:HUDController 实时同步事件数据,UIManager 面板栈管理正常。 #### 步骤 1:HP 显示同步 1. Play 模式下,选中 Player → `PlayerStats` 组件 2. Inspector 中修改 `_currentHP`(如 5 → 3) 3. 同时观察 Game 视图 HUD 的 HP 条 / 数字 4. **同步排查**:EventBusMonitorWindow 过滤 `HP` → 确认 `EVT_HPChanged` 有记录 #### 步骤 2:UIManager 面板栈验证 在测试脚本中: ```csharp IEnumerator Start() { var ui = FindObjectOfType(); var deathCanvas = FindObjectOfType().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,事件链连通性正确。 #### 步骤 1:AudioMixer 配置核查 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 名称**完全一致**(区分大小写) #### 步骤 2:ServiceLocator 注册验证 ```csharp void Start() { var audio = ServiceLocator.Get(); Debug.Log($"IAudioService 实现类型: {audio.GetType().Name}"); // 预期: AudioManager } ``` #### 步骤 3:Phase 1 桩实现验证(不验证真实音效) 1. 玩家攻击命中敌人 2. **预期 Console**: ``` [AudioManager] PlaySFX("hit_normal") — stub, Phase 2 will implement ``` 这证明事件链从 `EVT_HitConfirmed` → `CombatSFXController` → `AudioManager.PlaySFX()` 已连通,只是音频播放留待 Phase 2 实现。 --- ### V12:VFX 反馈 **验证目标**:命中特效在正确位置生成并回池,受击白闪正常显示。 #### 步骤 1:HitFX 生成位置验证 1. Play 并攻击敌人 2. 观察命中点:特效应在**敌人碰撞体位置**出现,而非世界原点 (0, 0, 0) 3. 若特效出现在原点:检查 `HitBox` 传给 `HitFXSpawner` 的坐标是否为 `contactPoint.point` #### 步骤 2:VFXPool 对象池验证 1. Play 模式下在 Hierarchy 展开 `VFXPool` 相关 GameObject 2. 触发多次攻击命中,观察: - 特效播放时对应 GameObject 变为 **Active**(白色显示) - 特效播放完毕后变为 **Inactive**(灰色),**而非被 Destroy** > 若 VFX Prefab 不断被 Destroy,说明 `PooledObject.ReturnToPool()` 未正确调用。 #### 步骤 3:HurtFlash 白闪验证 1. 让敌人攻击玩家 2. 玩家 SpriteRenderer 应短暂(约 0.1–0.2 秒)变为**纯白色**,然后恢复 3. 若不变白:确认 `HurtFlashController` 挂在 Player 上,且 Sprite 材质支持 `_FlashAmount` Shader 参数 --- ### V13:相机系统 **验证目标**:相机锁定在房间边界内,像素对齐无亚像素抖动。 #### 步骤 1:Cinemachine 配置核查 选中 `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) #### 步骤 4:CameraBlendProfileSO 混合验证 1. 场景中设置两个 `CameraTriggerZone`(对应两台 RoomCamera) 2. 玩家穿越触发区边界 3. **预期**:约 0.5 秒内平滑过渡(而非瞬间切换) 4. 过渡曲线由 `CameraBlendProfileSO.ToBlendDefinition()` 返回的 blend struct 配置 --- ## 5. 完整可玩流程验证 完成各系统单独验证后,执行以下端对端流程确认所有系统**联动**正常: ``` 操作序列 对应检查点 ───────────────────────────────────────────────────────────────── 1. 打开 Persistent.unity + TestRoom.unity(Additive 加载) 2. 按 Play ✅ 检查点1:Console 无红色 Error EventBusMonitorWindow 无 Subs=0 的红色行 3. 将玩家移至距出生点较远的位置 4. 与 SavePoint 交互(按 E 键) ✅ 检查点2:Console 打印 SaveAsync() completed EventBusMonitorWindow: EVT_SavePointActivated Subs≥1 save_slot0.json 中 Player.PosX / PosY 已更新 5. 将玩家移至敌人视野范围内(靠近约 5 格) ✅ 检查点3:Behavior Designer: BD_IsPlayerInRange 高亮绿色 敌人开始向玩家移动(BD_MoveToPlayer 绿色) Variables 面板: IsPlayerInRange = true 6. 攻击敌人 3 次 ✅ 检查点4:Console 打印 3 条 DamageInfo EventBusMonitorWindow: EVT_HitConfirmed 有 3 条记录 HitFX 特效在敌人位置出现(非世界原点) Animancer 窗口: AttackState 快速切入切出 7. 让玩家 HP 降至 0 ✅ 检查点5:DeathScreen Canvas 在 Hierarchy 变为 Active Game 视图显示死亡界面 EventBusMonitorWindow: EVT_PlayerDied → EVT_GameStateChanged(Dead) GameManager._fsm.CurrentStateId = "Dead" 8. 按确认键(Space) ✅ 检查点6:EventBusMonitorWindow: 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*