43 KiB
Phase 1 验证指南 v2.0
适用版本:Phase 0 + Phase 1 全部完成(2026-05-08)
Unity 版本:2022.3.62f1c1 LTS
目标读者:在 Unity Editor 中逐步验证当前实现是否符合预期功能
目录
1. 验证前准备
1.1 编译状态确认
打开项目后执行以下检查:
- 等待编译完成:Unity 左下角进度条消失,状态栏无旋转图标
- Console 检查:菜单
Window → General → Console- 点击 Console 右上角三个图标(Error / Warning / Log)全部开启
- 确认 红色 Error 数量 = 0(黄色 Warning 可容忍,但优先排查)
- Assembly 引用方向验证(通用做法):
- 某些 Unity 版本不会显示
Assembly Version Validation,这属于正常情况 - 直接以“能否正常编译 + 是否有程序集引用错误”作为判定标准:
- 菜单
Assets → Open C# Project - 在 IDE(Rider/Visual Studio)执行一次 Rebuild
- 回到 Unity Console,确认没有 asmdef/程序集循环引用相关错误
- 菜单
- 某些 Unity 版本不会显示
1.2 Addressables 构建
若跳过此步骤,运行时会出现
InvalidKeyException或资产加载失败。
步骤:
- 菜单
Window → Asset Management → Addressables → Groups - 在弹出窗口中,点击左上角 Build 下拉菜单
- 选择
New Build → Default Build Script - 等待右下角进度条完成
- Console 中确认无
[AddressKeyValidator] ❌错误(见 V2)
1.3 NavSurface 烘焙
PathBerserker2d 的寻路完全依赖烘焙结果,未烘焙时敌人原地站立无响应。
步骤:
- 在测试房间场景中选中挂有
NavSurface组件的 GameObject - 在 Inspector 中找到 NavSurface 组件
- 点击组件右上角的 Bake 按钮
- 成功后 Scene 视图中地面会显示蓝绿色半透明网格 Gizmo
提示:若 Scene 视图看不到 Gizmo,点击 Scene 视图右上角
Gizmos下拉 → 确认PathBerserker2d相关项已勾选
1.4 SO 事件频道资产确认
- 在 Project 窗口导航到
Assets/Data/Events/ - 确认存在若干
.asset文件(以EVT_开头命名) - 若目录为空:菜单
BaseGames → Tools → Create Event Channel Assets(一键生成全部全局频道资产) - 若部分资产 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 碰撞矩阵,未配置时攻击不触发伤害。
详细配置步骤:
- 打开菜单
Edit → Project Settings → Tags and Layers - 在 User Layer 中创建以下 Layer(名称必须完全一致,区分大小写):
| Layer 名称 | 用途 |
|---|---|
Player |
玩家主物体(Rigidbody2D 所在) |
Enemy |
敌人主物体(Rigidbody2D/导航代理所在) |
Ground |
地面、墙体、平台等场景碰撞体 |
PlayerHitBox |
玩家攻击判定触发器 |
PlayerHurtBox |
玩家受击判定触发器 |
EnemyHitBox |
敌人攻击判定触发器 |
EnemyHurtBox |
敌人受击判定触发器 |
TriggerZone |
存档点、相机区、剧情触发区 |
- 给对象分配 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 |
- 打开菜单
Edit → Project Settings → Physics 2D - 滚动到底部
Layer Collision Matrix - 按 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
-
组件级别补充设置(和矩阵一起生效):
- HitBox 与 HurtBox 的 Collider2D 必须
Is Trigger = true - Player/Enemy 主体 Collider2D 必须
Is Trigger = false - 至少一方需要 Rigidbody2D 才会触发 Trigger 回调(本项目通常在主体根节点)
- HitBox 与 HurtBox 的 Collider2D 必须
-
快速验证:
- 玩家攻击敌人:应触发
EVT_HitConfirmed - 敌人攻击玩家:玩家 HP 下降且 HUD 刷新
- 玩家接触 SavePoint:应触发
EVT_SavePointActivated
- 玩家攻击敌人:应触发
1.6 本文统一测试场景与加载方式(必须按此执行)
为避免“服务层未加载”或“房间场景缺对象”的误判,本文所有验证默认使用以下场景组合:
- 打开
Assets/Scenes/Persistent.unity - 使用 Additive 方式再打开
Assets/Scenes/TestRoom.unity - 在 Hierarchy 中确认
Persistent与TestRoom两个场景同时处于已加载状态 - 若当前只开了
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. 编辑器扩展工具一览
当前验证会用到两类入口:
- BaseGames 自定义菜单工具:会出现在
BaseGames菜单下 - 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:确认执行顺序配置
- 优先使用一键菜单:
BaseGames → Tools → Apply Script Execution Order Preset - (可选)执行校验菜单:
BaseGames → Tools → Validate Script Execution Order Preset - 若需人工复核,打开
Edit → Project Settings → Script Execution Order - 确认以下顺序存在(数字越小越先执行):
| 脚本 | ExecutionOrder |
|---|---|
GameServiceRegistrar |
-2000 |
GameManager |
-1000 |
SceneService |
-900 |
SaveManager |
-900 |
AudioManager |
-500 |
PlayerController |
-100 |
步骤 2:Play 模式验证
- 按 1.6 本文统一测试场景与加载方式 加载
Persistent.unity + TestRoom.unity - 按 Play
- 查看 Console,预期输出(顺序):
[GameServiceRegistrar] Registering services...
[GameManager] Awake
[AudioManager] Registered as IAudioService
步骤 3:ServiceLocator 状态验证
在任意 MonoBehaviour 的 Start() 中临时添加以下代码,Play 后观察 Console:
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:手动触发验证
- 菜单
BaseGames → Addressables → Validate Address Keys - 查看 Console
预期结果:
[AddressKeyValidator] ✅ 所有 AddressKey 均有效。
若出现错误:
[AddressKeyValidator] ❌ 发现 N 个失效 Key:
AddressKeys.PrefabPlayer = "PLY_Player" → 在 Addressable 中未找到
修复方法:
- 菜单
Window → Asset Management → Addressables → Groups - 在对应分组中找到 Prefab → 右键 →
Copy Address - 将复制的地址更新到
Assets/Scripts/Core/Assets/AddressKeys.cs对应常量中
步骤 2:自动触发验证
AddressKeyImportWatcher 会在 Addressable 分组 .asset 修改后自动运行验证。
修改任意 Addressable 分组后,Console 应自动出现验证结果,无需手动触发。
V3:SO 事件系统与 EventBusMonitorWindow
验证目标:事件频道在 Play 模式下正确触发,EventBusMonitorWindow 能追踪事件流。
步骤 1:打开 EventBusMonitorWindow
- 菜单路径:
BaseGames → Tools → Event Bus Monitor - 快捷键:
Ctrl+Shift+E - 窗口会显示工具栏 + 列表区域
步骤 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:验证事件链连通性
- 按 Play,打开 EventBusMonitorWindow
- 在 Filter 框输入
Player - 触发各操作,确认对应事件出现在列表中,且 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.6 本文统一测试场景与加载方式 加载场景
- 打开
BaseGames → Tools → Event Bus Monitor - 在 EventBusMonitorWindow 点击 Clear 清空历史记录
- 在 Filter 输入框输入
Pause
步骤 1:InputReader 挂载与引用核查
- 在
Persistent场景中选中InputReaderHolder - 检查
InputReaderBootstrap._inputReader:必须已拖拽Assets/Data/Input/InputReader.asset - 检查
InputReader.asset(ScriptableObject)中的_inputActions:必须指向Assets/Settings/PlayerInputActions.inputactions - 检查
_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 配置验证
- 双击
Assets/Settings/PlayerInputActions.inputactions打开 Input System 编辑器 - 确认以下 Action Maps 存在:
Gameplay(含 Move / Jump / Attack / DownAttack / UpAttack / Parry / Dash / UseSpring / Switch* / SoulSkill / SpiritSkill* / Interact / Pause)UI(含 Navigate / Submit / Cancel / Pause / Point)
- 检查
PauseAction 的 Binding(Gameplay 与 UI 均需存在):Keyboard→EscapeGamepad→Start
- 检查
JumpAction 的 Binding:Keyboard→SpaceGamepad→Button South(南键)
步骤 4:Escape 事件链验证(本阶段主验证项)
- 按 Play
- 按一次
Escape - 观察 Console,必须出现一次链路日志:
[InputReaderSO.HandlePause] PAUSE INPUT DETECTED!
[InputReaderSO.HandlePause] Invoking PauseEvent...
[InputReaderSO.HandlePause] Raising _onPauseRequested channel...
- 观察 EventBusMonitorWindow(Filter=Pause):
- 应新增 1 条
EVT_PauseRequested Subs应大于 0(通常为 2)
- 应新增 1 条
- 再按一次
Escape - 再次确认仅新增 1 条
EVT_PauseRequested
通过标准:按 2 次 Escape,新增 2 条
EVT_PauseRequested。若出现 4 条,表示重复绑定回归,需要排查 InputReaderSO 的重复 Bind。
步骤 5:缓冲窗口验证(跳跃缓冲)
- 让玩家跳跃到空中
- 在落地前约 100–150ms(目测约 3–5 帧前)按下跳跃键
- 落地后玩家应立即弹起(缓冲生效),而非落地后需重新按键
缓冲失效排查:确认
InputBuffer组件未被enabled = false,且Update()正常执行。
V5:玩家移动与 Animancer FSM
验证目标:Animancer FSM 在五个基础状态间正确切换,无动画卡顿或状态锁死。
步骤 1:打开 Animancer 调试窗口
- 菜单路径:
Window → Animation → Animancer - 窗口打开后显示当前所有 Animancer 实例
- Play 模式下选中 Player GameObject
- 窗口中出现该对象的 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 矩阵确认(战斗前必做)
- 菜单
Edit → Project Settings → Physics 2D - 找到 Layer Collision Matrix(矩阵表格)
- 确认
PlayerHitBox×EnemyHurtBox格子已勾选(显示小方块)
步骤 2:基础命中验证
- Play 后将玩家移动到敌人旁,按攻击键
- 预期 Console 输出:
[HurtBox] Received: Amount=3, Type=Normal, FinalDamage=2
[EnemyBase] TakeDamage: HP 10 → 8
- 在 EventBusMonitorWindow 中过滤
HitConfirmed:- 确认
EVT_HitConfirmed出现,Subs ≥ 1
- 确认
步骤 3:无敌帧验证
- 选中 Player 上的
HurtBox组件 - Inspector 中临时将
_isHurtBoxInvincible勾选为 true - 让敌人攻击玩家
- 预期:Console 无伤害日志,HP 不变
- 验证后取消勾选
步骤 4:防御计算验证
- 选中敌人
EnemyStats→ Inspector 将Defense临时改为 2 - 攻击基础伤害为 3 时:
FinalDamage = Max(1, 3-2) = 1 - Console 确认
FinalDamage=1(而非 3) - 恢复 Defense 值
步骤 5:事件广播确认
- 攻击命中后,EventBusMonitorWindow 中确认:
EVT_HitConfirmed有记录,Subs ≥ 1EVT_HPChanged有记录(敌人 HP 变化广播)
V7:敌人 AI 寻路(Behavior Designer)
验证目标:敌人在 NavSurface 上正确执行巡逻 → 追击 → 攻击行为树。
步骤 1:打开 Behavior Tree 调试窗口
- Play 模式下选中敌人 GameObject
- 在 Inspector 中找到 BehaviorTree(Behavior Designer RuntimeController)组件
- 点击组件中的
Open Behavior Designer按钮 - Behavior Designer 窗口打开,显示当前 BT 节点图
节点颜色含义:
| 颜色 | 含义 |
|---|---|
| 🟢 绿色 | 当前正在执行的节点 |
| ⬜ 灰色 | 未执行 / 条件未满足 |
| 🔴 红色 | 条件不满足(Conditional 类节点返回 Failure) |
| 🔵 蓝色 | 上一次执行成功(已完成) |
步骤 2:Variable Monitor 面板(Behavior Designer 内置)
- 在 Behavior Designer 窗口中,点击左侧工具栏的 Variables 面板(或顶部菜单 View → Variables)
- 可以看到 BT 黑板中定义的变量,如:
PlayerTransform(SharedTransform):追击目标IsPlayerInRange(SharedBool):是否检测到玩家PatrolIndex(SharedInt):当前巡逻点索引
- Play 模式下让玩家靠近敌人 → 观察
IsPlayerInRange实时变为true
步骤 3:巡逻状态验证
- 将玩家放在敌人视野范围之外(距离 >
BD_IsPlayerInRange检测半径) - 观察 Behavior Designer 窗口:
BD_Patrol节点高亮 绿色BD_IsPlayerInRange节点为红色(条件不满足)
- 场景中敌人在 Waypoints 之间往返移动
BD_Patrol 组件 Inspector 字段核查(选中敌人 BT 中的 BD_Patrol 任务节点):
Waypoints:Transform 数组(至少 2 个),每个指向场景内的巡逻点 GameObjectMoveSpeed:巡逻速度(浮点数)
步骤 4:追击状态验证
- 将玩家移入检测范围(靠近敌人)
- 预期:
- Behavior Designer 中
BD_IsPlayerInRange变绿色(Subs 满足) BD_Patrol停止BD_MoveToPlayer开始执行(绿色)- Variables 面板中
IsPlayerInRange = true
- Behavior Designer 中
- 敌人转向并向玩家寻路
步骤 5:NavSurface Gizmo 确认
- Scene 视图确认地面有蓝绿色半透明网格(已烘焙)
- 若没有:选中 NavSurface 组件 → 点击
Bake
V8:存档点读写
验证目标:激活存档点后数据正确写入磁盘,重新 Play 后玩家从存档位置复位。
步骤 1:确认存档路径
Windows: C:\Users\{用户名}\AppData\LocalLow\{公司名}\{产品名}\Saves\save_slot0.json
快速定位:在任意 MonoBehaviour 中临时添加:
void Start() => Debug.Log(Application.persistentDataPath);
Play 后 Console 中点击该日志 → 直接跳转到文件夹。
步骤 2:激活存档点
- Play 后将玩家移动到
SavePoint碰撞体范围内 - 按
E键交互(具体按键见PlayerInputActions.inputactions的InteractAction) - Console 预期:
[SaveManager] SaveAsync() started...
[LocalFileStorage] WriteAsync: save_slot0.json
[SaveManager] SaveAsync() completed. Version: 2.1
- EventBusMonitorWindow 中确认
EVT_SavePointActivated有记录,Subs ≥ 1
步骤 3:验证存档 JSON 结构
- 不退出 Play,用文本编辑器打开
save_slot0.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:验证加载
- 退出 Play 模式
- 重新按 Play
- 预期:
SaveManager.HasSave()返回true- Console:
[SaveManager] LoadAsync() completed. Restored player to (3.5, -1.2) in TestRoom - 玩家出生在步骤 2 中存档的位置
V9:死亡与复活流程
验证目标:完整死亡事件链按顺序执行,状态机转换正确,复活后状态恢复。
步骤 0:先完成一次存档(必要前提)
按 V8 步骤 2 完成存档后继续。
步骤 1:触发死亡
方法 A(Inspector 直接触发,最可控):
- Play 模式下选中
Assets/Data/Events/EVT_PlayerDied.asset - 在 Inspector 中点击
Raise Event按钮(若有 Editor 扩展) - 或在
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:确认按钮响应与复活
- 按
Space/Enter键(视 DeathScreenController 配置) - 预期 Console:
[DeathScreenController] Confirmed → raising EVT_DeathScreenConfirmed
[GameManager] DeathFlow() resumed after confirmation
[GameManager] Raising EVT_PlayerRespawned
[SceneLoader] ReloadFromSave() started
[GameManager] TransitionTo: Dead → Gameplay
- 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 显示同步
- Play 模式下,选中 Player →
PlayerStats组件 - Inspector 中修改
_currentHP(如 5 → 3) - 同时观察 Game 视图 HUD 的 HP 条 / 数字
- 同步排查:EventBusMonitorWindow 过滤
HP→ 确认EVT_HPChanged有记录
步骤 2:UIManager 面板栈验证
在测试脚本中:
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 配置核查
- 菜单
Window → Audio → Audio Mixer - 确认存在以下 Group 层级:
Master
├── BGM
└── SFX
├── Combat
└── Ambient
- 确认各 Group 的 Volume 参数已 Expose to Script(右键 Volume → Expose to Script → 在 Inspector 顶部 Exposed Parameters 中命名)
- 在
AudioMixerKeys.cs中确认常量名与 Exposed Parameter 名称完全一致(区分大小写)
步骤 2:ServiceLocator 注册验证
void Start()
{
var audio = ServiceLocator.Get<IAudioService>();
Debug.Log($"IAudioService 实现类型: {audio.GetType().Name}");
// 预期: AudioManager
}
步骤 3:Phase 1 桩实现验证(不验证真实音效)
- 玩家攻击命中敌人
- 预期 Console:
[AudioManager] PlaySFX("hit_normal") — stub, Phase 2 will implement
这证明事件链从 EVT_HitConfirmed → CombatSFXController → AudioManager.PlaySFX() 已连通,只是音频播放留待 Phase 2 实现。
V12:VFX 反馈
验证目标:命中特效在正确位置生成并回池,受击白闪正常显示。
步骤 1:HitFX 生成位置验证
- Play 并攻击敌人
- 观察命中点:特效应在敌人碰撞体位置出现,而非世界原点 (0, 0, 0)
- 若特效出现在原点:检查
HitBox传给HitFXSpawner的坐标是否为contactPoint.point
步骤 2:VFXPool 对象池验证
- Play 模式下在 Hierarchy 展开
VFXPool相关 GameObject - 触发多次攻击命中,观察:
- 特效播放时对应 GameObject 变为 Active(白色显示)
- 特效播放完毕后变为 Inactive(灰色),而非被 Destroy
若 VFX Prefab 不断被 Destroy,说明
PooledObject.ReturnToPool()未正确调用。
步骤 3:HurtFlash 白闪验证
- 让敌人攻击玩家
- 玩家 SpriteRenderer 应短暂(约 0.1–0.2 秒)变为纯白色,然后恢复
- 若不变白:确认
HurtFlashController挂在 Player 上,且 Sprite 材质支持_FlashAmountShader 参数
V13:相机系统
验证目标:相机锁定在房间边界内,像素对齐无亚像素抖动。
步骤 1:Cinemachine 配置核查
选中 RoomCamera → CinemachineVirtualCamera 组件,确认:
Follow:已拖拽 Player Transform- Extensions 列表中有
CinemachineConfiner2D CinemachineConfiner2D.Bounding Shape 2D:已指向房间边界 PolygonCollider2D
步骤 2:边界限制验证
- Play 后将玩家移动到房间最左/右/上/下边缘
- 预期:相机到达边界后停止跟随(不超出房间范围)
- 若超出边界:检查
CinemachineConfiner2D的 Bounding Shape 2D 字段是否已赋值
步骤 3:像素对齐验证
- Game 视图右上角选择目标分辨率
- Play 后缓慢移动玩家
- 观察背景图块:正确表现是平滑移动,无1像素跳动或闪烁
Pixel Perfect Camera 检查:
Assets Pixels Per Unit:与 Sprite 资产 PPU 一致(如 16 或 32)Reference Resolution:目标分辨率(如 320×180)
步骤 4:CameraBlendProfileSO 混合验证
- 场景中设置两个
CameraTriggerZone(对应两台 RoomCamera) - 玩家穿越触发区边界
- 预期:约 0.5 秒内平滑过渡(而非瞬间切换)
- 过渡曲线由
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
排查步骤:
- 选中
GameServiceRegistrar→ Inspector 检查_deathRespawnService、_sceneService、_eventChannelRegistry字段 - 若任一字段为 None → 将对应 GameObject 拖拽赋值
- 确认
GameServiceRegistrar.ExecutionOrder = -2000(Edit → Project Settings → Script Execution Order)
❌ EventBusMonitorWindow 中频道 Subs = 0(红色行)
排查步骤:
- 记录红色行的 Channel 名(如
EVT_PlayerDied) - 找到应订阅此频道的组件(
EVT_PlayerDied应由GameManager订阅) - 选中该组件 → Inspector 检查对应字段是否指向同一个
.asset(而非 None 或不同资产) - 若为 None → 拖拽
Assets/Data/Events/EVT_{Name}.asset赋值
❌ 玩家动画卡死在某个状态
排查步骤:
Window → Animation → Animancer→ 观察 Current State 名称和 NormalizedTime- 若 NormalizedTime 停滞:检查
PlayerAnimationConfigSO中对应 AnimationClip 字段是否已赋值 - Console 检查是否有与
PlayerController相关的NullReferenceException
❌ 敌人原地站立不移动
排查步骤:
- Scene 视图确认地面有蓝绿色网格 Gizmo
- 若无 → NavSurface 组件 → 点击
Bake - 选中敌人 →
NavAgentComponent的NavSurface字段是否指向已烘焙的 NavSurface
❌ 攻击命中后敌人 HP 不减少
排查步骤:
Edit → Project Settings → Physics 2D→ 确认PlayerHitBox与EnemyHurtBox碰撞已勾选- 选中 Player HitBox 子对象 → Collider2D Is Trigger = ✅
- 选中 Enemy HurtBox 子对象 → Collider2D Is Trigger = ✅
- 确认 Layer 设置:Player HitBox →
PlayerHitBox层,Enemy HurtBox →EnemyHurtBox层
❌ 存档后重新 Play 玩家仍在场景初始位置
排查步骤:
- 临时添加
Debug.Log(Application.persistentDataPath)→ 打开文件夹确认save_slot0.json存在 - Console 确认是否有
[SaveManager] LoadAsync() completed日志 - 若无日志:检查启动流程中是否调用了
SaveManager.LoadAsync() - 确认
SaveManager.HasSave()返回true
❌ 死亡界面出现但按确认键无响应
排查步骤:
- 选中
DeathScreenController→ Inspector 中_onDeathScreenConfirmed字段 → 双击该资产,Project 窗口高亮对应.asset,记录路径 - 选中
GameManager→ 同样双击_onDeathScreenConfirmed字段的资产 - 若两者高亮的是不同文件 → 统一引用同一个
.asset
❌ VFX 特效出现在世界原点 (0, 0, 0)
排查步骤:
- 检查
HitBox.OnTriggerEnter2D回调,确认传给HitFXSpawner的位置参数为:而非other.ClosestPoint(transform.position) // 推荐 // 或 other.transform.positionVector2.zero或transform.position(HitBox 自身位置)
文档版本:2.0 | 对应开发进度:Phase 0 + Phase 1 完成 | 更新日期:2026-05-08