1096 lines
43 KiB
Markdown
1096 lines
43 KiB
Markdown
# 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 → 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 |
|
||
|
||
#### 步骤 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<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
|
||
```
|
||
|
||
---
|
||
|
||
### V2:Addressables 完整性检查
|
||
|
||
**验证目标**:所有 `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 应自动出现验证结果,无需手动触发。
|
||
|
||
---
|
||
|
||
### 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 │ <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` | 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 <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)`,界面隐藏 |
|
||
|
||
---
|
||
|
||
### 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<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,事件链连通性正确。
|
||
|
||
#### 步骤 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<IAudioService>();
|
||
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*
|