Files
zeling_v2/Docs/Verification/Phase1_Verification_Guide.md

43 KiB
Raw Blame History

Phase 1 验证指南 v2.0

适用版本Phase 0 + Phase 1 全部完成2026-05-08
Unity 版本2022.3.62f1c1 LTS
目标读者:在 Unity Editor 中逐步验证当前实现是否符合预期功能


目录

  1. 验证前准备
  2. 场景搭建参考
  3. 编辑器扩展工具一览
  4. 各系统验证步骤
  5. 完整可玩流程验证
  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

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 存档点、相机区、剧情触发区
  1. 给对象分配 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
  1. 打开菜单 Edit → Project Settings → Physics 2D
  2. 滚动到底部 Layer Collision Matrix
  3. 按 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 × BB × A 是同一个格
  • 找不到某一格时,优先在“行名较靠下”的那一行里找对应列
  • 先设置 5 个核心开启项:Player-GroundEnemy-GroundPlayerHitBox-EnemyHurtBoxEnemyHitBox-PlayerHurtBoxTriggerZone-Player
  1. 组件级别补充设置(和矩阵一起生效):

    • HitBox 与 HurtBox 的 Collider2D 必须 Is Trigger = true
    • Player/Enemy 主体 Collider2D 必须 Is Trigger = false
    • 至少一方需要 Rigidbody2D 才会触发 Trigger 回调(本项目通常在主体根节点)
  2. 快速验证:

    • 玩家攻击敌人:应触发 EVT_HitConfirmed
    • 敌人攻击玩家:玩家 HP 下降且 HUD 刷新
    • 玩家接触 SavePoint应触发 EVT_SavePointActivated

1.6 本文统一测试场景与加载方式(必须按此执行)

为避免“服务层未加载”或“房间场景缺对象”的误判,本文所有验证默认使用以下场景组合:

  1. 打开 Assets/Scenes/Persistent.unity
  2. 使用 Additive 方式再打开 Assets/Scenes/TestRoom.unity
  3. 在 Hierarchy 中确认 PersistentTestRoom 两个场景同时处于已加载状态
  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

关键原则GameManagerDeathScreenController 必须引用同一个 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 本文统一测试场景与加载方式 加载 Persistent.unity + TestRoom.unity
  2. Play
  3. 查看 Console预期输出顺序
[GameServiceRegistrar] Registering services...
[GameManager] Awake
[AudioManager] Registered as IAudioService

步骤 3ServiceLocator 状态验证

在任意 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

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(秒)
  • FrameTime.frameCount(帧号)
  • ChannelSO 资产名称(即 .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 本文统一测试场景与加载方式 加载场景
  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.assetScriptableObject中的 _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 均需存在):
    • KeyboardEscape
    • GamepadStart
  4. 检查 Jump Action 的 Binding
    • KeyboardSpace
    • GamepadButton South(南键)

步骤 4Escape 事件链验证(本阶段主验证项)

  1. Play
  2. 按一次 Escape
  3. 观察 Console必须出现一次链路日志
[InputReaderSO.HandlePause] PAUSE INPUT DETECTED!
[InputReaderSO.HandlePause] Invoking PauseEvent...
[InputReaderSO.HandlePause] Raising _onPauseRequested channel...
  1. 观察 EventBusMonitorWindowFilter=Pause
    • 应新增 1 条 EVT_PauseRequested
    • Subs 应大于 0通常为 2
  2. 再按一次 Escape
  3. 再次确认仅新增 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 循环
落地 IdleStateRunState 对应 clip 恢复
按攻击键 AttackState "Attack" clip播放完毕后自动返回

步骤 3PlayerController 字段核查

选中 Player 上的 PlayerController 组件,确认:

