Files
zeling_v2/Docs/Verification/Phase1_Verification_Guide.md
2026-05-08 11:04:00 +08:00

36 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 → Physics 2D
  2. 滚动到底部 Layer Collision Matrix 区域
  3. 确认以下组合已勾选(绿色):
行 Layer 列 Layer 说明
PlayerHitBox EnemyHurtBox 玩家攻击打敌人
EnemyHitBox PlayerHurtBox 敌人攻击打玩家
Player Ground 玩家落地检测
Enemy Ground 敌人落地检测

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/Rooms/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. 编辑器扩展工具一览

Phase 0/1 实现了以下 Editor-only 工具,验证过程中会频繁用到:

工具 菜单路径 快捷键 用途
Event Bus Monitor BaseGames → Tools → Event Bus Monitor Ctrl+Shift+E Play 模式下实时查看所有 SO 事件触发记录
Validate AddressKeys Tools → Validate AddressKeys 手动校验 AddressKeys 常量与 Addressable 分组一致性
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. 菜单 Edit → Project Settings → Script Execution Order
  2. 确认以下顺序存在(数字越小越先执行):
脚本 ExecutionOrder
GameServiceRegistrar -2000
GameManager -1000
SceneService -900
SaveManager -900
AudioManager -500
PlayerController -100

步骤 2Play 模式验证

  1. 打开包含 Persistent.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. 菜单 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 应自动出现验证结果,无需手动触发。


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 缓冲窗口生效。

步骤 1Inspector 字段核查

选中 InputBuffer 组件(挂在 Player 上):

字段 预期值
_inputReader 已拖拽 InputReader.asset(非 None
_jumpBufferDuration 0.15
_attackBufferDuration 0.12
_dashBufferDuration 0.10

步骤 2InputActions 配置验证

  1. 双击 Assets/Settings/PlayerInputActions.inputactions 打开 Input System 编辑器
  2. 确认以下 Action Maps 存在:
    • Gameplay(含 Move / Jump / Attack / Dash / Parry 等 Actions
    • UI(含 Navigate / Submit / Cancel 等)
    • Cutscene(含 Skip 等)
  3. 检查 Jump Action 的 Binding
    • KeyboardSpace
    • GamepadButton South(南键)

步骤 3缓冲窗口验证跳跃缓冲

  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