字段 预期值
_statsConfig 已拖拽 PlayerStats.assetPlayerStatsSO
_formConfig 已拖拽 FormConfig.assetFormConfigSO

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
  1. 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 中找到 BehaviorTreeBehavior Designer RuntimeController组件
  3. 点击组件中的 Open Behavior Designer 按钮
  4. Behavior Designer 窗口打开,显示当前 BT 节点图

节点颜色含义

颜色 含义
🟢 绿色 当前正在执行的节点
灰色 未执行 / 条件未满足
🔴 红色 条件不满足Conditional 类节点返回 Failure
🔵 蓝色 上一次执行成功(已完成)

步骤 2Variable Monitor 面板Behavior Designer 内置)

  1. 在 Behavior Designer 窗口中,点击左侧工具栏的 Variables 面板(或顶部菜单 View → Variables
  2. 可以看到 BT 黑板中定义的变量,如:
    • PlayerTransformSharedTransform追击目标
    • IsPlayerInRangeSharedBool是否检测到玩家
    • PatrolIndexSharedInt当前巡逻点索引
  3. Play 模式下让玩家靠近敌人 → 观察 IsPlayerInRange 实时变为 true

步骤 3巡逻状态验证

  1. 将玩家放在敌人视野范围之外(距离 > BD_IsPlayerInRange 检测半径)
  2. 观察 Behavior Designer 窗口:
    • BD_Patrol 节点高亮 绿色
    • BD_IsPlayerInRange 节点为红色(条件不满足)
  3. 场景中敌人在 Waypoints 之间往返移动

BD_Patrol 组件 Inspector 字段核查(选中敌人 BT 中的 BD_Patrol 任务节点):

  • WaypointsTransform 数组(至少 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 中临时添加:

void Start() => Debug.Log(Application.persistentDataPath);

Play 后 Console 中点击该日志 → 直接跳转到文件夹。

步骤 2激活存档点

  1. Play 后将玩家移动到 SavePoint 碰撞体范围内
  2. E 键交互(具体按键见 PlayerInputActions.inputactionsInteract Action
  3. Console 预期
[SaveManager] SaveAsync() started...
[LocalFileStorage] WriteAsync: save_slot0.json
[SaveManager] SaveAsync() completed. Version: 2.1
  1. EventBusMonitorWindow 中确认 EVT_SavePointActivated 有记录Subs ≥ 1

步骤 3验证存档 JSON 结构

  1. 不退出 Play用文本编辑器打开 save_slot0.json
  2. 确认关键字段已正确写入:
{
  "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 完成存档后继续。

步骤 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查看

  • _fsmCurrentStateId 字段值 = "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
  1. 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 面板栈验证

在测试脚本中:

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
  1. 确认各 Group 的 Volume 参数已 Expose to Script(右键 Volume → Expose to Script → 在 Inspector 顶部 Exposed Parameters 中命名)
  2. AudioMixerKeys.cs 中确认常量名与 Exposed Parameter 名称完全一致(区分大小写)

步骤 2ServiceLocator 注册验证

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_HitConfirmedCombatSFXControllerAudioManager.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 配置核查

选中 RoomCameraCinemachineVirtualCamera 组件,确认:

  • 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 = -2000Edit → 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. 选中敌人 → NavAgentComponentNavSurface 字段是否指向已烘焙的 NavSurface

攻击命中后敌人 HP 不减少

排查步骤

  1. Edit → Project Settings → Physics 2D → 确认 PlayerHitBoxEnemyHurtBox 碰撞已勾选
  2. 选中 Player HitBox 子对象 → Collider2D Is Trigger =
  3. 选中 Enemy HurtBox 子对象 → Collider2D Is Trigger =
  4. 确认 Layer 设置Player HitBox → PlayerHitBoxEnemy 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 的位置参数为:
    other.ClosestPoint(transform.position)  // 推荐
    // 或
    other.transform.position
    
    而非 Vector2.zerotransform.positionHitBox 自身位置)

文档版本2.0 | 对应开发进度Phase 0 + Phase 1 完成 | 更新日期2026-05-08