LyraStarterGame 测试文档

本文档来自 LyraStarterGame(UE 5.8)项目的完整测试记录(TestDocs),随博客文章《对UE的教学项目进行测试实录》一起发布。目录可点击跳转。

00-总纲

01-系统测试

02-专项测试

03-实施

Bug报告


LyraStarterGame 测试总纲

本文件记录测试系统的划分、范围定义和讨论结论。

📌 当前状态:自动化 CQTest 96/96 全绿,手工 Session 1~4 完成,无 🔴 阻塞 Bug,
已按”功能封板”口径封板。详见《Lyra 测试总结报告》。

🚫 决策:状态系统(09)不需要,已从测试范围移除(血量/死亡相关验证并入事件测试 DT-01)。

测试系统清单(共 9 个;状态系统已移除)

编号 系统名称
01 武器与装备系统
02 控制系统
03 相机系统
04 HUD
05 前端页面
06 音频系统
07 队伍系统
08 初始化链路系统
09 状态系统 🚫(已移除)
10 性能测试

测试原则

  1. 黑盒测试为主 — 除非明确标注为自动化测试,否则一律从玩家视角描述
  2. 不单独测 GAS — Gameplay Ability System 不作为独立系统,相关功能归入各系统内部细则
  3. 网络测试 — 多线程/复制场景作为每个系统的细则考虑,不设单独模块
  4. 边界值测试 — 数值测试包含 0 / 负值 / 正常值 三种边界情况
  5. 测试方式选择(决策) — 仅两种方式:
    • C++ CQTest:重复性高的用例(数值边界、状态流转、输入模拟、回归验证)
    • 手工测试:其余需要视觉/手感/听感判断的用例
    • 弃用蓝图功能测试(FunctionalTest 蓝图)— 复杂逻辑在蓝图中难以编写和维护,效率低,回归 C++ CQTest 实现

用例分级清单 — 工作流主清单

现改为按 分级 → 工作流阶段 组织的可勾选清单:每个用例一行,勾掉的项是已完成的,未勾的项就是当前阻塞 / 待办
取舍决策的完整存档移入文末 变更记录,不再作为正文。

📌 基线:自动化 CQTest 96/96 全绿,手工 Session 1~4 完成,无 🔴 阻塞 Bug。下方勾选状态即该快照,后续以本清单推进。

怎么用

  1. 分级 = 工作流阶段:⚪(每次提交)→ 🔵(每次合并 / 构建)→ 🟢(合并 / 里程碑)→ 🟠(里程碑)→ 🟣(发布前)。
  2. 对应阶段触发时,逐项执行并勾掉通过的用例;未勾的项就是当前阻塞
  3. 与 测试工作流清单 配合使用:本清单回答”每个用例属于哪一级、在哪实现、当前状态”;工作流清单回答”什么时机跑哪些用例”。
  4. 状态图例:✅ 已通过 | ⏳ 待执行 / 排期 | 🔴 阻塞 | 🚫 已并入 / 删除(见变更记录)。

分级与工作流映射

分级 触发时机 失败意味着 谁执行
⚪ 回归·单元 每次提交 / 构建 数据 / 配置被改坏 开发(可挂 pre-commit)
🔵 冒烟 每次合并后 环境或基础链路断了(停止后续) CI
🟢 回归 每次合并 / 里程碑 功能被改坏 CI / QA
🟠 验收 里程碑 / 发布前 体验不达标,不能发版 QA
🟣 探索 验收通过后 / 发布前 未知组合问题,需回流 QA

阶段一 · 每次提交 — ⚪ 回归·单元

改动涉及哪个模块就勾哪组;全量可挂 pre-commit。落点均为单元级断言(NewObject / LoadObject,不走 PIE)。

注:NV-01/03/05 的中间值、最大值(跳高、下落、半速 / 全速实测)已并入 AM-01 / MV-01 集成断言,不在此列。

阶段二 · 每次合并 / 构建 — 🔵 冒烟

全绿才继续;任一失败先修环境或基础链路,回归结果不作数。

  • 初始化链路:IN-01-01/02、IN-02-01~03、IN-04-01/02(进程启动 / Experience / Pawn / AbilitySet / 默认武器 / 输入绑定)|InitChain/ShooterMapsInit.spec.cpp
  • 武器:EQ-01 初始武器(并入 Pawn 生成冒烟)、WF-01-01 单发射击|CQTest / 蓝图 ✅
  • 控制:MV-01 方向移动(四方向循环)、AM-01-01 平地跳跃|ControlSystem.spec.cpp
  • 前端:FE-02-01/02、FE-05-01/02 主菜单 → 进入对局 / 加载链路|Spec/Gauntlet ⏳ 覆盖不完整

阶段三 · 每次合并 / 里程碑 — 🟢 回归

武器与装备(宪章

  • EQ-02 武器切换(-01 滚轮 / -02 数字键 / -03 空槽 / -04 快速 / -05 冷却)|CQTest ✅
  • EQ-03 装备武器(拾取三态:正常 / EQ-04 重复 / EQ-05 槽满)|CQTest ✅
  • WF-01-01 单发射击、WF-01-03 移动射击|蓝图功能测试 ✅(WF-01-03 记录偏差:原计划 CQTest)
  • WF-02-01 自动换弹、WF-02-02 主动换弹(含 WF-02-03 满弹匣)|蓝图功能测试 ✅
  • WF-02-04/05 换弹射击互斥|手工补验 ⏳(Bug 报告 WF-02-04 已登记,🟡 非阻塞)
  • WS-04-03 部位伤害(爆头 2 倍,伤害链路集成)|CQTest ✅

控制(宪章

  • MV-01 方向移动(含 MV-01-05 斜向、MV-02 停止)|CQTest ✅
  • MV-03 移动中转向|CQTest ✅
  • AM-01 跳跃(-01 平地 / -02 移动 / -04 连续 / -05 空中;承载 NV-01 / NV-03 实测断言)|CQTest ✅
  • AM-02-01~04 蹲下 / 站起 / 蹲下行进 / 蹲跳(吸收 AM-01-03)|CQTest ✅

前端页面(宪章

  • FE-02-03~05 导航与返回、FE-03 设置、FE-04 确认弹窗、FE-05-03 加载中输入、FE-06 暂停菜单|Spec/Gauntlet ⏳ 覆盖不完整

初始化链路(宪章

  • IN-01-03 网络同步就绪、IN-02-04~06 属性 / 拾取 / 特效、IN-05 断线重连、NV-01 重生延迟边界|CQTest ✅
  • IN-03 重生链路(与 DT-06 重叠)|⏳ 并入决策

事件测试(宪章

  • DT-01 死亡状态机与冻结(吸收 FR-01 / WF-01-04 / DT-05-02)|CQTest ✅
  • DT-03 死亡状态标签|CQTest ✅
  • DT-05-01 连续致死、DT-05-03 蹲伏中死亡|CQTest ✅
  • DT-06 重生完整性(吸收 FR-02;与 IN-03 重叠)|CQTest ✅
  • DT-07-01 重生后立即死亡|CQTest ✅
  • DT-04 网络同步(死亡复制 / 预测回滚)|⏳ 后续排期
  • SP-01-01/02/04 热量累积 / 冷却 / 过热强制冷却|CQTest ✅
  • SP-02-02/03 弹孔散布实测|⏳(SP-02-01 曲线求值已由单元覆盖)
  • SP-03 散布修正乘数(吸收 WF-01-03 散布断言)|⏳
  • SP-04 首发精度|⏳
  • SP-05-01/02 冷却延迟时序|⏳(SP-05-03 负值已由单元覆盖)
  • SP-06-02/03 散布曲线分布|CQTest ✅

性能门禁(宪章,发布前执行)

  • PF-0112 帧时间 / 内存 / 网络延迟|Gauntlet ⏳ 骨架 + 冒烟管线待建(36 实例,约 36h)

阶段四 · 里程碑 — 🟠 验收(手工)

按 Session 打包执行;发现的问题登记到 TestDocs/Bug报告/

Session 1 核心视觉(武器)

  • WA-01~03 装备 / 换弹 / 射击动画|✅
  • EQ-01~05 装备动作手感|✅

Session 2 移动体验(控制 + 相机)

  • AN-01-01~05 移动动画|✅
  • LC-01-01/-02/-03/-05 视角与灵敏度|✅
  • CM-01-0103/-05 跟随与旋转、CM-02-0104 碰撞避免、CM-05-01/02 蹲下偏移|✅
  • AM-02-05 矮通道、AM-03-01~06 Dash、NV-02 Dash 冷却、NV-04 灵敏度|⏳ 未作为独立用例正式执行(AM-03 相关缺陷已有 Bug 报告)

Session 3 UI 与信息(HUD + 队伍 + 音频)

  • HD-01~05 准星 / 命中标记 / 伤害数字 / 布局 / 游戏内信息|✅
  • TM-01~05 团队归属 / 友军识别 / 团队显示 / 标签 / 战绩|✅
  • AU-01~04 音量 / 输出 / HDR / 加载音频|✅

事件补充

  • DT-02-02 重生后摄像机恢复、DT-05-04 Dash 中死亡|⏳ 未作为独立用例正式执行

阶段五 · 发布前 — 🟣 探索

  • EX-01 地图互动效果(弹射器 / 传送门 / 手雷)|✅,发现 EX-01-01~04 及关联缺陷
  • 剩余 5 轮 × 90 分钟(跨系统组合 / 边界操作)|⏳
  • 每轮结束回流决策:可复现且值得保护 → 固化为宪章用例(标 🟢 / 🟠);一次性 → 留 Bug 报告

当前待办(快照)

优先级 待办 来源
DT-04 网络同步排期(PIE 双开 DeathState 复制 / 预测回滚) 总结报告 §6
性能 PF-01~12 骨架 + 冒烟管线 总结报告 §6
手工缺口:AM-02-05、AM-03 部分、NV-02、NV-04、DT-02-02、DT-05-04 总结报告 §6
SP-02-02/03、SP-03、SP-04、SP-05-01/02 剩余子状态 总结报告 §6
FE-02~06 Spec/Gauntlet 覆盖补全 总结报告 §6
IN-03 与 DT-06 重复:并入决策 分级清单原建议
WF-02-04/05 互斥手工补验(Bug 已登记) 宪章 01

变更记录(取舍决策存档)

决策存档,供追溯;执行以正文清单为准。09 状态系统已移出测试范围;宪章”用例目的”列已补(✅)。

01 武器与装备

用例 处置 理由
EQ-01 并入进图冒烟 单断言,本质是 FMapTestSpawner 健康检查
EQ-02-02 并入 EQ-02 与 -01 同属切换入口,断言结构相同
EQ-04 / EQ-05 并入 EQ-03 拾取三态(正常 / 重复 / 满槽)一例覆盖
WF-01-02 并入 WS-03 与弹药递减断言重叠
WF-01-03 蓝图功能测试(记录偏差) 原计划 CQTest,实际蓝图落地;散布并入 SP-03
WF-01-04 并入 DT-01-04 死亡禁交互子集
WF-02-03 并入 WF-02-02 同一入口的边界分支
WF-02-04/05 合成一例,手工补验 互斥时序
WS-01 / WS-02 / WS-03 降级为单元 曲线 / 配置读取无需 PIE
WS-04-01/02/04 降级为单元 倍率数据校验;WS-04-03 保留集成(伤害链路)
WA-01~03 保持手工 视觉 / 手感

02 控制

用例 处置 理由
MV-01-01~04 循环合并 不写四个重复断言
MV-02 并入 MV-01 松键归零是隐含断言
AM-01-03 删除 与 AM-02-04 重复
AM-03 / NV-02 / NV-04 保持手工 蓝图 GA / 手感,已决策
NV-01/03/05 中间值、最大值 并入 AM-01 / MV-01 跳高 / 下落 / 速度实测
NV-01/03/05 风险点 降级为单元 clamp 校验
FR-01/02 删除(CQTest 侧保留等价断言) DT-01 / DT-06 已覆盖

03 相机 | 04 HUD | 05 前端 | 07 队伍

系统 用例 处置 理由
03 相机 CM-01-04 删除 与 CM-01-02(水平旋转)重复
03 相机 CM-03 / NV-03 删除 游戏无可调 FOV / 无 BlendTime
03 相机 CM-04 删除 游戏无相机模式切换
04 HUD HD-01-06 删除 游戏无空手状态
05 前端 FE-01 并入 FE-05 同一加载链路
07 队伍 TM-03-03 删除 游戏无换队功能
07 队伍 NV-01 删除 游戏无队伍数量参数

08 初始化 | 10 事件

系统 用例 处置 理由
08 初始化 IN-03 与 DT-06 重叠,建议并入 重生链路重复(待决策)
10 事件 DT-02-01 并入 DT-01-05 相机模式栈需自动化校验
10 事件 DT-05-02 并入 DT-01-03 死亡碰撞禁用变体
10 事件 DT-05-05 / DT-07-02 / DT-07-03 删除 已从宪章移除
10 事件 DT-04 暂缓排期 高风险区,后续补齐
10 事件 SP-01-03 / SP-02-01 / SP-05-03 / SP-06-01 降级为单元 除零 / clamp 风险点

🚫 已废弃 13 — 测试辅助 API 设计方案

🚫 已废弃:本方案(LyraTestUtilities 蓝图优先的测试辅助 API)整体被放弃,回归 C++ CQTest + 手工测试;本文档仅作历史复盘参考。

⚠️ 状态:冻结(决策):蓝图功能测试已弃用,回归 C++ CQTest + 手工。
本方案的”蓝图用轮子”设计前提不再成立(LyraTestUtilities 以 BlueprintCallable 蓝图分类优先)。
如需测试辅助 API,直接以 C++ 函数形式提供给 CQTest 使用,不再以蓝图分类为优先。
本文档仅作历史参考。

本文档记录 LyraTestUtilities 模块的设计思路、API 规范和使用方法。
属于框架层面的设计方案,不涉及具体测试用例。


1. 背景与动机

1.1 问题

UE 内置的蓝图功能测试(AFunctionalTest)提供了基础框架,但缺少以下能力:

缺失能力 影响
输入模拟(按住/点击/释放) 无法模拟玩家操作
Gameplay 系统查询 无法直接读取弹药/血量/武器状态
Actor 查找与生成 需要在测试中手动处理

1.2 设计理念

C++ 造轮子,蓝图用轮子。流程控制交给 EnhancedCodeFlow(ECF)插件:

1
2
3
4
5
6
7
8
9
C++ (LyraTestUtilities / LyraGame)   蓝图 (FunctionalTest + ECF)
┌──────────────────────────────┐ ┌────────────────────────────┐
│ Input → Click/Press/Release/Hold │──→│ 输入模拟 │
│ World → GetPlayerController/Pawn │──→│ 世界查询 │
│ Gameplay → GetAmmo/GetHealth │──→│ 游戏状态读取 │
│ Utility → LookAt/MoveTo/Teleport│──→│ 工具操作 │
└──────────────────────────────┘ │ │
│ WhileTrue + Delay + AmmoChanged│ ← ECF
└────────────────────────────┘

1.3 架构分层

负责 技术方案
流程控制 循环、等待、条件判断 EnhancedCodeFlowWhile True ExecuteDelay、协程)
Lyra 专有查询 弹药/血量/武器/标签 LyraTestGameplayLibrary(LyraGame 内部)
输入模拟 注入增强输入 LyraTestInputLibrary
世界/工具 获取 Actor、传送、移动 LyraTestWorldLibrary / UtilityLibrary

1.4 设计目标

  1. 最小侵入 — 不修改 LyraGame 核心逻辑,不碰已有头文件
  2. 蓝图优先 — 所有函数标记 BlueprintCallable,以 Lyra|Test| 分类出现
  3. 只读优先 — 查询操作不修改游戏状态
  4. 条件编译 — 仅在非 Shipping 构建中编译

2. 模块结构

2.1 模块信息

属性
模块名 LyraTestUtilities
类型 Runtime(仅非 Shipping 构建)
路径 Source/LyraTestUtilities/
依赖 Core Engine EnhancedInput AIModule

2.2 Gameplay 查询特殊归属

ULyraTestGameplayLibrary 位于 LyraGame 内部(非 LyraTestUtilities),原因:

  • 需要访问 Lyra 内部类(ULyraQuickBarComponentULyraInventoryItemInstance 等)
  • 这些类没有 LYRAGAME_API 导出宏,外部模块无法链接
  • 作为 LyraGame 的新建文件存在,不修改已有代码

路径:

  • Source/LyraGame/Public/TestUtilities/LyraTestGameplayLibrary.h
  • Source/LyraGame/Private/TestUtilities/LyraTestGameplayLibrary.cpp

2.3 文件组织

1
2
3
4
5
6
7
8
9
10
11
12
Source/LyraTestUtilities/
├── LyraTestUtilities.Build.cs
├── Public/
│ ├── LyraTestUtilitiesModule.h
│ ├── Input/LyraTestInputLibrary.h
│ ├── World/LyraTestWorldLibrary.h
│ └── Utility/LyraTestUtilityLibrary.h
├── Private/
│ ├── LyraTestUtilitiesModule.cpp
│ ├── Input/LyraTestInputLibrary.cpp
│ ├── World/LyraTestWorldLibrary.cpp
│ └── Utility/LyraTestUtilityLibrary.cpp

2.4 编译控制

LyraGame.Target.csApplySharedLyraTargetSettings() 中条件添加:

1
2
3
4
if (!bIsShipping && !bIsTest)
{
Target.ExtraModuleNames.Add("LyraTestUtilities");
}

Shipping/Test 配置下模块不会被编译。


3. 函数库 API 参考

3.1 ULyraTestInputLibrary — 输入模拟

蓝图分类: Lyra|Test|Input

实现方式:通过 UEnhancedInputLocalPlayerSubsystem::InjectInputVectorForAction 注入输入信号,无需真实硬件。

函数 参数 说明
Click PC, Action, Value=(1,0,0) 模拟按下 → 延迟 2 帧 → 释放
Press PC, Action, Value=(1,0,0) 模拟按下(Inject Started)
Release PC, Action 模拟释放(终止 Hold + FlushPressedKeys)
Hold WorldContext, PC, Action, Value=(1,0,0) 持续按住(每帧注入),直到 Release 停止

注意事项:

  • Click 内部使用 BindWeakLambda 延迟释放,PC 销毁时自动取消
  • Hold + Release 可模拟长时间按住(如持续移动)

与 ECF 配合示例:

1
2
3
4
While True Execute
├── Click(PC, IA_Weapon_Fire_Auto)
├── Delay(ECF, 0.05)
└── AmmoChanged? → 继续

3.2 ULyraTestWorldLibrary — 世界查询

蓝图分类: Lyra|Test|World

函数 参数 返回 说明
GetPlayerController WorldContext, PlayerIndex=0 APlayerController* 获取指定索引的玩家控制器
GetPlayerPawn WorldContext, PlayerIndex=0 APawn* 获取指定索引的玩家 Pawn
SpawnActor WorldContext, Class, Transform AActor* 在世界中生成本地 Actor
FindActorByClass WorldContext, Class AActor* 查找第一个指定类型的 Actor

等待操作已移除: 改用 ECF 的 Delay / Delay Ticks / 协程。


3.3 ULyraTestGameplayLibrary — Gameplay 系统查询

蓝图分类: Lyra|Test|Gameplay

位置: LyraGame/Public/TestUtilities(非 LyraTestUtilities 模块)

函数 参数 返回 说明
GetCurrentWeapon PC AActor* 获取当前装备的远程武器 Actor
GetAmmo PC int32 获取弹匣内剩余弹药(读 StatTagStack MagazineAmmo
GetMagazine PC int32 获取弹匣总容量
GetHealth PC float 获取当前生命值
HasGameplayTag Actor, Tag bool 检查是否拥有指定 GameplayTag
AmmoChanged PC bool 弹药量是否自上次调用时变化(用于 ECF While 循环的条件判断)

与 ECF 配合示例 —— 连射到空弹匣:

1
2
3
4
5
6
While True Execute (ECF)
├── Click(PC, IA_Weapon_Fire)
├── Delay(0.05) ← ECF Delay,等一帧
└── 循环条件: AmmoChanged(PC) ← 弹药变了就继续

退出后: GetAmmo(PC) == 0 ? → FinishTest(Pass) : FinishTest(Fail)

等待条件示例(用 ECF 协程):

1
2
3
4
5
6
ECF Coroutine:
While True
WaitFrames(1) ← ECF 等一帧
If AmmoChanged(PC) ← 查询是否变化
Break
→ OnAmmoChanged ← 弹药变化了

注意事项:

  • 需要玩家 Pawn 上存在 ULyraQuickBarComponent(Lyra 默认角色已包含)
  • GetMagazine 需要武器数据资产中配置了 UInventoryFragment_SetStats
  • 弹药 GameplayTag: Lyra.ShooterGame.Weapon.MagazineAmmo

3.4 ULyraTestUtilityLibrary — 工具函数

蓝图分类: Lyra|Test|Utility

函数 参数 返回 说明
LookAt PC, Target 使玩家控制器看向目标 Actor
LookAtLocation PC, Location 使玩家控制器看向指定位置
MoveTo Pawn, Location 命令 Pawn 移动到位置(AI 下有效)
TeleportTo Actor, Location, Rotation, bSweep=false bool 传送 Actor

4. 版本记录

版本 变更内容
v1.0 初始版本 — 5 个函数库 + AsyncCondition
v1.1 采用 ECF 的 While True/Coroutine 替代等待方法
— 删除 WaitUntil/WaitWhile/WaitUntilAmmoChanged
— 删除 Wait/WaitFrame/WaitFrames
— 精简依赖(取消 LyraGame/GameplayTags)
— 保留 AmmoChanged 作为纯查询函数(ECF 循环条件用)

文档结束


🚫 已废弃 15 — Lua Test DSL 设计方案(基于 LuaMachine)

🚫 已废弃:Lua 测试框架方案(UnLua → LuaMachine + Test DSL)整体放弃,回归 C++ CQTest + 手工测试;复盘见 17-Lua测试框架复盘,本文档仅作历史参考。

本文档记录 LuaMachine + Test DSL 测试框架的架构设计、实施阶段、验收标准与风险分析。
目标:用 Lua DSL 替代蓝图作为主力测试编写方式(80% Lua + 20% C++),蓝图退居辅助。
⚠️ v1.2 重大变更:底层 Lua 引擎从 UnLua 切换为 LuaMachine(复核发现 UnLua 不兼容 UE 5.8)。
本文档属于框架层面的设计方案,不涉及具体测试用例。


1. 背景与动机

1.1 现状

LyraStarterGame 已建立两层半测试体系:

编写方式 框架 优点 痛点
C++ 单元/集成 .spec.cpp CQTest 编译期检查、性能最优 编译慢、门槛高
BP FunctionalTest 蓝图节点 AFunctionalTest + ECF 可视化、策划友好 二进制 diff、版本控制不友好
LyraTestUtilities C++ 函数库 BlueprintFunctionLibrary 被蓝图调用、复用 C++ 能力 仅作为桥梁,不改变蓝图编写模式

设计出发点:C++ 造轮子,Lua 用轮子。流程控制内置于 DSL Runtime(参考 13-测试辅助API设计方案)。

底层 Lua 引擎:**LuaMachine**(活跃维护,支持 UE5,Luau 方言)。

1.2 动机

蓝图功能测试在实际迭代中暴露致命问题,且随着测试规模膨胀愈演愈烈:

问题 说明
版本控制不友好 .uasset 二进制格式,diff/merge 几乎不可能,PR review 只能看图
批量修改困难 无法用文本工具全局搜索/替换,重构代价高
CI 文本化报告难 蓝图层执行结果需额外手段导出为文本格式
编写效率 复杂流程(多步断言、循环等待)节点连线冗长,不如文本代码紧凑
编辑器依赖 写测试必须打开 UE 编辑器,无法在轻量编辑器中快速编写

引入 Lua DSL 的目标是替代蓝图作为主力测试编写方式,而非再增加一个并行选项。

最终测试工作流比例:

1
2
3
4
5
6
┌──────────────────────────────────────┐
│ Lua DSL (80%) │ C++ CQTest (20%) │
│ 功能测试/集成测试 │ 底层单元/数值测试 │
│ 热重载/文本diff │ 编译期检查/性能 │
└──────────────────────────────────────┘
蓝图 → 退居辅助 / 逐步淘汰
  • Lua DSL(80%):大部分功能测试、集成测试、回归测试 → 热重载、文本 diff、CI 友好
  • C++ CQTest(20%):需要编译期检查的底层单元测试、纯数值/数学验证
  • 蓝图:不再推荐用于编写新测试;已有蓝图测试逐步迁移到 Lua

1.3 设计目标

  1. Lua 为主力 — 80% 测试用 Lua DSL 编写,蓝图不再推荐用于新测试
  2. 复用已有 C++ 轮子 — 通过 LuaMachine 手动注册绑定调用 LyraTestUtilities 全部 API(12 个函数,胶水代码量可控)
  3. DSL 声明式 + 简洁 — 测试用例结构清晰,接近自然语言,降低 Lua 学习成本
  4. 热重载 — 修改 .lua 文件后 lua.reload() 即可生效
  5. 统一测试入口 — 注册到 UE Automation Framework,与 CQTest 同一面板
  6. CI 友好 — 命令行可执行、JSON 报告输出、非零退出码
  7. 零侵入 — 作为独立插件存在,不影响 LyraGame 核心逻辑

2. 架构分层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
┌──────────────────────────────────────────────────────────┐
│ 测试编写层 │
│ ┌──────────────────────────────────────┐ │
│ │ ★ Lua Test DSL (主力,80% 测试) │ │
│ │ (.lua 文本文件,热重载,Git diff) │ │
│ └────────────────┬─────────────────────┘ │
│ │ │
│ ┌────────────────┼──────────────────────┐ │
│ │ BP FunctionalTest(遗留,逐步迁移) │ │
│ │ C++ CQTest(底层 20%) │ │
│ └────────────────┬──────────────────────┘ │
│ │ │
├───────────────────┼────────────────────────┤
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Test DSL Runtime(Lua 层) │ │
│ │ ┌───────────┐ ┌────────────┐ ┌────────────────┐ │ │
│ │ │ DSL Parser│ │ Assertion │ │ Test Runner │ │ │
│ │ │ (语法解析) │ │ (断言链) │ │ (发现+执行+报告)│ │ │
│ │ └───────────┘ └────────────┘ └────────────────┘ │ │
│ └────────────────────────┬────────────────────────────┘ │
│ │ │
├───────────────────────────┼────────────────────────────────┤
│ LuaMachine 桥接层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Lua ↔ C++ Binding(LuaMachine 反射系统) │ │
│ │ • LyraTestUtilities 全套 API 暴露 │ │
│ │ • GAS / Weapon / Character 核心 API │ │
│ │ • UE 通用类型(FVector / FRotator / FName / Tag) │ │
│ │ • Latent Action → Lua 协程 yield │ │
│ │ • ULuaFunctionalTest(Level-placed test actor) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
├───────────────────────────┼────────────────────────────────┤
│ C++ 轮子层 │
│ ┌───────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │LyraTest │ │ CQTest │ │ Automation │ │
│ │Utilities │ │ Framework │ │ Framework │ │
│ │(输入/世界/ │ │ (断言/固件/ │ │ (发现/执行/报告) │ │
│ │ Gameplay/工具) │ │ 参数化) │ │ │ │
│ └───────────────┘ └──────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────┘

2.1 三层职责

负责 技术
测试编写层 用户编写测试用例 .lua 文本文件(DSL)为主 + .spec.cpp(CQTest 底层)
LuaMachine 桥接层 Lua 调用 UE C++ API LuaMachine 手动注册 + ULuaTestBridge 薄封装
C++ 轮子层 核心测试能力 LyraTestUtilities + CQTest + Automation Framework

2.2 与现有体系的关系

1
2
3
4
5
6
7
8
现有(即将被替代):C++ → LyraTestUtilities → BP FunctionalTest → ECF(流程控制)
目标(Lua DSL): C++ → LyraTestUtilities → LuaMachine 桥接 → Lua DSL(流程控制内置于 DSL Runtime)

复用:
✅ LyraTestUtilities 全部 API(输入/世界/Gameplay/工具)
✅ Automation Framework(测试发现/执行/报告)
✅ 现有测试宪章/文档体系
✅ ECF 的流程控制思想 → DSL 内置 Wait/协程

替代路线:
🔄 新测试 → 一律用 Lua DSL 编写(除非需要编译期检查 → CQTest)
🔄 已有 BP FunctionalTest → 按优先级逐步迁移到 Lua DSL(Phase 6)
🔄 蓝图仅保留作为 Lua DSL 尚不支持的兜底方案

不碰:
❌ LyraGame 核心逻辑
❌ CQTest 已有测试(C++ 底层测试保持不动)



Phase 依赖关系

1
2
3
4
5
6
7
Phase 1 (LuaMachine 集成) ──┬──→ Phase 2 (DSL Runtime) ──┬──→ Phase 4 (Automation 集成)
│ │
└──→ Phase 3 (C++ 桥接) ──────┘

└──→ Phase 5 (CI/CD)

Phase 6 (迁移 + 手册) ←──────────────────────────────────────────────┘
  • Phase 1 是前提 — 所有后续 Phase 依赖 LuaMachine 可用
  • Phase 2 和 Phase 3 可并行 — DSL Runtime(纯 Lua)和 C++ 桥接(纯 C++)互不依赖
  • Phase 4 依赖 Phase 2+3 — 需要 DSL 语法可用 + C++ API 可用
  • Phase 5 依赖 Phase 4 — 需要 Automation 注册完成后才能 CLI 执行
  • Phase 6 可尽早启动 — 迁移指南和用户手册的部分内容可从 Phase 2 开始并行编写

3. 实施阶段

3.1 Phase 1 — LuaMachine 插件集成与基础设施

目标:引入 LuaMachine 插件,验证基础 Lua ↔ UE 互操作能力,搭建测试脚本目录结构。

背景:原方案使用 UnLua,经复核确认 UnLua 最新 release (v2.3.6) 仅支持到 UE 5.3,项目已实质停滞。切换为 LuaMachine — 当前唯一活跃维护的 UE Lua 插件,采用更保守的手动注册架构,对 UE 版本升级敏感度低。

工作项

  • rdeioris/LuaMachine 获取最新 release,安装到 Plugins/LuaMachine/
  • LyraStarterGame.uproject 注册插件
  • 配置项目 Lua 脚本目录 Content/Scripts/
  • 创建 Content/Scripts/Tests/ 作为测试脚本根目录
  • 学习 LuaMachine 的 API 注册模式:
    • 每个需要暴露给 Lua 的 C++ 函数需要在派生类中通过 LuaValue::Function 手动注册
    • Luau 方言特性:类型注解、if-then-else 表达式、复合赋值等
  • 编写 Smoke 验证用例:
    • 创建 ULuaTestRunner Actor,在 BeginPlay 中创建 Lua 状态并执行测试脚本
    • Lua 侧调用 C++ 注册的 PrintString 输出日志
  • 验证热重载:通过自定义控制台命令 lua.reload 重新加载脚本后执行新代码
  • 编写 ULuaTestBridge C++ 基类,作为所有 Lua 测试的入口点。提供:
    • static void RegisterAll(LuaState) — 在所有模式下注册 C++ API 到 Lua
    • ULuaTestRunner(派生 Actor)— P1 验证用,在 BeginPlay 中创建 VM + RegisterAll + 执行脚本
    • ULuaFunctionalTest(派生 AFuncTest)— P3 实现,PIE 关卡放置用

验收标准

编号 验收项 验证方式
✅ P1.1 LuaMachine 插件在 UE 5.8 编辑器正常加载,无启动报错 编辑器启动日志无 LuaMachine 相关 Error
✅ P1.2 Content/Scripts/Tests/ 目录创建,LuaMachine 可扫描该路径 .lua 文件放在该路径下可被 LuaMachine 加载
✅ P1.3 ULuaTestRunner Actor 在 PIE 中创建 Lua 状态并执行测试脚本 Output Log 显示 Lua 脚本的 print 输出
✅ P1.4 热重载:通过 ULuaTestRunner::ConsoleCommand("lua.reload") 触发重载后新代码生效 修改 Lua 脚本 → 执行 reload 命令 → PIE 中运行新逻辑
✅ P1.5 C++ 函数可通过 LuaMachine 注册到 Lua 全局表 ULuaTestBridge::RegisterAPI() 注册的 UE.PrintString 在 Lua 中可调用
✅ P1.6 Luau 方言语法可正常使用(类型注解不报错) --!strict 和类型注解的 .lua 文件正常加载执行

3.2 Phase 2 — Test DSL 语言设计与 Runtime 实现

目标:定义 DSL 语法规范,实现 Lua 侧 DSL Runtime(解析器 + 断言 + 执行器)。

工作项

  • 定义 DSL 语法 EBNF(Lua table 声明式)
  • 实现 DSL 核心模块:
    • TestRunner.lua — 测试发现、标签过滤、执行调度
    • TestContext.lua — 测试上下文(Setup / Execute / Teardown 生命周期管理)
    • Assertion.lua — 断言链(expect(x):to_equal(y) / :to_be_truthy() 等)
  • 实现生命周期钩子:before_all / after_all / before_each / after_each
  • 实现参数化测试:test.each(data) 语法
  • 实现超时控制:默认 30 秒,可配置
  • 编写语法规范文档(独立章节,见 16-LuaDSL-API参考

DSL 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
local suite = TestSuite("武器系统", {
tags = { "weapon", "ammo", "smoke" },
timeout = 30,
})

function suite:before_each(ctx)
ctx:LoadMap("/Game/Maps/L_Expanse")
ctx.hero = ctx:SpawnHero()
end

function suite:test_single_shot_ammo_consume(ctx)
ctx:GiveWeapon("WID_Rifle")
local before = ctx:GetAmmo()
ctx:Click("IA_Weapon_Fire")
ctx:WaitFrames(5)
ctx:expect(ctx:GetAmmo()):to_equal(before - 1)
end

function suite:test_magazine_empty_reload(ctx)
ctx:GiveWeapon("WID_Rifle")
local mag = ctx:GetMagazine()
for i = 1, mag do
ctx:Click("IA_Weapon_Fire")
ctx:WaitFrames(5)
end
ctx:expect(ctx:GetAmmo()):to_equal(0)
end

suite:run()

验收标准

编号 验收项 验证方式
✅ P2.1 test() / TestSuite() 语法正确解析,不报 Lua 语法错误 在 Lua 解释器中加载 DSL 模块无报错
✅ P2.2 expect(x):to_equal(y) / :to_be_truthy() / :to_be_falsy() / :to_contain(v) / :to_throw(fn) 五个断言可用 每个断言写单独的肯定/否定用例,全部通过
✅ P2.3 before_each / after_each 钩子在每个 test 前后正确执行 在钩子和 test 中各打一条日志,验证顺序
✅ P2.4 test.each(table_data) 参数化:同一 test 对多组数据各执行一次 3 组参数 → 3 次执行结果,失败时显示对应数据行
✅ P2.5 超时控制:默认 30s,可配;超时后标记为 Fail 并输出 “Test timed out” 写一个死循环 test,设 timeout=2,执行后 2 秒内 Fail
✅ P2.6 标签过滤:--filter-tags weapon,smoke 只执行匹配标签的测试 准备 3 个不同 tag 的 test,过滤后只执行 2 个
✅ P2.7 Luau coroutine.create / coroutine.resume / coroutine.yield 在 LuaMachine 嵌入环境中行为与标准 Lua 5.1 一致 写一个简单协程:yield 后由 C++ FTimerHandle resume,验证返回值正确传递

3.3 Phase 3 — C++ ↔ Lua 桥接层(手动注册)

目标:LyraTestUtilities 全部 12 个 API 通过 LuaMachine 手动注册到 Lua 侧,类型无损传递,Latent 操作支持协程。

与 LuaMachine 方案的核心差异

维度 UnLua(不可用) LuaMachine(当前方案)
API 暴露 自动反射绑定,零胶水代码 手动 AddFunction 注册,12 个函数 ≈ 120 行胶水代码
类型转换 自动 手动实现 FVector/FRotator/FName ↔ Lua table 转换
架构 侵入 UE 反射系统内部 独立 Lua VM,不碰 UE 内部
维护前景 已停滞 活跃维护

工作项

  • 编写 ULuaTestBridge C++ 类(Plugins/LuaTestFramework/Source/),提供静态方法 RegisterAll(LuaState)
    • 注意:不依赖 Actor 生命周期 — Automation 模式下无 World,必须支持纯静态注册
    • 封装 LyraTestInputLibrary → 注册为 Lua 全局函数 Click / Press / Release / Hold
    • 封装 LyraTestWorldLibrary → 注册为 GetPC / GetPawn / SpawnActor / FindActor / LoadMap
    • 封装 LyraTestGameplayLibrary → 注册为 GetAmmo / GetMagazine / GetHealth / GetCurrentWeapon / HasTag
    • 封装 LyraTestUtilityLibrary → 注册为 Teleport / LookAt / MoveTo
  • 实现类型转换层(手动):
    • FVector ↔ Lua {X, Y, Z}
    • FRotator ↔ Lua {Pitch, Yaw, Roll}
    • FName ↔ Lua string
    • FGameplayTag ↔ Lua string
    • TSubclassOf<AActor> ↔ Lua string(资产路径)
    • UObject 指针 → Lua 侧用整数 handle 或轻量 userdata
  • 实现 Latent Action 协程桥接:
    • ctx:Wait(seconds) → Lua coroutine.yield + C++ FTimerHandle resume
    • ctx:WaitFrames(n) → Lua coroutine.yield + C++ FTickableGameObject 计数 resume
    • ctx:WaitUntil(fn, timeout) → 每帧 poll fn(C++ 侧 Tick → 回调 Lua) + 超时
  • 实现 ULuaFunctionalTest(继承 AFunctionalTest):
    • 挂载 Lua 脚本路径
    • PrepareTest / StartTest / Tick 中调用 Lua 对应生命周期
    • 支持 PIE 模式下在关卡中放置并自动执行

验收标准

编号 验收项 验证方式
✅ P3.1 全部 12 个 LyraTestUtilities API 在 Lua 中可调用 每个 API 写一个 Lua 调用并检查返回值
✅ P3.2 FVector / FRotator 在 Lua ↔ C++ 间正确传递 调用注册的 ctx:GetPawnLocation() 返回 {X, Y, Z}ctx:Teleport(actor, pos) 将表正确解析为 FVector
✅ P3.3 FGameplayTag 从 Lua string 转换为 C++ Tag,HasTag 查询正常 ctx:HasTag(actor, "Lyra.ShooterGame.Weapon.MagazineAmmo") 返回 true/false
✅ P3.4 ctx:Wait(3) 在 Lua 协程中暂停 3 秒后继续 记录前后时间戳,差值 ≤ 3.0 ± 0.2 秒
✅ P3.5 ctx:WaitFrames(5) 在 Lua 协程中等待 5 帧后继续 与 UE Tick 对齐验证
✅ P3.6 ULuaFunctionalTest 可在关卡中放置,PIE 中自动执行 Lua 脚本 放置到 L_Expanse,PIE 中触发,输出日志含测试结果
✅ P3.7 手动注册胶水代码总量 ≤ 200 行 C++ 行数统计

3.4 Phase 4 — 测试运行器与 Automation 集成

目标:Lua 测试可在 UE Automation Framework 中发现、执行,并生成统一报告。

工作项

  • 实现 FLuaTestAutomationProvider:将 Lua 测试用例注册到 Automation Framework
    • IFileManager::IterateDirectoryRecursively 扫描 Content/Scripts/Tests/ 下所有 *_spec.lua 文件
    • 对每个文件:创建临时 Lua VM → require 该脚本(不调 run(),仅获取 TestSuite 元数据)→ Lua 回调 C++ 注册 test_* → 销毁临时 VM → 注册到 FAutomationTestFramework
  • 实现三种执行模式:
    • 编辑器 UI:Session Frontend → Automation 面板 → 勾选/运行 Lua 测试
    • 命令行UEEditor-Cmd.exe LyraStarterGame.uproject -ExecCmds="Automation RunTests LuaTests.Weapon.*" -Unattended -NullRHI
    • MCP:通过 UnrealMCP AutomationTestToolset 发现和触发 Lua 测试
  • 统一报告格式:Lua 测试结果对接 FAutomationTestFramework::Get().GetReport()
  • 失败时输出 Lua 错误堆栈和断言上下文(期望值 vs 实际值)

验收标准

编号 验收项 验证方式
✅ P4.1 Session Frontend Automation 面板中可见 LuaTests.* 条目 打开 Automation 面板,搜索 “LuaTests”,出现已注册的测试
✅ P4.2 在 UI 中勾选并运行 Lua 测试,结果(Pass/Fail)正确显示 准备 1 个 Pass + 1 个 Fail 测试,面板中分别显示绿色勾和红色叉
✅ P4.3 命令行 -ExecCmds="Automation RunTests LuaTests.*" 可执行并返回非零退出码(失败时) echo %ERRORLEVEL% → 失败时非零
✅ P4.4 UnrealMCP AutomationTestToolset 可发现并执行 Lua 测试 通过 MCP HTTP 调用 /mcp,返回 JSON 含 LuaTests 条目
✅ P4.5 失败时 Output Log 包含 Lua 堆栈和断言详情 故意让 to_equal(1, 2) 失败,Output Log 显示 expected 1 but got 2

3.5 Phase 5 — 工具链与 CI/CD 集成

目标:命令行执行、JSON 报告、CI Pipeline 示例。

工作项

  • 编写 RunLuaTests.bat / RunLuaTests.ps1:封装 UE 命令行执行
  • JSON 报告生成:测试名 / 状态 / 耗时 / 失败原因 / 断言详情
  • JUnit XML 报告转换(兼容 Jenkins / GitLab CI)
  • 编写 GitHub Actions workflow 示例:
    1
    2
    3
    4
    5
    6
    7
    - name: Run Lua Tests
    run: |
    .\RunLuaTests.ps1 -Filter "Smoke" -Report "test_report.json"
    - name: Publish Test Report
    uses: dorny/test-reporter@v1
    with:
    path: test_report.json
  • Lua 测试发现 CLI 工具:扫描 Content/Scripts/Tests/ 输出测试清单 JSON
  • Automation_LyraStarterGame.sln 的自动化方案并行配置

验收标准

编号 验收项 验证方式
✅ P5.1 RunLuaTests.ps1 -Filter "Weapon" -Report "report.json" 可正确执行 在 CI 机器上无图形界面执行,ExitCode 正确
✅ P5.2 report.json 包含每个测试的 name / status / duration_ms / error_message 跑 3 个测试(2 Pass + 1 Fail),JSON 数组含 3 个条目
✅ P5.3 JUnit XML 格式可用 Jenkins JUnit Plugin 解析 在本地 Jenkins 实例导入,显示测试趋势图
✅ P5.4 GitHub Actions workflow 示例文件在 .github/workflows/lua-tests.yml 可被 GitHub 识别 Push 后 Actions tab 出现 “Lua Tests” workflow

3.6 Phase 6 — 蓝图 → Lua 迁移与用户手册

目标:制定蓝图测试迁移策略,批量迁移现有 BP FunctionalTest,验证 80/20 目标可行性。

工作项

  • 梳理现有 BP FunctionalTest 清单(ShooterTests 插件 + Content/FunctionalTests/):
    • 标注复杂度(简单/中等/复杂)、优先级(高/中/低)
    • 识别可自动转换的模式(如 Click + WaitFrames + AssertAmmo)
  • 迁移策略(分批实施):
    • 第一批(Phase 6 内完成):简单 Smoke 测试 — B_Test_FireWeaponFT_WF_01_01_SingleShot
    • 第二批:中等复杂度 — WF_01_02_MultiShotB_Test_AutoRun
    • 第三批:复杂集成测试 — 动画测试、网络复制测试
  • 编写《蓝图 → Lua DSL 迁移指南》:
    • 常见 BP 节点 → Lua DSL 等价写法速查表
    • ECF 节点(WhileTrueExecute / Delay / Coroutine)→ Lua 协程转换
    • 迁移检查清单(功能对等、性能不退化、断言一致)
  • 编写《LuaMachine Test DSL 用户手册》(TestDocs/04-用户手册/):
    • 快速开始(5 分钟上手)
    • DSL 语法速查表
    • 常用测试模式(武器测试 / 输入测试 / 状态变更测试 / 网络测试)
    • FAQ

验收标准

编号 验收项 验证方式
✅ P6.1 现有 BP FunctionalTest 清单梳理完毕,每项标注复杂度和迁移优先级 可查看清单表格(CSV/Markdown)
✅ P6.2 ≥4 个 BP FunctionalTest 有功能等价的 Lua DSL 版本 两个版本在相同条件下执行,断言结果一致
✅ P6.3 迁移后代码行数对比:Lua DSL 版本 ≤ BP 版本节点数 1/3(估算) 统计表格
✅ P6.4 《蓝图 → Lua DSL 迁移指南》含常见 BP 节点等价写法 ≥20 条 速查表最少 20 条常见映射
✅ P6.5 《用户手册》覆盖 80% 以上日常测试场景 让一个不了解 Lua DSL 的开发者阅读,15 分钟内能写出第一个测试用例

3.7 执行模式:PIE vs Automation 双入口

Lua 测试脚本写一次,两种执行模式都可用:

PIE 模式ULuaFunctionalTest Automation 模式LuaTests.*
触发方式 关卡中放置 ULuaFunctionalTest → PIE 运行 → 自动执行 Session Frontend / 命令行 / UnrealMCP
视觉反馈 ✅ 实时看到角色开枪、移动、换弹 ❌ 无渲染(-NullRHI
调试体验 ✅ 可暂停、可单步断言、可截图 ❌ 仅日志输出
CI 集成 ❌ 需要 GPU/引擎渲染 ✅ 命令行无头执行
适用场景 开发期调试、录演示、手工验证 回归测试、CI 流水线、批量执行
GameFeature 加载 ✅ PIE 加载 Experience 时自动激活 ⚠️ 需代码显式激活 GameFeature 再执行测试

同一份 .lua 脚本,两条执行路径:

1
2
3
Content/Scripts/Tests/Weapon/single_shot_spec.lua
├─→ ULuaFunctionalTest(关卡放置) → PIE 执行 → 可视化反馈
└─→ FLuaTestAutomationProvider → Automation Framework → CI 批量跑

GameFeature 懒加载处理策略:

ShooterTests 及其他 GameFeature 插件默认处于 Registered 状态(不自动激活)。Lua 测试在执行前需要确保依赖的 GameFeature 已加载:

  • PIE 模式ULuaFunctionalTest 放置在 ShooterTests 关卡中时,关卡本身会触发 Experience 加载,GameFeature 自动激活。重写 IsReady() 等待 GameFeature Active 状态。
  • Automation 模式FLuaTestAutomationProviderRunTest() 前调用 UGameFeaturesSubsystem::LoadAndActivateGameFeaturePlugin() 显式激活依赖的 GameFeature。每个 TestSuite 通过 options.depends_on = {'ShooterCore', 'ShooterTests'} 声明所依赖的 GameFeature 插件名。
  • 超时保护:GameFeature 加载超时(默认 10 秒),超时后跳过依赖该 Feature 的测试并标记为 Skipped。

4. 模块结构

4.1 文件组织

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
LyraStarterGame/
├── Plugins/
│ ├── LuaMachine/ # LuaMachine 官方插件(已有仓库)
│ └── LuaTestFramework/ # 自定义 Lua 测试框架插件 (Phase 3 创建)
│ ├── LuaTestFramework.uplugin
│ ├── Source/
│ │ └── LuaTestFramework/
│ │ ├── LuaTestFramework.Build.cs
│ │ ├── Public/
│ │ │ ├── LuaTestBridge.h # Lua ↔ C++ API 桥接
│ │ │ ├── LuaFunctionalTest.h # AFuncTest 子类
│ │ │ └── LuaTestAutomationProvider.h # Automation 注册
│ │ └── Private/
│ │ ├── LuaTestBridge.cpp
│ │ ├── LuaFunctionalTest.cpp
│ │ └── LuaTestAutomationProvider.cpp
│ └── Content/
│ └── Scripts/ # (可选)C++ 侧的 Lua 配套脚本
├── Content/
│ └── Scripts/
│ └── Tests/ # ★ 所有 Lua 测试脚本的根目录
│ ├── Core/ # DSL Runtime
│ │ ├── TestRunner.lua # 测试发现 + 执行器
│ │ ├── TestContext.lua # 测试上下文 (Setup/Execute/Teardown)
│ │ └── Assertion.lua # 断言链
│ ├── Helpers/ # 测试辅助工具
│ │ ├── WeaponHelper.lua # 武器相关辅助
│ │ ├── InputHelper.lua # 输入模拟辅助
│ │ └── PawnHelper.lua # 角色生成辅助
│ ├── Weapon/ # 武器系统测试
│ │ ├── single_shot_spec.lua
│ │ ├── auto_fire_spec.lua
│ │ └── reload_spec.lua
│ ├── Control/ # 控制系统测试
│ │ ├── movement_spec.lua
│ │ └── camera_spec.lua
│ └── Integration/ # 集成测试
│ └── combat_flow_spec.lua
└── TestDocs/
├── 00-总纲/
│ ├── 15-UnLuaTestDSL设计方案.md # ★ 本文档(文件名含 UnLua 为历史原因,实际基于 LuaMachine)
│ └── 16-LuaDSL-API参考.md # ★ API 参考
└── 04-用户手册/
└── LuaMachineTestDSL用户手册.md # Phase 6 产出

4.2 模块依赖

1
2
3
4
5
6
7
8
9
10
11
12
LuaTestFramework (C++ Plugin)
├── LuaMachine # Lua VM + 手动注册 API(不侵入反射系统)
├── LyraGame # LyraTestGameplayLibrary (Link)
├── LyraTestUtilities # 输入/世界/工具 函数库 (Link)
├── CQTest # Automation Framework (Link)
├── EnhancedInput # InputAction 引用
├── GameplayAbilities # GameplayTag / ASC 引用
└── GameFeatures # UGameFeaturesSubsystem(懒加载控制)

脚本层 (Lua side)
└── Content/Scripts/Tests/Core/*.lua # 纯 Lua,零 C++ 依赖
└── Content/Scripts/Tests/Helpers/*.lua → 调用 LuaMachine 注册的 C++ API

5. 关键设计决策

决策 选择 理由
测试编写主力 Lua DSL(80%) 替代蓝图:热重载、文本 diff、CI 友好、脱离编辑器编写
C++ 角色 底层轮子(20%) CQTest 负责需要编译期检查的数值/数学验证
DSL 宿主语言 Luau(LuaMachine 内置) Lua 5.1 超集,类型注解可选,Roblox 开源,活跃维护
DSL 风格 Lua table 声明式 接近现有测试宪章的描述风格;expect():to_xxx() 链式断言业界通用
桥接方式 LuaMachine 手动注册 + ULuaTestBridge 薄封装 12 个 API 手动注册 ≈120 行胶水代码;薄封装处理 Latent、类型转换、上下文管理
流程控制 Lua 协程 coroutine.yield ECF 在 Lua 侧无直接等价物;协程是最自然的异步方案
测试注册 UE Automation Framework 与 CQTest / BP FunctionalTest 统一面板、统一 CI 流程
模块归属 独立 LuaTestFramework 插件 遵循 GameFeatures 模式;可选加载;零侵入 LyraGame
测试发现 文件系统扫描 + 约定优于配置 Content/Scripts/Tests/**/*_spec.lua 自动注册,无需手动维护列表

6. 风险与缓解

风险 等级 缓解措施
LuaMachine 在 UE 5.8 上的 API 兼容性 🟢 低 LuaMachine 采用独立 VM 架构,不侵入 UE 反射系统,对 UE 版本升级不敏感;已发版,Phase 1 首轮验证
Luau 方言学习成本 🟢 低 Luau 是 Lua 5.1 的超集(类型注解可选),DSL 语法极简,用户手册 5 分钟上手
手动注册胶水代码维护成本 🟢 低 LyraTestUtilities 仅 12 个函数,胶水代码量可控(≤200 行);新增 API 时遵循模板即可
Lua 调试体验不如蓝图可视化 🟡 中 详细断言失败上下文 + Lua 堆栈映射;PIE 模式可实时观察角色行为;后续可考虑 VSCode Luau 调试器集成
Lua VM GC / 生命周期管理 🟡 中 LuaMachine 每个 UObject 实例可持有独立 Lua 状态;测试结束后 VM 随 Actor 销毁自动回收
Lyra 内部类 API 变更导致 Lua 测试断裂 🟢 低 LyraTestUtilities 是 API 稳定层;Lua 只调用 LyraTestUtilities,不直接调用 LyraGame 内部类
GameFeature 懒加载导致 Lua 测试脚本加载时资产不可用 🟡 中 Phase 4 处理:Lua 测试在执行前等待 GameFeature 加载完成;由 ULuaFunctionalTestIsReady 保证
Lua 热重载在运行中的协程行为不确定 🟡 中 Phase 1 充分验证;热重载仅在测试未运行状态下生效;运行中的测试不受影响

7. 与 UnrealMCP 的协同

本项目已集成 UnrealMCP v0.9.1Plugins/UnrealMCP/),其 AutomationTestToolset 可发现和执行 UE Automation 测试。

Lua Test DSL 注册到 Automation Framework 后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AI Agent (Reasonix/Copilot/Cursor)


UnrealMCP (HTTP :2929/mcp)


AutomationTestToolset.discover() → 返回 CQTest + LuaTests 列表
AutomationTestToolset.run(name) → 执行指定测试


FAutomationTestFramework
├── CQTest tests
├── BP FunctionalTest tests
└── LuaTests.* ← 新增

这意味着:所有现有 MCP-based AI 测试工作流无需任何修改即可驱动 Lua 测试


8. 版本记录

版本 变更内容
v1.0 初始版本 — 6 阶段实施计划 + 架构设计 + 验收标准(当时基于 UnLua)
— 确立 LuaMachine + Lua DSL 方案
— 复用 LyraTestUtilities / Automation Framework / UnrealMCP
v1.1 定位修正 — Lua DSL 从「并行选项」升级为「蓝图替代品」
— 明确目标比例:80% Lua DSL + 20% C++ CQTest,蓝图逐步淘汰
— Phase 6 扩展为「蓝图→Lua 迁移」含分批策略 + 迁移指南
— 新增「蓝图节点→Lua DSL 等价写法」迁移速查表要求
v1.2 底层引擎切换 — UnLua → LuaMachine(复核发现 UnLua 不兼容 UE 5.8)
— Phase 1 重写:LuaMachine 安装 + Luau 方言 + 新验收标准
— Phase 3 重写:手动注册替代自动反射,新增 UnLua vs LuaMachine 对比表
— 新增 §3.7:PIE vs Automation 双入口 + GameFeature 懒加载处理策略
— 风险表全面更新:8 项风险重新评估
— 设计决策表更新:Luau 方言、手动注册架构

文档结束。配套 API 参考见 16-LuaDSL-API参考


🚫 已废弃 16 — Lua DSL API 参考

🚫 已废弃:随 Lua 测试框架方案整体放弃,本 API 参考不再维护,仅作历史参考;复盘见 17-Lua测试框架复盘

本文档是 Lua Test DSL 重写后的完整 API 参考手册。
新 API 按引擎底层概念组织为 7 个显式子命名空间,而非扁平的 ctx 方法。
原扁平 API(ctx:GetAmmo / ctx:Click 等)已移除,迁移到 ctx.input / Helpers/*.lua
设计文档见 15-UnLuaTestDSL设计方案


1. 快速开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- Content/Scripts/Tests/Weapon/default_weapon_spec.lua
local TestRunner = require("Tests.Core.TestRunner")
local WeaponHelper = require("Tests.Helpers.WeaponHelper")

local suite = TestRunner.TestSuite("武器基础测试", {
tags = { "weapon", "smoke" },
timeout = 30,
})

function suite:before_each(ctx)
local pc = ctx.actor:GetPC()
ctx:WaitUntil(function() return WeaponHelper.HasQuickBar(ctx, pc) end, 60)
ctx.cheat:GiveWeapon("WID_Rifle")
ctx:WaitFrames(30)
end

function suite:test_fire_consumes_one_ammo(ctx)
local pc = ctx.actor:GetPC()
local before = WeaponHelper.GetAmmo(ctx, pc)
ctx.input:Click("IA_Weapon_Fire")
ctx:WaitFrames(5)
ctx:expect(WeaponHelper.GetAmmo(ctx, pc)):to_equal(before - 1)
end

suite:run()

调用约定:每个 Helper 函数第一个参数为 ctx(TestContext 实例),后续为业务参数。


2. DSL 语法规范

2.1 TestSuite 定义

1
TestRunner.TestSuite(name:string, options:table?) → TestSuite
参数 类型 必填 说明
name string 测试套件名称,作为 Automation 注册名的一部分
options.tags table<string> 标签列表,如 { "weapon", "smoke" }
options.timeout number 每个 test 的超时秒数,默认 30
options.before_all function(suite, ctx) Suite 级别:所有 test 之前执行一次
options.after_all function(suite, ctx) Suite 级别:所有 test 之后执行一次

Automation 注册名LuaTests.<name>.<test_method_name>

2.2 Test 定义

1
2
3
function suite:test_<name>(ctx:TestContext)
-- test body
end
  • 方法名 test_xxx 自动识别为测试用例
  • 不以 test_ 开头的方法不会被当作测试执行
  • 所有 test_* 方法按声明顺序执行

2.3 生命周期钩子

1
2
3
4
function suite:before_all(ctx)  end   -- Suite 开始时执行一次
function suite:before_each(ctx) end -- 每个 test 之前执行
function suite:after_each(ctx) end -- 每个 test 之后执行
function suite:after_all(ctx) end -- Suite 结束时执行一次

执行顺序:

1
2
3
4
5
before_all
├─ before_each → test_1 → after_each
├─ before_each → test_2 → after_each
└─ ...
after_all

2.4 参数化测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function suite:test_each_reload(ctx)
ctx:each({
{ weapon = "WID_Rifle", mag = 30 },
{ weapon = "WID_Shotgun", mag = 8 },
})
end

function suite:test_each_reload.check_full_mag_after_reload(ctx, row)
local pc = ctx.actor:GetPC()
ctx.cheat:GiveWeapon(row.weapon)
for i = 1, row.mag do
ctx.input:Click("IA_Weapon_Fire")
ctx:WaitFrames(3)
end
ctx:expect(WeaponHelper.GetAmmo(ctx, pc)):to_equal(0)
ctx.input:Press("IA_Weapon_Reload")
ctx:WaitUntil(function() return WeaponHelper.GetAmmo(ctx, pc) == row.mag end, 5)
ctx:expect(WeaponHelper.GetAmmo(ctx, pc)):to_equal(row.mag)
end

2.5 标签过滤

1
2
3
-- 定义时打标签
local suite = TestRunner.TestSuite("全部武器测试", { tags = { "weapon", "regression" } })
-- 在 Automation 窗口中按标签名过滤执行

3. 命名空间 API 完整参考

3.1 ctx.actor — 世界/场景查询与操作

方法 参数 返回 说明
GetPC(index?) index:number 默认 0 APlayerController 获取第 N 个玩家控制器
GetPawn(index?) index:number 默认 0 APawn 获取第 N 个玩家 Pawn
SpawnActor(classPath, transform?) classPath:string, transform:table? AActor 在世界中生成 Actor
FindActor(classPath) classPath:string AActor? 查找第一个指定类型的 Actor
Teleport(actor, location, rotation?) actor, location:FVector, rotation:FRotator? 传送 Actor
LookAt(target) target:AActor 让 PC 注视 Actor
LookAtLocation(location) location:FVector 让 PC 注视位置
MoveTo(pawn, location) pawn:APawn, location:FVector 命令 Pawn 移动到位置
GetActorLocation(actor) actor:AActor {X,Y,Z} 获取位置(反射)
GetActorRotation(actor) actor:AActor {Pitch,Yaw,Roll} 获取旋转(反射)
GetVelocity(actor) actor:AActor {X,Y,Z} 获取速度(反射)
GetActorBounds(actor) actor:AActor {Origin,BoxExtent} 获取包围盒(反射)
LineTrace(from, to) from, to:FVector HitResult? 碰撞检测(反射)
GetAllActorsOfClass(classPath) classPath:string table<AActor> 按类查找所有 Actor(反射,当前 stub)
FindActorByTag(tag) tag:string AActor? 按 Tag 查找 Actor(反射)
GetComponentByClass(actor, className) actor, className:string UActorComponent? 按类名字符串查找组件(C++ 桥接)
GetChildActor(actor, name) actor, name:string AActor? 获取指定名称的子 Actor(反射)

3.2 ctx.ability — GAS 通用操作

方法 参数 返回 说明
HasTag(actor, tagName) actor, tagName:string boolean 检查 GameplayTag(现有 C++ 桥接)
CanActivateAbility(name) name:string boolean 查询能力是否能激活(反射,stub)
ActivateAbility(name) name:string boolean 主动触发能力(反射,stub)
GetCooldownRemaining(name) name:string number 查询能力冷却剩余时间(反射,stub)
GetAbilityTags(name) name:string table 查询能力标签(stub)
HasActiveEffect(effectClass) effectClass:string boolean 检查活跃 GameplayEffect(反射,简化)
GetEffectDuration() number 查询 Effect 持续时间(stub)
GetEffectStacks() number 查询 Effect 堆叠层数(stub)
GetAttribute(attributeName) attributeName:string number? 泛化 GAS 属性查询(stub,需专用桥接)

stub 标志表示该函数需要进一步 C++ 桥接支持(FGameplayAttribute 等 struct 类型),当前返回 nil/0。

3.3 ctx.state — 数据层通用原语

方法 参数 返回 说明
FindComponent(actor, className) actor, className:string UActorComponent? 按类名字符串查找组件(C++ 桥接)
FindComponents(actor, className) actor, className:string table 查找所有匹配组件
HasComponent(actor, className) actor, className:string boolean 检查组件是否存在
GetWorldSubsystem(className) className:string UWorldSubsystem? 获取世界子系统(反射)
GetGameState() AGameState? 获取 GameState(反射)
GetPlayerState(playerIndex?) playerIndex:number? APlayerState? 获取 PlayerState(反射)
GetProperty(object, propertyName) object, propertyName:string any 反射读取任意 UProperty。支持 10 种类型:int/float/bool/string/FName/FText/UObject/FVector/FRotator/FGameplayTag/TArray
GetStatTag(owner, tagName) owner, tagName:string number 读取 StatTagStack 计数(反射,string→FGameplayTag 自动转换)
HasStatTag(owner, tagName) owner, tagName:string boolean 检查 StatTag 是否存在
SetStatTag(owner, tagName, count) owner, tagName:string, count:number boolean 设置 StatTagStack 计数(仅 Authority)
GetInventoryItems(actor) actor:AActor table<ULyraInventoryItemInstance> 获取背包物品列表(反射)
GetItemStatTag(itemInstance, tagName) item, tagName:string number 读取物品级 StatTag
GetItemDefinition(itemInstance) item ULyraInventoryItemDefinition? 获取物品定义

3.4 ctx.asset — 资产操作

方法 参数 返回 说明
LoadClass(path) path:string UClass? 按路径加载类(C++ 桥接)
LoadAsset(path) path:string UObject? 按路径加载资产(C++ 桥接,同 LoadClass)
FindObject(path) path:string UObject? 按路径查找已加载对象(反射,stub)
ResolvePath(shortName) shortName:string"WID_Rifle" string? 将简写名解析为完整资产路径(Lua 实现)
GetDataTableRow(dtPath, rowName) dtPath, rowName:string table? 读取 DataTable 行(反射,stub)
GetCurveTableValue(ctPath, curveName, input) ctPath, curveName:string, input:number number? 读取 CurveTable 值(反射,stub)
AssetExists(path) path:string boolean 检查资产是否存在

3.5 ctx.input — 输入模拟

方法 参数 说明
Click(action, value?) action:string (如 "IA_Weapon_Fire"), value:FVector? (默认 {X=1,Y=0,Z=0}) 单次点击:按下 → 2 帧 → 释放
Press(action, value?) 同上 按下(Started),不释放
Release(action) action:string 释放(Completed
Hold(action, value?) 同 Click 持续按住,每帧注入 Ongoing,直到调用 Release
MoveAxis(axis, value) axis:string, value:FVector 模拟轴输入(包装 Click)
KeyDown(key) key:string"LeftMouseButton" 键盘按键按下(反射)
KeyUp(key) key:string 键盘按键释放(反射)
TypeKey(key) key:string 按下再释放
MouseMove(dx, dy) dx, dy:number 鼠标增量移动(通过 AddYawInput/AddPitchInput
MouseClick(button?) button:string? 默认 "LeftMouseButton" 鼠标点击快捷方式
MouseWheel(delta) delta:number 鼠标滚轮(反射,stub)
ActivateInputMapping(context) context:string 激活输入上下文(stub)
DeactivateInputMapping(context) context:string 停用输入上下文(stub)

3.6 ctx.cheat — 作弊与状态覆盖

方法 参数 说明
GiveWeapon(weaponPath) weaponPath:string"WID_Rifle" 给当前 Hero 装备武器(C++ 桥接)
SpawnHero() 生成本地 Hero Pawn(C++ 桥接)
SetHealth(pc, value) pc, value:number 覆盖血量(反射+FindComponent)
SetMaxHealth(pc, value) pc, value:number 覆盖最大血量(ConsoleCommand)
SetAmmo(pc, ammo, reserve?) pc, ammo:number, reserve:number? 设置弹药量(反射+StatTag)
GrantAbility(abilityClass) abilityClass:string 授予能力(反射+ASC)
RemoveAbility(abilityClass) abilityClass:string 移除能力
ForceActivateAbility(abilityClass) abilityClass:string 强制激活能力
ApplyEffect(effectClass, level?) effectClass:string, level:number? 应用 GameplayEffect(ConsoleCommand)
RemoveEffect(handle) handle 移除 Effect
ClearAllEffects() 清除所有 Effect
AddTag(actor, tag) actor, tag:string 添加 GameplayTag(反射+ASC)
RemoveTag(actor, tag) actor, tag:string 移除 GameplayTag
KillActor(actor) actor:AActor 杀死 Actor(反射+HealthComponent)
ResetHealth(actor) actor:AActor 恢复满血
TriggerDeath() 自杀(ConsoleCommand)
SetProperty(object, name, value) object, name:string, value 反射写入任意 UProperty

3.7 ctx.config — 配置

方法 参数 说明
ConsoleCommand(command) command:string 执行控制台命令(C++ 桥接)
ActivateGameFeature(pluginName) pluginName:string"ShooterCore" 激活 GameFeature(C++ 桥接)
SetCVar(name, value) name, value:string 设置控制台变量(ConsoleCommand 包装)
GetCVar(name) name:string 读取控制台变量(stub)
ResetCVar(name) name:string 重置控制台变量为默认值
GetLocalSettings() 获取本地设置对象(反射)
GetSharedSettings(playerIndex?) playerIndex:number? 获取共享设置对象(反射)
GetDeveloperSettings(className) className:string 获取开发者设置(stub)

4. 顶层方法(保留在 ctx

4.1 流程控制

方法 参数 说明
:Wait(seconds) seconds:number 暂停指定秒数。必须在协程中调用
:WaitFrames(n) n:number 暂停 N 帧。必须在协程中调用
:WaitUntil(fn, timeout?) fn:function→boolean, timeout:number? (默认 10s) 每帧轮询 fn,fn 返回 true 或超时继续
:WaitWhile(fn, timeout?) 同上,fn 返回 false 时继续 与 WaitUntil 逻辑相反
:Tick(fn) fn:function(deltaTime) 注册每帧回调,返回取消函数(stub — 当前无操作)

4.2 断言

所有断言通过 ctx:expect(value) 返回断言对象,链式调用。

1
2
3
4
5
6
7
8
9
ctx:expect(1 + 1):to_equal(2)
ctx:expect(true):to_be_truthy()
ctx:expect(nil):to_be_nil()
ctx:expect(5):to_be_less_than(10)
ctx:expect(3.1415):to_be_approx(3.1416, 0.001)
ctx:expect({ "a", "b" }):to_contain("a")
ctx:expect({ key = 1 }):to_have_key("key")
ctx:expect({}):to_be_empty()
ctx:expect(42):to_be_type("number")

ctx:expect(v, label?) — 可选标签。to_xxx(expected, msg?) — 可选失败消息。

4.3 其他

方法 参数 说明
:Log(message) message:string 输出日志到 UE Output Log
:TakeScreenshot(name?) name:string? 截屏

5. Helpers 层 API

Helpers 是基于命名空间 API 构建的语义层,提供 Lyra 项目特定的高层查询函数。

5.1 StateHelper

local StateHelper = require("Tests.Helpers.StateHelper")

函数 说明
StateHelper.GetHealth(ctx, pc) 获取玩家血量(通过 FindComponent→GetProperty)
StateHelper.GetMaxHealth(ctx, pc) 获取玩家最大血量
StateHelper.IsDead(ctx, pc) 判断玩家是否死亡(血量 ≤ 0)
StateHelper.GetCurrentWeapon(ctx, pc) 获取当前远程武器 Actor
StateHelper.HasQuickBar(ctx, pc) 判断 QuickBar 是否已初始化(用于 WaitUntil 轮询)
StateHelper.HasWeaponReady(ctx, pc) 判断武器是否就绪(弹药 > 0)

5.2 WeaponHelper

local WeaponHelper = require("Tests.Helpers.WeaponHelper")

函数 说明
WeaponHelper.GetAmmo(ctx, pc) 获取当前弹药数(通过 StatTagStack)
WeaponHelper.GetMagazine(ctx, pc) 获取弹匣容量
WeaponHelper.AmmoChanged(ctx, pc) 弹药变化边缘检测(预读第一次)
WeaponHelper.FireUntilEmpty(ctx, actionName) 循环射击直到空膛,返回最终弹药数

5.3 CombatHelper

require("Tests.Helpers.CombatHelper")

函数 说明
IsDeadOrDying(ctx, actor) 判断角色死亡/正在死亡
GetDeathState(ctx, actor) 获取死亡状态枚举值

5.4 TeamHelper

require("Tests.Helpers.TeamHelper")

函数 说明
GetTeamId(ctx, actor) 获取队伍 ID
CompareTeams(ctx, a, b) 比较两队(返回 “Same”/“Different”/“Invalid”)
GetTeamScore(ctx, teamId) 获取队伍分数

5.5 InventoryHelper

require("Tests.Helpers.InventoryHelper")

函数 说明
HasItem(ctx, pc, itemDefName) 判断是否有指定物品
ItemCount(ctx, pc, itemDefName) 获取物品数量
GetEquippedItem(ctx, pc) 获取当前装备槽物品实例

6. 调用约定速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-- 获取玩家控制器/Pawn
local pc = ctx.actor:GetPC()
local pawn = ctx.actor:GetPawn()

-- 输入模拟
ctx.input:Click("IA_Weapon_Fire")
ctx.input:Press("IA_Weapon_Reload")
ctx.input:Hold("IA_Weapon_Fire_Auto")

-- 装备/作弊
ctx.cheat:GiveWeapon("WID_Rifle")
ctx.cheat:SetHealth(pc, 50)

-- 状态查询(Helpers)
local ammo = WeaponHelper.GetAmmo(ctx, pc)
local health = StateHelper.GetHealth(ctx, pc)
local ready = StateHelper.HasQuickBar(ctx, pc)

-- 状态查询(通用原语)
local hc = ctx.state:FindComponent(pawn, "LyraHealthComponent")
local healthVal = ctx.state:GetProperty(hc, "Health")
local ammoCount = ctx.state:GetStatTag(item, "Lyra.ShooterGame.Weapon.MagazineAmmo")

-- GAS 查询
ctx.ability:HasTag(actor, "State.Dead")

-- 配置
ctx.config:ConsoleCommand("stat fps")
ctx.config:SetCVar("r.SetRes", "1280x720")

-- 资产
ctx.asset:LoadClass("/Game/Weapons/WID_Rifle.WID_Rifle_C")
ctx.asset:ResolvePath("WID_Rifle") -- Lua 侧路径解析

-- 顶层方法(保持不变)
ctx:Wait(0.5)
ctx:WaitFrames(5)
ctx:WaitUntil(fn, 10)
ctx:expect(value):to_equal(expected)
ctx:Log("message")

7. 类型转换说明

Lua ↔ UE 类型映射

UE 类型 Lua 表示 说明
int32/float number 自动转换
bool boolean 自动转换
FString/FName/FText string 自动转换
UObject* userdata FLuaValue(Object)
FVector {X=number, Y=number, Z=number} GetProperty / _Call 自动转换
FRotator {Pitch=number, Yaw=number, Roll=number} 自动转换
FGameplayTag string _Call / GetProperty 中自动转换
TArray table (1-indexed) GetProperty 自动转换

示例

1
2
3
4
5
6
7
local loc = ctx.actor:GetActorLocation(actor)
-- loc = { X = 100.0, Y = 200.0, Z = 50.0 }

ctx.ability:HasTag(actor, "State.Dead") -- 字符串→FGameplayTag

local tags = ctx.state:GetProperty(actor, "Tags")
-- tags = { [1] = "PlayerTag", [2] = "TeamA" }

8. 内部 API(不对外暴露)

以下函数注册在 Lua 全局中,仅供 TestContext.lua 内部使用,测试代码不得直接调用:

函数 类型 说明
GetPC(index) / GetPawn(index) C++ 桥接 从 ctx.actor 转发到 C++
SpawnActor / FindActor C++ 桥接 从 ctx.actor 转发
Click / Press / Release / Hold C++ 桥接 从 ctx.input 转发
HasTag C++ 桥接 从 ctx.ability 转发
GiveWeapon / SpawnHero C++ 桥接 从 ctx.cheat 转发
ConsoleCommand / ActivateGameFeature C++ 桥接 从 ctx.config 转发
LoadObject C++ 桥接 从 ctx.asset 转发
FindComponent C++ 桥接 从 ctx.state 转发
_Call(obj, func, {args}) 反射入口 调用任意 UFunction
GetProperty(obj, name) 反射入口 读任意 UProperty
SetProperty(obj, name, value) 反射入口 写任意 UProperty
Log / TakeScreenshot C++ 桥接 通用工具

9. 已知限制与计划

功能 状态 障碍
_Call 支持 out-parameters ❌ 限制 GetAllActorsOfClass 等带输出参数的函数无法工作
GetAttribute ❌ stub 需要 FGameplayAttribute struct 专用桥接
GetDeveloperSettings ❌ stub GetDefault 是静态 C++ 函数,非 UFUNCTION
GetCVar ❌ stub 需要 IConsoleManager::FindConsoleVariable 桥接
Tick(fn) callback ❌ stub 需要真正的 FTickableGameObject 每帧回调
完整 Ability/Effect 查询 ⚠️ 简化 HasActiveEffect 等用反射简化实现,Notable 查询待完善

10. 版本记录

版本 变更内容
v2.0 全面重写 — 7 个命名空间 API + 5 个 Helper 文件 + 4 个反射入口
— 替换所有扁平 API 为命名空间调用
— 新增 ctx.state 通用原语层(FindComponent/GetProperty/StatTag/Inventory)
— 新增 ctx.ability:HasTag/GetCooldownRemaining/HasActiveEffect
— 新增 ctx.actor:GetActorLocation/LineTrace/GetComponentByClass 等反射函数
— 新增 ctx.input:MoveAxis/KeyDown/KeyUp/MouseMove 等扩展输入
— 新增 ctx.cheat:SetHealth/GrantAbility/ApplyEffect 等 15 个作弊函数
— 新增 ctx.config:SetCVar/Get/Reset + LocalSettings
— Helpers: 5 个文件 18 个语义函数
— 全部 7 个已有测试已迁移到新 API

文档结束。设计文档见 15-UnLuaTestDSL设计方案。架构决策见 docs/adr/0001-*.mddocs/adr/0002-*.md


17 — Lua 测试框架搭建复盘

状态:复盘完成
结论:Lua(LuaMachine)测试框架方案已放弃,测试体系回归 C++ CQTest + 手工测试。
本文档是 15-UnLuaTestDSL设计方案16-LuaDSL-API参考docs/adr/0001-* / 0002-* 的收尾记录,仅作历史参考。


0. 一句话结论

在 LyraStarterGame 中用 Lua 搭建测试框架的尝试,最终在决策窗口终止。放弃的直接原因是:

  1. 对 Lua DSL 设计缺乏深入调研 —— 没有先弄清”DSL 要隐藏什么、暴露什么、写给谁”,就进入了实现;
  2. 对抽象层设计没有把握 —— Lua API 层直接连接 C++ 桥接层,没有形成有语义厚度的抽象;
  3. 测试中的 Lua 脚本混入大量游戏引擎层逻辑 —— 脚本编写者仍需直面 PC / Pawn / ASC / StatTag / QuickBar 等引擎概念,没有做到”简化测试”的初衷

最终选择:放弃 Lua 方案,直接用 C++(CQTest)编写自动化测试。


1. 背景与目标(当初为什么搭 Lua 测试框架)

蓝图功能测试(AFunctionalTest)在项目迭代中暴露了 5 个痛点(见 15-UnLuaTestDSL设计方案 §1.2):

痛点 说明
版本控制不友好 .uasset 二进制,diff/merge 几乎不可能
批量修改困难 无法文本搜索/替换,重构成本高
CI 文本化报告难 蓝图层结果需额外手段导出
编写效率低 多步断言/循环等待的节点连线冗长
编辑器依赖 写测试必须打开 UE 编辑器

由此提出 Lua DSL 方案,目标是把测试编写主力从蓝图换成 Lua:80% 测试用 Lua DSL(功能/集成/回归),20% 用 C++ CQTest(底层数值/编译期检查),蓝图退居辅助。核心卖点是热重载、文本 diff、CI 友好

方案演进:

  • 13-测试辅助API设计方案:C++ 造轮子、蓝图用轮子 → LyraTestUtilities 函数库 + ECF 流程控制
  • 15-UnLuaTestDSL设计方案(v1.0 → v1.2):C++ 造轮子、Lua 用轮子 → LuaMachine 桥接 + DSL Runtime(底层引擎从 UnLua 切换为 LuaMachine,因 UnLua 不兼容 UE 5.8)

2. 实际发生的时间线

事件 产物 / 证据
测试辅助 API 方案 v1.0(蓝图优先) TestDocs/00-总纲/13-测试辅助API设计方案.md
Lua DSL 设计方案 v1.0→v1.2(UnLua → LuaMachine);API 命名空间与混合桥接决策 15-*docs/adr/0001-*0002-*
DSL Runtime 与学习记录:metatable 是命名空间基石 learning-records/0001-*
API 参考 v2.0:7 个命名空间 + 5 个 Helper + 反射入口;7 个测试脚本迁移 16-LuaDSL-API参考.md
PIE 集成 handoff:发现 Automation 中测试”跑通”但 PIE 从未启动 .reasonix/handoffs/handoff-lyra-lua-test-dsl-pie-plan-d.md
决策:弃用蓝图功能测试;事件测试改用 CQTest 显式驱动;13 号方案冻结 00-测试总纲.mddocs/adr/0003-*、13 号方案头部冻结标记
测试体系收敛为 C++ CQTest + 手工;Lua 方案正式终止并复盘 本文档

实际完成量:

  • Plugins/LuaTestFramework:C++ 桥接层(32 个注册函数)+ Automation 注册 + PIE 入口
  • Content/Scripts/Tests/:DSL Runtime(TestRunner / TestContext / Assertion)+ 7 个测试脚本 + 5 个 Helper
  • 配套设计文档、ADR、学习记录、运行脚本(RunLuaTests.bat
  • 最终状态:LuaTestFramework 插件源码保留在仓库,但未登记进 LyraStarterGame.uproject 的启用插件列表LuaMachine 仍有登记),已不在项目主链路中

3. 放弃原因复盘(核心)

3.1 原因一:对 Lua DSL 设计缺乏深入调研

现象:设计方案在关键问题没有调研结论的情况下直接进入实施:

  • 没有回答 DSL 的语义模型:测试语法描述的是”给武器上弹、射击、角色复活”这类游戏行为,还是”调用引擎 API”?
  • 没有验证 LuaMachine / Luau 的能力边界(反射、类型、协程、IDE 补全)就写进目标:spec 把”完整 intellisense / IDE 补全”列为验收目标,实际从未落地;
  • 风险表把”手动注册胶水代码维护成本”估为 🟢 低(≤200 行),实际每个新 API 都要改 C++、重编译、再同步 Lua 文档与类型转换,维护点远不止代码行数;
  • 实施后期才暴露基础问题:API 参考 v2.0标注了 6 个 stub(GetAttribute / GetEffectDuration / GetCVar / GetDeveloperSettings / Tick 回调等),次日 PIE 集成 handoff 又发现”测试跑通但 PIE 从未启动”。框架的地基问题是在投入大量工作量之后才被验证的。

教训:DSL 设计属于”前置设计投入占比很高”的工作,不调研语言生态、IDE 生态、执行模型就开工,等于用实现成本去买设计结论。

3.2 原因二:抽象层设计没有把握,Lua API 层直接连接 C++ 桥接层

现象TestContext.lua 几乎是桥接函数的”别名表”,没有语义厚度:

1
2
3
4
5
6
7
8
9
10
11
12
-- 透传:Lua 命名空间 = C++ 桥接函数换个名字
function ActorNS:GetPC(index) return GetPC(index or 0) end
function ActorNS:SpawnActor(...) return SpawnActor(...) end

-- 反射入口直接裸露在各命名空间实现中(约定"内部 API 不暴露"没守住)
function ActorNS:GetActorLocation(actor)
return _Call(actor, "K2_GetActorLocation", {})
end
function StateNS:GetWorldSubsystem(className)
...
return _Call(world, "GetSubsystemBase", { LoadObject(className) })
end

后果:

  • 约定为内部 API_Call / GetProperty / SetProperty / FindComponent 被各命名空间普遍使用,测试可见路径上到处是字符串函数名、类名字符串;
  • 引擎概念原样穿过抽象层:魔法数字(ECC_WorldStatic = 0IE_Pressed = 1IE_Axis = 2)、硬编码 Tag("Lyra.ShooterGame.Weapon.MagazineAmmo")、console 命令字符串("cheat SetHealth %f");
  • API 表面远大于实现:多个命名空间函数是 stub(返回 0 / nil / 空表),”抽象层”实际是”未完成的桥接层的门面”。

本质:抽象层不是”把 C++ 函数包一层”,而是”把引擎概念消化成领域语义”。这一步没有做,抽象层就等于零。

3.3 原因三:Lua 脚本混入大量引擎层逻辑,未实现”简化测试”的初衷

现象:测试脚本要求编写者具备完整的 UE 引擎知识,复杂度并不比 C++ 低:

single_shot_spec.luabefore_each 需要 15+ 行引擎层准备逻辑:

1
2
3
4
5
6
7
8
function suite:before_each(ctx)
local pc = ctx.actor:GetPC()
ctx:WaitUntil(function() return StateHelper.HasQuickBar(ctx, pc) end, 60)
ctx.cheat:GiveWeapon("WID_Rifle")
ctx:WaitUntil(function() return StateHelper.HasWeaponReady(ctx, pc) end, 30)
ctx:Log(string.format("Weapon ready: ammo=%d mag=%d",
WeaponHelper.GetAmmo(ctx, pc), WeaponHelper.GetMagazine(ctx, pc)))
end

Helper 层也没有把抽象下沉到业务语义,而是用引擎概念组合:

1
2
3
4
-- StateHelper / WeaponHelper 内部:
local hc = FindComponent(pawn, "LyraHealthComponent")
local qb = FindComponent(pc, "LyraQuickBarComponent")
_Call(item, "GetStatTagStackCount", { "Lyra.ShooterGame.Weapon.MagazineAmmo" })

结论:编写者需要同时懂 Lua、UE 反射体系、Lyra 内部组件——比直接写 C++ 多了一层翻译成本,还丢了 C++ 的编译期检查。这与弃用蓝图的理由完全同构:”复杂逻辑(输入模拟 + 异步等待 + 状态断言)在蓝图中难以编写和维护”——换成 Lua 并没有解决这个问题,只换了一种写法

3.4 工程事实(加速决策的最后一根稻草)

  1. PIE 集成断裂:handoff 文档确认,测试在 Automation 中”跑通”了,但 PIE 全程未启动;EditorContextCurrentWorld == nullptr,依赖 World 的桥接函数静默返回假值。测试验证的是 Lua 语法,不是集成行为。 而这正是 Lua 方案最大的卖点(替代蓝图做集成测试)。
  2. 双语言断层成本:类名/函数名/属性名以字符串形式在运行时才报错;C++ 桥接改一处,Lua 文档、API 参考、类型转换要同步多处;调试需要跨 Lua 协程与 UE Tick 两套执行模型。
  3. 对比实验:同一时期的 C++ CQTest(EventSystem_Death.spec.cpp)证明,死亡/重生这类复杂链路(事件捕获、状态轮询、真实伤害管线、重生完整性)在 C++ 下直接可行——引擎类型、编译期检查、调试器都是现成的。

4. 根源分析(为什么会出现这个结果)

# 根源 说明
1 目标不可度量 “80% Lua””简化测试””CI 友好”没有量化验收标准,无法在早期发现”测试没有变简单”这一事实
2 用换语言解决编写方式问题 痛点是框架能力(输入模拟、异步等待、断言、报告),这些 C++ 已有轮子(CQTest + LyraTestUtilities);Lua 没有带来新能力,只带来新断层
3 跳过语义模型设计 抽象层的前置条件(DSL 到底表达什么、给谁写、怎么写最省)没做完,直接进入桥接实现
4 先搭框架、后写用例 验证路径是”语法跑通”,而不是”测试编写者能否快速写出并维护真实用例”;金样用例(golden tests)没有先行
5 对 UE 测试本质认识不足 CQTest + FMapTestSpawner 下几乎所有用例都是集成测试,地图加载、异步时序、状态轮询的成本不会因为换了脚本语言而消失;Lua 只增加了翻译层,没有减少任何固有成本

5. 做对了什么(保留价值)

这次尝试不是零产出,以下资产被保留并继续生效:

  • 测试宪章体系:84 个用例(含子状态)的划分、优先级、质量门禁保留至今,是”测什么”的稳定资产;
  • 自动化 vs 手工的划分框架:决策固化为”重复性高的用例 → C++ CQTest;需要视觉/手感/听感判断 → 手工”,选择标准是”是否会被反复回归执行”;
  • LyraTestUtilities C++ 函数库:输入模拟(Click/Press/Release/Hold)、世界查询、Gameplay 查询(弹药/血量/Tag)从”蓝图优先”转向供 CQTest 直接调用,避免了重复造轮子;
  • UE5.8 Automation 生态调研结论:Session Frontend 无 Device 标签页、ClientContext 测试不可被 Automation 发现、Epic 用 Gauntlet 进程外驱动等,这些结论对后续测试工具选型仍有价值;
  • Lua 语言学习:metatable / 闭包 / 协程的理解沉淀在 learning-records/0001-*,是有效的个人知识资产;
  • 决策记录习惯:ADR 流程(0001/0002/0003)与本次复盘本身,证明了”决策留痕”的价值。

6. 如果重来:三条可执行建议

  1. 先做最小验证,再决定引入新语言。用 3 个”金样用例”(覆盖输入模拟 + 异步等待 + 状态断言)分别用 C++ CQTest 和候选 DSL 各写一遍,对比编写时间、调试体验、维护成本后再立项。本次的结果大概率在立项阶段就会被否决。
  2. 真要做 DSL,先定义语义模型。DSL 的语法应该描述游戏行为(”给武器上弹””射击””等待弹药变化””角色复活”),而不是引擎 API(GetPC / FindComponent / GetStatTag)。用目标用例倒推语法,先让最少的桥接跑通金样用例,再决定是否扩 API。
  3. **抽象层必须验收”语义厚度”**。禁止透传和 stub 进入公开 API 表面;以”新测试编写者写出第一个用例的时间与行数、新人是否需要懂引擎概念”作为抽象层是否成立的验收标准。

7. 结论与当前状态

当前测试策略(决策,见 00-测试总纲.md

1
2
3
4
5
仅两种方式:
1. C++ CQTest —— 重复性高的用例(数值边界、状态流转、输入模拟、回归验证)
2. 手工测试 —— 其余需要视觉/手感/听感判断的用例
蓝图功能测试:弃用
Lua 测试 DSL :终止(本次复盘)

框架分工:CQTest(游戏内集成/数值/输入)+ Spec(UI 流程)+ Gauntlet(启动链路/性能)+ 手工。

Lua 框架资产状态

资产 状态
Plugins/LuaTestFramework/(C++ 桥接 + Automation Provider) 保留源码,未登记进 uproject 启用列表,不再维护
Content/Scripts/Tests/(DSL Runtime + 7 个脚本) 保留作历史参考与 Lua 学习材料,不再新增用例
TestDocs/00-总纲/13/15/16 设计文档 标记为历史参考(13 已冻结)
docs/adr/0001/0002 历史决策记录,保留
learning-records/0001 保留(Lua 知识本身仍有价值)

8. 证据索引

证据 路径 说明
设计目标 80/20 与风险表 TestDocs/00-总纲/15-UnLuaTestDSL设计方案.md 当初的目标与”低风险”评估
API 表面与 stub 清单 TestDocs/00-总纲/16-LuaDSL-API参考.md 7 命名空间 + 6 个 stub/限制(§9)
混合桥接决策 docs/adr/0001-*docs/adr/0002-* 命名空间与反射桥接的历史决策
API 层透传实现 Content/Scripts/Tests/Core/TestContext.lua _Call 直接裸露、魔法数字、stub
脚本中的引擎层逻辑 Content/Scripts/Tests/Weapon/single_shot_spec.lua before_each 15+ 行引擎准备逻辑
PIE 从未启动的发现 .reasonix/handoffs/handoff-lyra-lua-test-dsl-pie-plan-d.md Automation “跑通”但 World==null
决策 TestDocs/00-总纲/00-测试总纲.md测试计划.md §8.4 CQTest + 手工双方式;弃用蓝图
C++ 路线可行性 Source/LyraGame/Tests/CQTest/EventSystem_Death.spec.cppdocs/adr/0003-* 复杂链路在 CQTest 下直接可行
插件启用状态 LyraStarterGame.uproject LuaTestFramework 未登记;LuaMachine 仍启用

文档结束。关联设计文档:15-UnLuaTestDSL设计方案16-LuaDSL-API参考13-测试辅助API设计方案


Lyra 测试总结报告

项目:LyraStarterGame(UE 5.8)| 封板口径:功能封板
前置材料:18-封板审查材料(证据索引)、19-CQTest代码审查报告(代码审查及 A1~A3 处置)


1. 结论

  • 自动化 CQTest 全量 96/96 全绿(7 套件; 冷启动回归复跑确认,含 DT-06/07 测试时序加固后验证;该时序问题非游戏 bug,见 §4)
  • 无 🔴 阻塞 Bug;13 份已知问题/建议随封板发布,不阻塞(🟡×11、🟢×1、玩法建议×1;同步)
  • 手工测试 Session 1~4 已完成
  • 允许功能封板;不推上线,成果本地 commit

2. 自动化结果(96/96)

套件 测试文件 用例 结果
武器与装备(EQ/WS/WF 数值) WeaponSystem_Equipment/Numerics/Firing.spec.cpp 21 21/21
控制系统(MV/AM/NV/FR) ControlSystem.spec.cpp 30 30/30
事件测试(DT-01/03/05/06/07) EventSystem_Death.spec.cpp 13 13/13
初始化链路(IN-01~05) InitChain/ShooterMapsInit.spec.cpp 5 5/5
相机数值(NV-01/02) CameraSystem_Numeric.spec.cpp 9 9/9
音频数值(NV-01/02 + UI) AudioSystem_Numeric.spec.cpp 8 8/8
散布系统(SP-01/02/05/06) SpreadSystem_Numeric/Functional.spec.cpp 10 10/10
合计 96 96/96

3. 手工与视觉验证

  • Session 1(武器动画 WA-0103、装备 EQ-0105):✅ 完成
  • Session 2(控制动画 AN-01、视角 LC-01、相机 CM-01/02/05):✅ 完成
  • Session 3(HUD HD-0105、队伍 TM-0105、音频 AU-01~04):✅ 完成
  • Session 4(探索性 EX-01)(地图互动效果:弹射器/传送门/手雷,90 分钟纯手工):✅ 完成,发现 EX-01-01~04 + 关联 AM-02-06/AM-03-06/AM-04-01/AU-05-01/CM-06
  • 蓝图功能测试 WF-01-0104 / WF-02-0103:✅ 完成(与宪章”CQTest 实现”不符 → 记录偏差,不补 CQTest)
  • Bug 报告 13 份(封板时 4 份 + EX-01 及后续新增 9 份):装备动画时序、WF-02-04 换弹互斥、AM-03-05 Dash 行为不一致、NV-03-02 动画蓝图除零(CQTest 发现)、AM-02-06、AM-03-06、AM-04-01、AU-05-01、CM-06、EX-01-01~04

4. 测试侧修复与硬化(本周期)

说明
音频音量 clamp(契约硬化) C++ setter 补 FMath::Clamp(0,1),使”负值→0”契约成立(非 Bug)
7 个测试侧失败修复 过期资产路径(WID_*/GA_Weapon_Fire)、double 属性读取、出生加载竞态、EQ-04 并入 EQ-03
19 号代码审查 A1~A3 NV_05_01 恒真等待→帧计数;EQ_02_05 断言与注释对齐;EQ_03 Sleep→轮询
DT-06/07 重生时序 flake(非游戏 bug) 封板回归中偶发:双重生 Pawn 抢占 PlayerState ASC + 死亡能力异步授予,若在初始化窗口内结算致死伤害会”血量归零但死亡状态不转换”。该窗口被重生无敌(Gameplay.DamageImmunity / GE_SpawnIn)覆盖,正常伤害被归零、玩家不可复现;仅测试用自杀伤害(DamageSelfDestruct,穿透无敌)可触发。测试侧加 3 帧稳定/一致性/死亡能力门禁后确定性通过(DT_07 连跑 3 次 + 套件内均绿),生产代码未改动(勘误)

5. 已知问题(随封板发布,无 🔴 阻塞)

完整清单见 Bug报告/00-Bug清单.md(汇总)。

编号 标题 等级 来源
NV-03-02 GravityScale=0 时 ABP_Mannequin_Base 除零 🟡 中(非阻塞 Warning) CQTest
AM-03-05 Dash 中操作行为不一致 🟡 严重 手工
WF-02-04 射击途中换弹被静默拒绝 🟡 严重(非阻塞) 手工/蓝图
装备动画 装备时武器先显示、后播放拿出动作 🟢 一般 手工
AM-03-06 无方向 Dash 仅在特定朝向触发 🟡 严重(非阻塞) 手工(EX-01 关联)
AM-02-06 滞空时按下蹲被忽略 🟡 中 手工
AM-04-01 近战中输入射击被忽略 🟡 中 手工
AU-05-01 贴脸时敌人枪声变沉闷 🟡 中 手工(EX-01 关联)
CM-06 瞄准时无方向 Dash 相机缩放与准心闪变 🟡 中 手工(EX-01 关联)
EX-01-01 Dash 中触发弹射器力度被削弱 🟡 中 探索性 EX-01
EX-01-02 方向弹射器按玩家朝向弹射 🟡 中 探索性 EX-01
EX-01-03 手雷可触发弹射装置(玩法建议) ⚪ 建议 探索性 EX-01
EX-01-04 传送门仅下半部分可触发传送 🟡 中 探索性 EX-01

6. 已知缺口 / 封板后 backlog

内容
DT-04 网络同步 PIE 双开 DeathState 复制(暂缓)
性能测试 PF-0112 × 3 地图 = 36 实例;本机 36h,建议骨架 + 冒烟管线
探索性测试 余 5 轮 × 90 分钟(EX-01 ✅ 已完成)
手工用例缺口 AM-02-05、AM-03(部分子状态)、NV-02、NV-04、DT-02-02、DT-05-04 未作为独立用例正式执行(AM-03 相关项已有 Bug 报告)
SP 剩余子状态 SP-02-02/03 弹孔实测、SP-03-01~06 乘数动态、SP-04 首发精度、SP-05-01/02 冷却延迟时序
前端页面(FE) FE-02~06 Spec/Gauntlet 覆盖不完整
环境 flake 防护 DT-06 移动输入解锁超时、L_Expanse socket_send_failure(冷启动复跑可过;DT-06 时序 flake 已加固,非游戏 bug,方法留档)

7. 交付物与记录

  • 测试代码:Source/LyraGame/Tests/CQTest/(12 份 spec + 辅助类)
  • 生产改动:Settings/LyraSettingsLocal.cpp(音量 clamp)
  • 文档:00-测试总纲、测试计划、18-封板审查材料、19-CQTest代码审查报告、本报告
  • Git:本地 master(不 push);关键提交 4670919(审查材料)、2f0b077(A1~A3 + DT-06/07 修复)

武器与装备系统 — 测试宪章

测试范围

# 测试项 说明
1 武器装备 切换动作 + 装备动作,共 5 个用例(含 5 个子状态)
2 武器功能 射击 + 换弹,共 2 个用例(含 8 个子状态)
3 武器数值 自动化测试,距离衰减/射速/弹药量/部位伤害,共 4 个用例(含 15 个子状态)
4 武器动画 装备/换弹/射击动画,共 3 个用例

测试计划

测试方式 适用用例 说明
🖐️ 手工测试 WA-01 ~ WA-03 需要玩家实际操作和视觉判断,如切换手感、动画播放是否正确、操作反馈是否正常
🤖 C++ CQTest EQ-01 ~ EQ-05, WF-01 ~ WF-02, WS-01 ~ WS-04 装备/射击/换弹/数值/边界值需要精确控制和遍历,用 CQTest 实现(回归决策,弃用蓝图功能测试)

详细分配

类别 手工测试 C++ CQTest
武器装备 (EQ) 5 个全部(含 5 个子状态)✅ 已完成
武器功能 (WF) 无(WF-01-03 由蓝图功能测试覆盖) WF-01-01、WF-01-02、WF-02(含8个子状态)
武器数值 (WS) 4 个全部(含 15 个子状态)
武器动画 (WA) 3 个全部

测试细则

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 武器装备(5 个用例,含 5 个子状态)w

切换动作
EQ-01 初始武器状态
编号 前置条件 测试步骤 预期结果 优先级 用例目的
EQ-01 角色首次进入游戏 检查角色当前持有的武器 角色持有默认初始武器 🔴 高 🔵 冒烟(并入进图冒烟)
EQ-02 武器切换
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
EQ-02-01 滚轮轮切武器 持有 2 把以上武器 使用鼠标滚轮向后滚动 切换到下一把武器,前一把收起 🔴 高 🟢 回归(切换链路)
EQ-02-02 数字键切换武器 持有 3 把武器,分别在槽位 1/2/3 依次按数字键 1 / 2 / 3 切换到对应槽位的武器 🔴 高 🟢 回归(切换链路)
EQ-02-03 切换到空槽位 槽位 3 为空 切换到槽位 3 无反应,保持当前武器 🟡 中 🟢 回归(边界:切空槽)
EQ-02-04 ⚠️ 重复快速切换 持有 2 把武器 快速反复按切换键多次 每次切换正常,不卡死,不崩溃 🟡 中 🟢 回归(并发/时序)
EQ-02-05 ⚠️ 切换武器冷却/延迟 持有 2 把武器 ① 切换到武器 B ② 立即尝试切回武器 A 切换操作有冷却时间,不能无延迟连续切换 🟡 中 🟢 回归(冷却时序)

⚠️ EQ-02-04 / EQ-02-05 风险:快速切换时动画未播完可能导致武器模型重叠或状态错乱,测试时关注是否有残留状态

装备动作
编号 用例名 前置条件 测试步骤 预期结果 优先级 用例目的
EQ-03 装备武器 角色靠近地上的武器 走到武器旁触发拾取 武器进入快捷栏,角色持有武器 🔴 高 🟢 回归(拾取主链路)
EQ-04 重复装备同一武器 快捷栏中已有该武器 再次拾取同一把武器 不能重复拾取(或替换为新的堆叠/弹药) 🟡 中 🚫 并入 EQ-03
EQ-05 快捷栏已满时拾取 3 个槽位全部有武器 尝试拾取地上第 4 把武器 拾取失败 或 替换当前武器 🟡 中 🚫 并入 EQ-03

代码实现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
环境: FMapTestSpawner(L_Expanse) → QuickBarComponent / EquipmentManagerComponent

EQ-01(初始武器状态):
[核心] Pawn生成后→EquipManager存在,已装备实例>0
数据源: EquipmentManager->GetEquipmentInstancesOfType()

EQ-02-01(滚轮轮切):
[核心] 持有≥2把→CycleActiveSlotForward()后槽位索引改变

EQ-02-02(数字键切换):
[核心] 3把分别放槽0/1/2→SetActiveSlotIndex(0/1/2)均能正确切换

EQ-02-03(切空槽):
[核心] 切到空槽位→GetActiveSlotItem()=nullptr(无反应,保持当前)

EQ-02-04(快速切换):
[风险] 20次循环SetActiveSlotIndex/CycleActiveSlotForward→不崩溃,状态可查

EQ-02-05(切换冷却):
[风险] 连续两次CycleActiveSlotForward→每次槽位都变化(可用时)

EQ-03(装备武器):
[核心] 生成B_WeaponSpawner(含WeaponPickupData_Rifle)→碰撞重叠触发拾取→快捷栏新增武器
数据源: B_WeaponSpawner蓝图, WeaponPickupData_Rifle DataAsset

EQ-04(重复装备):
[核心] 同一Instance添加两次→槽位计数不增加

EQ-05(槽满拾取):
[核心] 填满3槽→加第4个→槽位不增加(被拒绝)

2. 武器功能(2 个用例,含 8 个子状态)

此部分宪章决策为 C++ CQTest 实现(回归决策:弃用蓝图功能测试)。输入模拟用 InputTestActions(按下/按住/松开)。

📌 实际执行:WF 全组已用蓝图功能测试完成(ShooterTests/Content/Blueprint/weapon/:WF_01_01_SingleShot / WF_01_02_MultiShot / WF_01_03_ShotMove / WF-01-04 / WF-02-01 / WF-02-02 / WF-02-03 + 对应地图)。与宪章 CQTest 决策不符,先按蓝图执行(记录偏差,暂不迁移)。CQTest 侧仅 WeaponSystem_Firing.spec.cpp(WF-01-04 自动开火/死亡停止)已实现,与蓝图 WF-01-04 重复,作为补充保留。

射击测试
WF-01 射击
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
WF-01-01 单发武器射击 持有单发武器,有弹药 按射击键 1 次 射出 1 颗子弹,弹药 -1 🔴 高 🔵 冒烟 + 🟢 回归(弹药链路)
WF-01-02 连发武器射击 持有连发武器,有弹药 按住射击键不放 持续射出子弹,直到松手或弹药耗尽 🔴 高 🚫 并入 WS-03
WF-01-03 射击时角色移动 持有武器,有弹药 ① 跑动射击 ② 跳跃射击 移动中射击功能正常,散布比站立更大 🟡 中 🟢 回归(低优先;散布并入 SP-03)
WF-01-04 ⚠️ 射击途中被打断 正在执行射击动作 死亡 射击被打断 🟡 中 🚫 并入 DT-01-04
换弹测试
WF-02 换弹
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
WF-02-01 自动换弹 弹药为 0 按射击键 不能射击,自动触发换弹动作 🔴 高 🟢 回归(自动换弹路径)
WF-02-02 主动换弹 弹药未满 按换弹键 换弹动作播放,弹药恢复满 🔴 高 🟢 回归(换弹主路径)
WF-02-03 满弹匣时主动换弹 弹药全满 按换弹键 不应有弹药变化事件触发,弹药数量不变 🟡 中 🚫 并入 WF-02-02
WF-02-04 ⚠️ 射击途中换弹 正在执行射击动作 在换弹过程中按换弹键 射击被打断执行换弹 🔴 高 🖐️ 手工补验(决策:蓝图未覆盖;互斥时序,与 -05 合一例)
WF-02-05 ⚠️ 换弹途中射击 正在执行换弹动作 换弹过程中被射击指令 换弹被打断,弹药不恢复,换弹动画停止 🟡 中 🚫 并入 WF-02-04(手工补验)

⚠️ WF-02-01 / WF-02-04 / WF-02-05 风险:弹药耗尽+换弹+切换武器+换弹中断的并发操作可能造成弹药状态不一致。建议额外验证:换弹中切换武器 → 换弹是否取消、弹药是否回满

代码实现思路(文件:Source/LyraGame/Tests/CQTest/WeaponSystem_Fire.spec.cpp):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
环境: FMapTestSpawner(L_Expanse) → QuickBarComponent / 武器实例

WF-01-01(单发射击):
[核心] 装备武器→InputTestActions 按下 LMB→等待→弹药 -1
数据源: GetStatTagStackCount("Lyra.ShooterGame.Weapon.MagazineAmmo")

WF-01-02(连发射击):
[核心] InputTestActions 按住→持续出弹→松开→停止
[风险] 按住期间弹药持续递减,松开后不再递减

WF-02-01(自动换弹):
[核心] 弹药为 0→按射击键→不能射击,触发换弹
WF-02-02(主动换弹):
[核心] 弹药未满→按换弹键→弹药恢复满
WF-02-04/05(换弹与射击互斥):
[风险] 换弹中射击/射击中换弹→打断逻辑,弹药状态一致

🔧 实际落点:上述思路由蓝图功能测试落地(资产见 §2 顶部注记)。WF-02-04/05 互斥用例在资产中没有独立蓝图(WF-01-04.umap 内仅有 WF-02-04_C 残留引用),已决策:手工补验,执行清单见《测试计划》§5,结果记录到 Bug报告/ 或本表。


3. 武器数值(4 个用例,含 15 个子状态)— 自动化测试

测试方法:边界值分析。对每个参数定义有效区间 [a, b],从通用测试集 B={a-ε, a, a+ε, (a+b)/2, b-ε, b, b+ε} 中按实际风险选择性选取,重点覆盖除零/符号反转/数值溢出风险点。

WS-01 距离衰减参数验证(有效区间 [0, 1])
编号 细分状态 配置距离衰减系数 测试步骤 预期结果 风险关注 优先级 用例目的
WS-01-01 ⚠️ a-ε 低于最小 -0.01 在远近距离分别射击目标 衰减不应反向放大伤害 负系数→距离越远伤害越高 🟡 中 ⚪ 回归·单元
WS-01-02 a 最小值 0 在远近距离分别射击目标 远近距离伤害相同(无衰减) 🟡 中 ⚪ 回归·单元
WS-01-03 (a+b)/2 中间值 0.5 在远近距离分别射击目标 远距离伤害低于近距离 🔴 高 ⚪ 回归·单元(可选 1 个集成冒烟)
WS-01-04 b 最大值 1.0 在远近距离分别射击目标 远距离伤害大幅降低(最大衰减) 🔴 高 ⚪ 回归·单元(可选 1 个集成冒烟)
WS-02 射速参数验证(有效区间 [0, 1.0],项目 fireDelayTimeSecs=0.1)

射速由 GA_Weapon_Fire 的 fireDelayTimeSecs 控制(两次射击间延迟秒数),项目默认 0.1s(600 RPM)。
此外 autoRate=1 启用全自动连射。Heat 系统不限制射速,仅控制散布与过热(见事件测试 SP)。

编号 细分状态 配置 fireDelayTimeSecs 测试步骤 预期结果 风险关注 优先级 用例目的
WS-02-01 ⚠️ a-ε 低于最小 -0.01 按射击键 不崩溃,延迟被 clamp 到 0 负值导致 Timer 异常 🟡 中 ⚪ 回归·单元
WS-02-02 a 最小值 0 按射击键 无延迟,每帧触发射击 ⚠️ 帧率风险:delay=0 时射速=帧率,60FPS=60发/秒而120FPS=120发/秒,高帧率玩家获得双倍射速优势 🔴 高 ⚪ 回归·单元
WS-02-03 (a+b)/2 中间值 0.5 按射击键 每 0.5 秒射一发(120 RPM) 🔴 高 ⚪ 回归·单元
WS-02-04 b 最大值 1.0 按射击键 每 1.0 秒射一发(60 RPM) 🔴 高 ⚪ 回归·单元

⚠️ 帧率风险:UE 计时器基于帧 Tick,fireDelayTimeSecs=0 时射速完全等于帧率。在可变帧率环境下(20~144 FPS),相同配置下实际 RPM 可差 7 倍以上。建议锁定最小帧率(bUseFixedFrameRate)或将射速逻辑改为基于 DeltaTime 累积而非帧计数。

WS-03 弹药量参数验证(以步枪标配 30 发弹匣为例,有效区间 [0, 30])
编号 细分状态 配置弹匣容量 测试步骤 预期结果 风险关注 优先级 用例目的
WS-03-01 a 最小值 0 拾取武器后尝试射击 不能射击,触发换弹或空仓提示 除零:弹药为0导致UI/逻辑除零 🔴 高 ⚪ 回归·单元
WS-03-02 (a+b)/2 中间值 15 连续射击至弹匣打空 弹药 -1 递减,到 0 时触发空仓/换弹 🔴 高 ⚪ 回归·单元(递减断言并入 WF-01-01)
WS-03-03 b 最大值 30 连续射击至弹匣打空 弹药从满值递减至 0 🔴 高 ⚪ 回归·单元(递减断言并入 WF-01-01)
WS-04 部位伤害倍率验证(有效区间 [0, 10])
编号 细分状态 配置倍率 测试步骤 预期结果 风险关注 优先级 用例目的
WS-04-01 ⚠️ a-ε 低于最小 -1.0 分别射击敌人头部/身体/四肢 不崩溃,伤害不应为负(不倒扣血) 负倍率→击中回血 🔴 高 ⚪ 回归·单元
WS-04-02 a 最小值 0 分别射击敌人头部/身体/四肢 对应部位伤害为 0 🟡 中 ⚪ 回归·单元
WS-04-03 (a+b)/2 中间值 2.0 (头部) / 1.0 (身体) / 1.0 (四肢) 分别射击各部位 爆头伤害为身体2倍,四肢等于身体 🔴 高 🟢 回归(集成:伤害链路)
WS-04-04 b 最大值 10.0 射击敌人头部 爆头伤害为身体10倍 数值溢出→单发致死异常 🟡 中 ⚪ 回归·单元

代码实现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
环境: FMapTestSpawner(L_Expanse) / LoadObject 读 DataAsset
WS-02/03 为单元测试(直接加载资产读配置,无需PIE)
WS-01/04 为集成测试(PIE+装备武器后读运行时数据)

WS-01(距离衰减):
[核心] 近距/中距/远距各点→GetDistanceAttenuation()返回[0,1]倍率
[Min] 距离=0 → 衰减>0且≤1.0
[Risk] 负距离 → 衰减≤1.0(不放大的安全值)
数据源: ULyraRangedWeaponInstance.DistanceDamageFalloff(FRuntimeFloatCurve)

WS-02(射速 [0,1.0]):
[核心] LoadObject GA_Weapon_Fire→CDO→UProperty反射读fireDelayTimeSecs
[Risk] 负值 → 不崩溃,被clamp到≥0
[Max] 1.0 → 60RPM
数据源: GA_Weapon_Fire 蓝图变量 fireDelayTimeSecs

WS-03(弹药量 [0,30]):
[核心] LoadObject WID_Rifle→NewObject实例→SetItemDef触发StatTag初始化
→GetStatTagStackCount("MagazineAmmo") 读弹药量
[Min] 0 → 可加载,不崩溃
[Max] 30 → 步枪标配
数据源: UInventoryFragment_SetStats.InitialItemStats

WS-04(部位伤害 [0,10]):
[核心] 读取MaterialDamageMultiplier Map→所有倍率≥0且≤10.0
[Risk] 负倍率→不倒扣血(≥0)
数据源: ULyraRangedWeaponInstance.MaterialDamageMultiplier(TMap<FGameplayTag,float>)

4. 武器动画(3 个用例)

编号 用例名 前置条件 测试步骤 预期结果 优先级 用例目的
WA-01 装备武器动画 当前为默认武器 拾取或切换到武器 切换/装备动画播放,角色正确持枪 🟡 中 🟠 验收(视觉,手工)
WA-02 ⚠️ 换弹动画 弹药未满 触发换弹 换弹动画正确播放,与武器类型匹配 🟡 中 🟠 验收(视觉,手工)
WA-03 射击动画 持有武器 射击 枪口火焰和后坐力动画正常播放 🟡 中 🟠 验收(视觉,手工)

⚠️ WA-02 风险:换弹动画与实际弹药恢复可能不同步(动画播完但弹药未恢复,或弹药恢复但动画未播完),测试时关注操作时机的准确性


控制系统 — 测试宪章

测试范围

# 测试项 说明
1 基础移动 🤖 方向移动 + 停止 + 转向,共 3 个用例(含 7 个子状态)
2 高阶移动 🤖/🖐️ 跳跃 + 蹲下 CQTest;Dash + 矮通道 手工,共 3 个用例(含 16 个子状态)
3 视角控制 🖐️ 手工 — 鼠标/手柄视角、灵敏度,共 1 个用例(含 4 个子状态)
4 动画测试 🖐️ 手工 — 移动/跳跃/蹲下/Dash 动画表现,共 1 个用例(含 5 个子状态)
5 数值测试 🤖/🖐️ 边界值分析:CQTest 覆盖跳跃速度/重力/MaxWalkSpeed;手工覆盖 Dash 冷却/灵敏度

测试计划

测试方式 适用用例 说明
🖐️ 手工测试 LC-01, AN-01, AM-02-05, AM-03, NV-02, NV-04 视角手感、动画表现、Dash 手感/冷却、灵敏度需玩家判断(Dash 为蓝图 GA,自动化成本高)
🤖 C++ CQTest MV-01 ~ MV-03, AM-01, AM-02(-01~-04), NV-01, NV-03, NV-05, FR-01 ~ FR-02 输入模拟 + 组件精确验证 + 数值边界(回归决策,弃用蓝图功能测试)

详细分配

类别 手工测试 C++ CQTest
基础移动 (MV) 3 个全部(含 7 个子状态)
高阶移动 (AM) AM-02-05(矮通道)、AM-03 全部(6 个子状态) AM-01 全部(含 5 个子状态)、AM-02 的 -01~-04
视角控制 (LC) 1 个全部(含 4 个子状态)
动画测试 (AN) 1 个全部(含 5 个子状态)
数值测试 (NV) NV-02(Dash 冷却)、NV-04(灵敏度) NV-01、NV-03、NV-05(MaxWalkSpeed 回填)
死亡冻结 (FR) FR-01、FR-02(自动化补充,见 §2 注记)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 基础移动(3 个用例,含 7 个子状态)

MV-01 方向移动
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
MV-01-01 向前移动 角色正常站立 按 W / 上方向键 角色向前移动 🔴 高 🔵 冒烟 + 🟢 回归(四方向循环实现)
MV-01-02 向后移动 角色正常站立 按 S / 下方向键 角色向后移动 🔴 高 🔵 冒烟 + 🟢 回归(四方向循环实现)
MV-01-03 向左平移 角色正常站立 按 A / 左方向键 角色向左平移 🔴 高 🔵 冒烟 + 🟢 回归(四方向循环实现)
MV-01-04 向右平移 角色正常站立 按 D / 右方向键 角色向右平移 🔴 高 🔵 冒烟 + 🟢 回归(四方向循环实现)
MV-01-05 斜向移动 角色正常站立 同时按住 W + D 角色向右前方移动 🟡 中 🟢 回归(斜向)
MV-02 ⚠️ 停止移动
编号 前置条件 测试步骤 预期结果 优先级 用例目的
MV-02 角色正在移动 松开所有方向键 角色停止移动 🟡 中 🚫 并入 MV-01
MV-03 移动中转向
编号 前置条件 测试步骤 预期结果 优先级 用例目的
MV-03 角色正在移动 移动时旋转视角 角色跟随视角方向转向 🟡 中 🟢 回归(转向链路)

⚠️ MV-02 风险:快速松开方向键后立即反方向移动,可能造成停止距离异常或角色滑行

2. 高阶移动(3 个用例,含 16 个子状态)

跳跃、蹲下、Dash 作为统一的”高阶移动方式”管理,死亡时应全部冻结。

🤖 死亡冻结由 CQTest 补充覆盖:FR-01(死亡后无法移动)、FR-02(复活后恢复移动),对应 Gameplay.MovementStopped Tag 的添加/移除。

AM-01 跳跃
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AM-01-01 平地跳跃 角色站在地面上 按跳跃键 角色跳起,落地后动作结束 🔴 高 🔵 冒烟 + 🟢 回归(承载 NV-01 跳高断言)
AM-01-02 移动中跳跃 角色正在向前移动 移动中按跳跃键 角色在移动中跳起,保持水平速度 🔴 高 🟢 回归
AM-01-03 蹲跳 角色蹲下中 按跳跃键 角色从蹲伏状态跳起(Lyra 允许) 🟡 中 🚫 删除(与 AM-02-04 重复)
AM-01-04 ⚠️ 连续跳跃 角色落地后 落地瞬间再按跳跃键 可以连续跳跃 🟡 中 🟢 回归(落地时机边界)
AM-01-05 空中移动 角色在空中 跳跃后在空中使用方向键 空中可以控制移动方向 🟡 中 🟢 回归(空中控制)

⚠️ AM-01-04 风险:连续跳跃的时机判定可能不准,落地瞬间按跳跃可能不触发

AM-02 蹲下
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AM-02-01 蹲下 角色正常站立 按蹲下键 角色进入蹲伏状态,碰撞体高度降低 🔴 高 🟢 回归(碰撞高度)
AM-02-02 站起 角色蹲伏中 按蹲下键 角色站起,恢复正常碰撞高度 🔴 高 🟢 回归(碰撞高度)
AM-02-03 蹲下行进 角色蹲伏中 按方向键移动 角色以蹲伏姿态移动(速度受限) 🟡 中 🟢 回归(速度受限)
AM-02-04 ⚠️ 蹲跳 角色蹲伏中 按跳跃键 角色从蹲伏状态跳起 🟡 中 🟢 回归(吸收 AM-01-03)
AM-02-05 🖐️ 蹲下穿过矮通道 前方有矮通道 在矮通道前蹲下并前进 角色通过矮通道 🟡 中 🟠 验收(几何/视觉,手工)

⚠️ AM-02-04 风险:Lyra 允许蹲跳但站起时可能卡在矮空间中,碰撞检测可能异常

AM-03 Dash 🖐️ 手工测试(决策:Dash 为蓝图 GA,自动化成本高,转手工)
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AM-03-01 向前 Dash 角色正常站立 按 Dash 键 角色朝面向方向快速冲刺一段距离 🔴 高 🟠 验收(手感,已决策手工)
AM-03-02 方向 Dash 角色正在移动 移动中按 Dash 键 角色朝移动方向冲刺 🔴 高 🟠 验收(手感,已决策手工)
AM-03-03 ⚠️ Dash 冷却 使用 Dash 后 立即再次按 Dash 键 Dash 不能使用,在冷却中 🔴 高 🟠 验收(冷却手感,已决策手工)
AM-03-04 冷却结束可再次使用 Dash 冷却中 等待冷却时间结束 Dash 可在冷却结束后再次使用 🟡 中 🟠 验收(已决策手工)
AM-03-05 ⚠️ Dash 中操作 Dash 过程中 Dash 过程中按跳跃/方向键/近战/手雷/蹲下/射击 Dash 过程中不能进行其他操作(或可提前取消) 🟡 中 🟠 验收(已决策手工)
AM-03-06 无移动时 Dash 角色静止 不按方向键,按 Dash 键 角色朝当前面向方向 Dash 🟡 中 🟠 验收(已决策手工)

3. 视角控制 🖐️ 手工测试(1 个用例,含 4 个子状态)

LC-01 视角控制
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
LC-01-01 鼠标水平视角 角色正常 左右移动鼠标 角色水平旋转视角 🔴 高 🟠 验收(手感,手工)
LC-01-02 鼠标垂直视角 角色正常 上下移动鼠标 角色垂直调整视角 🔴 高 🟠 验收(手感,手工)
LC-01-03 手柄摇杆视角 使用手柄(xbox) 推动右摇杆 角色视角按照摇杆方向旋转 🟡 中 🟠 验收(手感,手工)
LC-01-05 ⚠️ 灵敏度调节 设置中调整视角灵敏度 以相同幅度移动鼠标 高灵敏度时视角转动幅度更大 🟡 中 🟠 验收(灵敏度手感,手工)

📌 LC-01-04 视角反转已移除:当前游戏版本未提供”反转 Y 轴”设置,无测试入口。若后续版本加入该设置,需恢复此用例。
⚠️ LC-05 风险:灵敏度为 0 或负值时视角无法转动,灵敏度极高时可能导致视角飞转
⚠️ LC-03 / LC-02 风险(死区 & 视角上下限):手柄摇杆死区设置过大会导致轻微推动无响应,死区过小会导致视角漂移。需关注不同手柄(Xbox/PS/第三方)的死区表现差异
⚠️ LC-02 风险(视角顶部/底部映射):垂直视角到达天花板(仰视极限)或地板(俯视极限)时,继续推动可能发生视角翻转或卡死,需关注上下视角限幅是否正确

4. 动画测试 🖐️ 手工测试(1 个用例,含 5 个子状态)

AN-01 移动动画验证
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AN-01-01 移动动画 角色正常 前后左右移动 移动动画自然,与速度匹配 🟡 中 🟠 验收(视觉,手工)
AN-01-02 跳跃动画 角色在地面 按跳跃键 跳跃起跳/空中/落地动画完整播放 🟡 中 🟠 验收(视觉,手工)
AN-01-03 ⚠️ 蹲下动画 角色正常 蹲下/站起 蹲下过渡动画流畅 🟡 中 🟠 验收(视觉,手工)
AN-01-04 Dash 动画 角色正常 按 Dash 键 Dash 动作动画与冲刺效果匹配 🟡 中 🟠 验收(视觉,手工)
AN-01-05 移动状态切换动画 角色在移动中 走路→跳跃→蹲下→dash循环切换 动画过渡自然,不卡顿/瞬移 🟡 中 🟠 验收(视觉,手工)

⚠️ AN-01-03 风险:快速蹲站可能导致动画与碰撞体状态不同步,角色可能卡在地形中

5. 数值测试 🤖/🖐️ 边界值分析(5 个参数组,含 19 个子状态)

测试方法:边界值分析。对每个参数定义有效区间 [a, b],从通用边界测试集
B = { a-ε, a, a+ε, (a+b)/2, b-ε, b, b+ε } 中按实际风险选择性选取,
重点覆盖除零错误、数值溢出、符号反转等真实风险点。
注:部分参数(如 Dash 冷却)在蓝图中配置,需参照对应 GameplayEffect 资产确认项目实际值。

NV-01 跳跃速度参数验证(有效区间 [0, 420],项目默认值)
编号 细分状态 配置 JumpZVelocity 测试步骤 预期结果 风险关注 优先级 用例目的
NV-01-01 ⚠️ a-ε 低于最小 -0.01 按跳跃键 不崩溃,角色不应向下弹射 负值未过滤→反向跳跃 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-01-02 a 最小值 0 按跳跃键 角色跳不起来(或轻微离地) 除零:若用作除数则崩溃 🔴 高 ⚪ 回归·单元(clamp 校验)
NV-01-03 (a+b)/2 中间值 210 按跳跃键 角色跳到正常高度(≈ 默认值 420 的一半) 🔴 高 🚫 并入 AM-01(跳高实测)
NV-01-04 b 最大值 420 按跳跃键 角色跳到最大设定高度(项目默认值) 🔴 高 🚫 并入 AM-01(跳高实测)
NV-02 Dash 冷却参数验证 🖐️ 手工测试(有效区间 [0, 30],项目值见 GE_HeroDash_Cooldown)
编号 细分状态 配置冷却时间 测试步骤 预期结果 风险关注 优先级 用例目的
NV-02-01 a 最小值 0 连续使用 Dash Dash 可无冷却连续使用 除零:冷却→0 可能除零崩溃 🔴 高 🟠 验收(已决策手工)
NV-02-02 (a+b)/2 中间值 15 使用 Dash 后等待冷却 等待约 15 秒后可再次使用 🔴 高 🟠 验收(已决策手工)
NV-02-03 b 最大值 30 使用 Dash 后等待冷却 等待 30 秒后可再次使用 🔴 高 🟠 验收(已决策手工)
NV-03 重力参数验证(有效区间 [0, 10])
编号 细分状态 配置 GravityScale 测试步骤 预期结果 风险关注 优先级 用例目的
NV-03-01 ⚠️ a-ε 低于最小 -0.01 跳跃后观察 不崩溃,角色可能向上飘 负重力→角色升空 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-03-02 a 最小值 0 跳跃后观察 角色浮空不下落 除零:物理引擎除零崩溃 🔴 高 ⚪ 回归·单元(clamp 校验)
NV-03-03 (a+b)/2 中间值 5 跳跃后观察 角色快速下落 🔴 高 🚫 并入 AM-01(下落实测)
NV-03-04 b 最大值 10 跳跃后观察 角色以最大重力下落 🔴 高 🚫 并入 AM-01(下落实测)
NV-04 视角灵敏度参数验证 🖐️ 手工测试(有效区间 [0.01, 5.0])
编号 细分状态 配置灵敏度 测试步骤 预期结果 风险关注 优先级 用例目的
NV-04-01 ⚠️ a-ε 低于最小 0.0 移动鼠标 不崩溃,视角不能转动 零灵敏度→视角卡死 🟡 中 🟠 验收(手感,手工)
NV-04-02 a 最小值 0.01 移动鼠标 视角极缓慢旋转 🟡 中 🟠 验收(手感,手工)
NV-04-03 (a+b)/2 中间值 2.505 移动鼠标 视角正常跟随鼠标旋转 🔴 高 🟠 验收(手感,手工)
NV-04-04 b 最大值 5.0 移动鼠标 视角以最大速度旋转 🔴 高 🟠 验收(手感,手工)
NV-05 MaxWalkSpeed 参数验证 🤖 CQTest(有效区间 [0, 600],项目默认值)
编号 细分状态 配置 MaxWalkSpeed 测试步骤 预期结果 风险关注 优先级 用例目的
NV-05-01 ⚠️ a-ε 低于最小 -1.0 按方向键 不崩溃,角色不应反向奔跑 负速度→方向反转 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-05-02 a 最小值 0 按方向键 角色原地不动 除零:物理除零崩溃 🔴 高 ⚪ 回归·单元(clamp 校验)
NV-05-03 (a+b)/2 中间值 300 按方向键 角色以半速移动 🟡 中 🚫 并入 MV-01(半速实测)
NV-05-04 b 最大值 600 按方向键 角色全速移动(项目默认值) 🔴 高 🚫 并入 MV-01(全速实测)

代码实现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
环境: FMapTestSpawner(L_Expanse) → CharacterMovement / ULyraSettingsShared

NV-01(跳跃速度 [0,420]):
[Risk] a-ε=-0.01 → 负值不应向下弹射, 引擎/项目应 clamp
[Min] a=0 JumpZVelocity=0 → 角色跳不起(或轻微离地),不崩溃
[Mid] (a+b)/2=210 → 跳至约默认高度的一半
[Max] b=420 → 跳至最大设定高度(项目默认值)
数据源: CharacterMovement->JumpZVelocity(直接写属性)

NV-03(重力 [0,10]):
[Risk] a-ε=-0.01 → 可能向上飘(负重力升空),不崩溃
[Min] a=0 → 角色浮空不下落,物理不除零
[Mid] (a+b)/2=5 → 明显加速下落
[Max] b=10 → 最大重力下落
数据源: CharacterMovement->GravityScale(直接写,默认1.0)

NV-05(MaxWalkSpeed [0,600]):
[Risk] a-ε=-1.0 → 不应反向奔跑,不崩溃
[Min] a=0 → 角色原地不动
[Mid] (a+b)/2=300 → 角色以半速移动
[Max] b=600 → 角色全速移动(项目默认值)
数据源: CharacterMovement->MaxWalkSpeed(直接写属性)

相机系统 — 测试宪章

测试范围

# 测试项 说明
1 第三人称跟随 🖐️ 相机跟随 + 旋转,共 1 个用例(含 4 个子状态)
2 碰撞避免 🖐️ 贴墙推近 + 恢复 + 侧向 + 墙角,共 1 个用例(含 4 个子状态)
3 FOV 与变焦 🚫 已删(:游戏无可调 FOV)
4 模式切换与过渡 🚫 已删(:游戏无相机切换)
5 蹲下偏移 🖐️ 蹲下/站起相机高度,共 1 个用例(含 2 个子状态)
6 数值测试 🤖 FOV / 俯仰限幅 边界值,共 2 个用例(含 9 个子状态)

测试计划

测试方式 适用用例 说明
🖐️ 手工测试 CM-01/02、CM-05 相机的平滑度、碰撞感受需要视觉判断
🤖 C++ CQTest NV-01 ~ NV-02 FOV/俯仰限幅的边界值

详细分配

类别 手工测试 C++ CQTest
第三人称跟随 (CM-01) 1 个全部(含 4 个子状态)
碰撞避免 (CM-02) 1 个全部(含 4 个子状态)
FOV 与变焦 (CM-03) 🚫 已删
模式切换 (CM-04) 🚫 已删
蹲下偏移 (CM-05) 1 个全部(含 2 个子状态)
数值测试 (NV) 2 个全部(含 9 个子状态)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 第三人称跟随(1 个用例,含 4 个子状态)

CM-01 第三人称跟随
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
CM-01-01 相机跟随角色移动 角色在场景中 前后左右移动 相机平滑跟随,不抖动、不滞后 🔴 高 🟠 验收(视觉/手感,手工)
CM-01-02 相机水平旋转 角色正常 左右移动鼠标 相机绕角色水平旋转,角色朝向跟随 🔴 高 🟠 验收(视觉/手感,手工)
CM-01-03 相机垂直旋转 角色正常 上下移动鼠标 相机上下倾斜,俯仰视角变化 🔴 高 🟠 验收(视觉/手感,手工)
CM-01-04 相机环绕角色 角色静止 以角色为中心旋转视角 360° 相机平滑环绕,角色始终在画面中心 🟡 中 🚫 删除(与 CM-01-02 重复)
CM-01-05 角色旋转时相机表现 角色正在转向 快速转动角色方向 相机平滑跟随,不出现瞬间跳转 🟡 中 🟠 验收(视觉/手感,手工)

🚫 删除说明:CM-01-04(相机环绕角色)与 CM-01-02(相机水平旋转)动作重叠——360° 环绕即水平旋转一周,不再单独立项。

2. 碰撞避免(1 个用例,含 4 个子状态)

CM-02 碰撞避免
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
CM-02-01 贴墙时相机推近 角色背对墙壁 向后退到墙边 相机逐渐推近角色,不穿模 🔴 高 🟠 验收(视觉/手感,手工)
CM-02-02 离开墙壁后恢复 相机被墙壁推近后 向前离开墙壁 相机平滑恢复至正常位置 🔴 高 🟠 验收(视觉/手感,手工)
CM-02-03 侧向碰撞 角色靠近墙壁的右侧 向左移动使相机扫过墙壁 相机推近避免穿模,平滑处理 🟡 中 🟠 验收(视觉/手感,手工)
CM-02-04 墙角场景 角色站在墙角 围绕墙角移动 相机始终不穿墙 🟡 中 🟠 验收(视觉/手感,手工)

3. FOV 与变焦 — 🚫 已删

🚫 删除说明:游戏不存在可调 FOV(相机模式固定默认 80°,无瞄准变焦/变焦恢复实现),CM-03-01~03 全部移除。

4. 模式切换与过渡 — 🚫 已删

🚫 删除说明:游戏不存在相机模式切换(能力相机/默认相机无接入,仅 Cheat 命令可切),CM-04-01~03 全部移除。

5. 蹲下偏移(1 个用例,含 2 个子状态)

CM-05 蹲下偏移
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
CM-05-01 蹲下时相机降低 角色正常 按蹲下键 相机高度平滑降低至蹲姿高度 🟡 中 🟠 验收(视觉/手感,手工)
CM-05-02 站起时相机恢复 角色蹲下 按蹲下键站起 相机高度平滑恢复至站姿高度 🟡 中 🟠 验收(视觉/手感,手工)

6. 数值测试 🤖 自动化测试(2 个用例,含 9 个子状态)

测试方法:边界值分析。对每个参数定义有效区间 [a, b],按实际风险从 B={a-ε, a, (a+b)/2, b} 中选取测试点,重点覆盖除零/符号反转/数值溢出风险。

NV-01 FOV 参数验证(有效区间 [5.0, 170.0],项目 clamp 值)
编号 细分状态 配置 FOV 测试步骤 预期结果 风险关注 优先级 用例目的
NV-01-01 ⚠️ a-ε 低于最小 4.9 进入游戏 不崩溃,FOV clamp 到 5.0 或最小值 除零:FOV=0 导致投影矩阵除零崩溃 🔴 高 ⚪ 回归·单元(clamp 校验)
NV-01-02 a 最小值 5.0 进入游戏 FOV 显示为最小有效值(5°) 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-01-03 (a+b)/2 中间值 87.5 进入游戏 FOV 显示正常(项目默认 80° 附近) 🔴 高 ⚪ 回归·单元(数据校验)
NV-01-04 b 最大值 170.0 进入游戏 FOV 显示为最大有效值(170°) 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-02 俯仰限幅参数验证(有效区间 [-89.0, 89.0],项目定义值)
编号 细分状态 配置俯仰限幅 测试步骤 预期结果 风险关注 优先级 用例目的
NV-02-01 ⚠️ a-ε 低于下限 -89.1 上下移动视角 不崩溃,视角被 clamp 到 -89.0 越界→视角锁定/抖动 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-02-02 a 下限 -89.0 上下移动视角 视角最大俯角为 -89°(项目默认) 🟡 中 ⚪ 回归·单元(数据校验)
NV-02-03 (a+b)/2 中间值 0 上下移动视角 视角可正常俯仰 🔴 高 ⚪ 回归·单元(数据校验)
NV-02-04 b 上限 89.0 上下移动视角 视角最大仰角为 89°(项目默认) 🟡 中 ⚪ 回归·单元(数据校验)
NV-02-05 ⚠️ b+ε 超出上限 89.1 上下移动视角 不崩溃,视角被 clamp 到 89.0 越界→视角翻转 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-03 混合时间参数验证 — 🚫 已删

🚫 删除说明:无相机模式切换即无 BlendTime 过渡,NV-03-01~03 全部移除。

代码实现思路

1
2
3
4
5
6
7
8
9
10
11
环境: FMapTestSpawner(L_Expanse) → 获取 PlayerCameraManager / UCameraComponent

NV-01(FOV [5,170]):
[Min] a=5 默认值 80°, 低于5被clamp, UCameraComponent.FieldOfView 可读写
[Max] b=170 >170被clamp, 不泄漏异常FOV
[风险] 负值/零值不崩溃, 引擎侧 ClampMin=5 生效

NV-02(俯仰限制 [-89,89]):
[边界] ±89.1 → clamp 到 ±89.0, 不越界
[区间] 0 时俯仰正常(仅限制反转, 非锁定)
[数据] APlayerCameraManager.PitchLimit/Min/Max / LyraCameraMode 配置

HUD — 测试宪章

测试范围

# 测试项 说明
1 准星 🖐️ 不同武器的准星样式、散布缩放反馈,共 1 个用例(含 5 个子状态)
2 命中标记 🖐️ 命中敌人时的视觉确认标记 + 击杀特殊标记,共 1 个用例(含 5 个子状态)
3 伤害数字 🖐️ 命中时浮出的伤害数值,含暴击区分,共 1 个用例(含 4 个子状态)
4 HUD 布局 🖐️ ESC 菜单、手柄断开提示等层级管理,共 1 个用例(含 3 个子状态)
5 游戏内信息 🖐️ 血条、当前武器信息、比分板,共 1 个用例(含 5 个子状态)

HUD 全部为手工测试,所有元素需要视觉判断。


测试计划

测试方式 适用用例
🖐️ 手工测试 全部 22 个用例

详细分配

类别 手工测试 C++ CQTest
准星 (HD-01) 1 个全部(含 5 个子状态)
命中标记 (HD-02) 1 个全部(含 5 个子状态)
伤害数字 (HD-03) 1 个全部(含 4 个子状态)
HUD 布局 (HD-04) 1 个全部(含 3 个子状态)
游戏内信息 (HD-05) 1 个全部(含 5 个子状态)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 准星(1 个用例,含 5 个子状态)

HD-01 准星显示
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
HD-01-01 准星显示 持有武器 进入游戏 屏幕中央显示当前武器的准星 🔴 高 🟠 验收(视觉,手工)
HD-01-02 不同武器不同准星 持有武器 A 和武器 B 切换武器 准星样式随武器切换而改变 🟡 中 🟠 验收(视觉,手工)
HD-01-03 散布反馈 — 连续射击 持有武器 连续射击 准星随散布增大而放大 🟡 中 🟠 验收(视觉,手工)
HD-01-04 散布反馈 — 停止后恢复 准星已放大 停止射击等待数秒 准星随散布恢复而缩小 🟡 中 🟠 验收(视觉,手工)
HD-01-05 移动时准星变化 持有武器 跑动中观察准星 跑动时准星比静止时更大 🟡 中 🟠 验收(视觉,手工)
HD-01-06 空手时无准星 未持有武器 切换到空手 屏幕中央无准星显示 🟡 中 🚫 删除(游戏无空手状态)

🚫 删除说明:游戏无空手状态(EQ-02-03 切空槽为“无反应,保持当前武器”),HD-01-06 前置条件不成立。

2. 命中标记(1 个用例,含 5 个子状态)

HD-02 命中标记
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
HD-02-01 命中敌人显示标记 持有武器 射击命中敌人 屏幕中央/命中位置出现命中标记 🔴 高 🟠 验收(视觉,手工)
HD-02-02 命中标记消退 命中标记已显示 等待数秒 命中标记逐渐淡出消失 🟡 中 🟠 验收(视觉,手工)
HD-02-03 连续命中多个标记 持有连发武器 连续命中敌人多次 每次命中都有对应的命中标记 🟡 中 🟠 验收(视觉,手工)
HD-02-04 未命中时无标记 持有武器 射击未命中目标 无命中标记显示 🔴 高 🟠 验收(视觉,手工)
HD-02-05 击杀特殊标记 持有武器,敌人血量可被一击击杀 射击击杀敌人 击杀时显示特殊标记,样式与普通命中标记明显区分 🔴 高 🟠 验收(视觉,手工)

3. 伤害数字(1 个用例,含 4 个子状态)

HD-03 伤害数字
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
HD-03-01 命中显示伤害数字 持有武器 射击命中敌人 命中位置浮出伤害数字 🟡 中 🟠 验收(视觉,手工)
HD-03-02 伤害数值正确 持有武器 射击命中敌人 伤害数字显示的值与实际伤害一致 🟡 中 🟠 验收(视觉,手工)
HD-03-03 暴击区分 武器可造成暴击 触发暴击 暴击伤害数字样式与普通伤害不同 🟡 中 🟠 验收(视觉,手工)
HD-03-04 伤害数字消退 伤害数字已显示 等待 伤害数字逐渐消失 🟡 中 🟠 验收(视觉,手工)

4. HUD 布局(1 个用例,含 3 个子状态)

HD-04 HUD 布局
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
HD-04-01 ESC 菜单 游戏中 按 ESC 键 弹出暂停菜单 🔴 高 🟠 验收(视觉,手工)
HD-04-02 手柄断开提示 使用手柄 断开手柄连接 屏幕显示控制器断开连接提示 🟡 中 🟠 验收(视觉,手工)
HD-04-03 HUD 元素布局 进入游戏 检查各 UI 元素位置 准星、血量、弹药等元素在正确位置显示 🟡 中 🟠 验收(视觉,手工)

5. 游戏内信息(1 个用例,含 5 个子状态)

HD-05 游戏内信息
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
HD-05-01 血条显示 角色正常 进入游戏 屏幕上显示血量条/数值 🔴 高 🟠 验收(视觉,手工)
HD-05-02 受伤后血条变化 角色有血量 受到伤害 血条随着伤害实时减少 🔴 高 🟠 验收(视觉,手工)
HD-05-03 治疗回血 角色血量不满 接受治疗 血条随着治疗增加 🟡 中 🟠 验收(视觉,手工)
HD-05-04 当前武器信息 持有武器 检查屏幕 显示当前武器的名称/图标/弹药量 🔴 高 🟠 验收(视觉,手工)
HD-05-05 弹药量更新 持有武器 射击消耗弹药 弹药数字随射击减少 🔴 高 🟠 验收(视觉,手工)

比分板测试已移至队伍系统


前端页面 — 测试宪章

测试范围

# 测试项 说明
1 加载动画 🚫 已删(:与 FE-05 加载画面重复)
2 主菜单 导航、游戏模式选择、进入游戏,共 1 个用例(含 5 个子状态)
3 设置界面 画面/音频/控制等设置,共 1 个用例(含 4 个子状态)
4 确认弹窗 退出确认、操作确认等模态弹窗,共 1 个用例(含 3 个子状态)
5 加载画面 菜单间切换/进入游戏时的加载界面,共 1 个用例(含 3 个子状态)
6 ESC 暂停菜单 游戏中的暂停菜单、回到大厅、退出游戏,共 1 个用例(含 3 个子状态)

前端页面全部可自动化测试(UI 交互流程可通过脚本验证)。


测试计划

测试方式 适用用例
🤖 Spec + Gauntlet 全部 5 个用例(含 18 个子状态)

详细分配

类别 手工测试 Spec + Gauntlet
全部 (FE-02~06) 5 个全部(含 18 个子状态)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 加载动画 — 🚫 已删

🚫 删除说明:FE-01(加载动画)与 FE-05(加载画面)覆盖同一加载链路,已并入 FE-05,不再单独立项。

2. 主菜单(1 个用例,含 5 个子状态)

FE-02 主菜单
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
FE-02-01 主菜单显示 进入主菜单 等待菜单加载 显示游戏模式选择、设置、退出等选项 🔴 高 🔵 冒烟(启动链路)
FE-02-02 选择游戏模式 主菜单中 点击一个游戏模式 进入该模式加载流程 🔴 高 🔵 冒烟 + 🟢 回归(进入对局路径)
FE-02-03 导航到设置 主菜单中 点击设置按钮 切换到设置界面 🔴 高 🟢 回归(导航流程)
FE-02-04 返回主菜单 在子页面中 按返回键 回到主菜单 🟡 中 🟢 回归(导航流程)
FE-02-05 主菜单背景 主菜单中 观察背景 背景场景正常显示(大厅/角色展示) 🟡 中 🟢 回归(资源存在性)

代码实现思路

1
2
3
4
5
6
FE-02(主菜单):
[核心] 各子按钮(模式选择/设置/退出)IsVisible=true
[核心] 点击模式→进入匹配/加载流程
[核心] 点击设置→切换到设置界面
[核心] 子页面返回→主菜单重新可见
[扩展] 背景Brush资源非空

3. 设置界面(1 个用例,含 4 个子状态)

FE-03 设置界面
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
FE-03-01 设置页面布局 进入设置 浏览各设置标签页 画面/音频/控制等标签页正常切换 🟡 中 🟢 回归(设置流程)
FE-03-02 修改设置 设置界面中 修改一个设置项 设置值即时更新或显示已更改标记 🟡 中 🟢 回归(设置流程)
FE-03-03 保存设置 已修改设置 应用/保存 设置保存,下次启动时仍然有效 🔴 高 🟢 回归(设置持久化)
FE-03-04 取消设置 已修改设置 不保存并退出 设置恢复到修改前的状态 🟡 中 🟢 回归(设置流程)

代码实现思路

1
2
3
4
FE-03(设置界面):
[核心] Tab(画面/音频/控制)切换→内容Panel可见性联动
[核心] Slider.SetValue→值即时更新,保存后重启保持
[核心] 取消→值恢复修改前

4. 确认弹窗(1 个用例,含 3 个子状态)

FE-04 确认弹窗
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
FE-04-01 退出游戏确认 主菜单中 选择退出游戏 弹出确认弹窗,有确认/取消选项 🔴 高 🟢 回归(弹窗流程)
FE-04-02 确认弹窗 — 确认 确认弹窗已显示 点击确认 执行对应操作(退出游戏) 🔴 高 🟢 回归(弹窗流程)
FE-04-03 确认弹窗 — 取消 确认弹窗已显示 点击取消 / 按 ESC 弹窗关闭,不执行操作 🟡 中 🟢 回归(弹窗流程)

代码实现思路

1
2
3
4
FE-04(确认弹窗):
[核心] 退出→ConfirmDialog出现,含确认/取消按钮且可见
[核心] 确认→执行操作(退出)
[核心] 取消/ESC→弹窗关闭,主菜单仍在

5. 加载画面(1 个用例,含 3 个子状态)

FE-05 加载画面
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
FE-05-01 进入游戏时加载画面 选择游戏模式后 等待加载 显示加载画面(有提示/进度) 🔴 高 🔵 冒烟 + 🟢 回归(加载链路)
FE-05-02 加载完成后进入游戏 加载画面中 加载完成 加载画面消失,进入游戏 🔴 高 🔵 冒烟 + 🟢 回归(加载链路)
FE-05-03 加载画面期间输入 加载画面中 按任意键 加载不受影响,继续执行 🟡 中 🟢 回归(加载中输入)

代码实现思路

1
2
3
4
FE-05(加载画面):
[核心] 选择模式后→LoadingScreen可见,进度条/提示非空
[核心] 加载完成→LoadingScreen隐藏,游戏HUD显示
[风险] 加载中按任意键→不影响加载

6. ESC 暂停菜单(1 个用例,含 3 个子状态)

FE-06 暂停菜单
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
FE-06-01 暂停菜单 游戏中 按 ESC 弹出暂停菜单 🔴 高 🟢 回归(暂停流程)
FE-06-02 继续游戏 暂停菜单中 点击继续 暂停菜单关闭,返回游戏 🔴 高 🟢 回归(暂停流程)
FE-06-03 从暂停菜单回到大厅 暂停菜单中 选择回到大厅 退出当前对局,回到主菜单 🟡 中 🟢 回归(退出流程)

代码实现思路

1
2
3
4
FE-06(ESC暂停):
[核心] 游戏中ESC→PauseMenu可见
[核心] 继续→PauseMenu关闭,游戏恢复
[核心] 回到大厅→主菜单重新显示

音频系统 — 测试宪章

测试范围

# 测试项 说明
1 音量设置 🖐️ 总体/音乐/SFX/对白/语音 音量滑块调节,共 1 个用例(含 5 个子状态)
2 音频输出 🖐️ 切换音频输出设备(耳机/扬声器),共 1 个用例(含 2 个子状态)
3 HDR 音频 🖐️ 高动态范围音频开关切换,共 1 个用例(含 2 个子状态)
4 加载画面音频 🖐️ 加载画面期间的音频表现,共 1 个用例(含 2 个子状态)
5 数值测试 🤖 音量边界值 + 通道独立调节,共 2 个用例(含 5 个子状态)

测试计划

测试方式 适用用例
🖐️ 手工测试 AU-01 ~ AU-04
🤖 C++ CQTest NV-01 ~ NV-02

详细分配

类别 手工测试 C++ CQTest
音量设置 (AU-01) 1 个全部(含 5 个子状态)
音频输出 (AU-02) 1 个全部(含 2 个子状态)
HDR 音频 (AU-03) 1 个全部(含 2 个子状态)
加载画面音频 (AU-04) 1 个全部(含 2 个子状态)
数值测试 (NV) 2 个全部(含 5 个子状态)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 音量设置(1 个用例,含 5 个子状态)

AU-01 音量设置
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AU-01-01 总体音量调节 游戏中 在设置中调整总体音量滑块 游戏所有音量随之变化 🔴 高 🟠 验收(听感/设置生效,手工)
AU-01-02 音乐音量调节 游戏中有背景音乐 调整音乐音量 背景音乐音量变化,SFX 不受影响 🟡 中 🟠 验收(听感/设置生效,手工)
AU-01-03 SFX 音量调节 游戏中有音效 调整 SFX 音量 射击/脚步等音效变化,音乐不受影响 🔴 高 🟠 验收(听感/设置生效,手工)
AU-01-04 对白音量调节 游戏中有对白 调整对白音量 对白音量变化 🟢 低 🟠 验收(听感/设置生效,手工)
AU-01-05 语音聊天音量 多人游戏中 调整语音音量 其他玩家的语音音量变化 🟢 低 🟠 验收(听感/设置生效,手工)

2. 音频输出(1 个用例,含 2 个子状态)

AU-02 音频输出
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AU-02-01 切换音频输出设备 游戏中 在设置中切换音频输出设备(耳机→扬声器) 音频从新设备输出 🟡 中 🟠 验收(听感,手工)
AU-02-02 设备切换失败处理 设置中 切换到一个不存在的设备 不崩溃,保持当前设备或显示错误提示 🟡 中 🟠 验收(容错,手工)

3. HDR 音频(1 个用例,含 2 个子状态)

AU-03 HDR 音频
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AU-03-01 开启 HDR 音频 默认 LDR 模式 在设置中开启 HDR 音频 音频动态范围增强,音质变化可感知 🟢 低 🟠 验收(听感,手工)
AU-03-02 关闭 HDR 音频 HDR 模式 关闭 HDR 音频 恢复到 LDR 音频表现 🟢 低 🟠 验收(听感,手工)

4. 加载画面音频(1 个用例,含 2 个子状态)

AU-04 加载画面音频
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
AU-04-01 加载画面期间音频淡出 进入加载画面 观察加载过程中音频 游戏音频逐渐淡出或切换到加载画面背景音 🟡 中 🟠 验收(听感,手工)
AU-04-02 加载完成后音频恢复 加载完成 加载完成进入游戏 音频恢复正常 🟡 中 🟠 验收(听感,手工)

5. 数值测试 🤖 自动化测试(2 个用例,含 5 个子状态)

NV-01 音量参数验证(有效区间 [0, 1.0])
编号 细分状态 配置音量 测试步骤 预期结果 风险关注 优先级 用例目的
NV-01-01 ⚠️ a-ε 低于最小 -0.01 播放任意音效 不崩溃,音量为 0 负值导致音频计算异常/除零 🟡 中 ⚪ 回归·单元(clamp 校验)
NV-01-02 a 最小值 0 播放任意音效 所有音频静音 🟡 中 ⚪ 回归·单元(边界校验)
NV-01-03 (a+b)/2 中间值 0.5 播放任意音效 音频正常播放,音量为 50% 🔴 高 ⚪ 回归·单元(数据校验)
NV-01-04 b 最大值 1.0 播放任意音效 音频正常播放,不破音 浮点溢出→音频裁剪失真 🟡 中 ⚪ 回归·单元(边界校验)
NV-02 各通道音量独立调节
编号 前置条件 测试步骤 预期结果 优先级 用例目的
NV-02 分别调整各通道 逐一验证 各通道音量独立生效,互不影响 🟡 中 ⚪ 回归·单元(通道独立)

代码实现思路

1
2
3
4
5
6
7
8
9
10
11
12
环境: 单元测试,无需PIE → ULyraSettingsLocal(UPROPERTY Config)

NV-01(音量 [0,1]):
[Risk] SetOverallVolume(-1.0) → 不崩溃,值被clamp到≥0
[Min] 音量=0 → 静音,各通道无输出
[Mid] 音量=0.5 → 正常50%音量
[Max] 音量=1.0 → 最大音量,无破音失真
数据源: ULyraSettingsLocal.OverallVolume(Config属性)

NV-02(各通道独立):
[核心] Music/SFX/Dialogue/VoiceChat 各通道单独调整时互不影响
数据源: ULyraSettingsLocal.{MusicVolume, SoundFXVolume, DialogueVolume, VoiceChatVolume}

队伍系统 — 测试宪章

测试范围

# 测试项 说明
1 团队归属 分配到队伍 + 切换 + 平衡,共 1 个用例(含 4 个子状态)
2 友军识别 队友/敌人标记 + 伤害判定,共 1 个用例(含 3 个子状态)
3 团队显示 队伍颜色 + 名称 + 更新,共 1 个用例(含 3 个子状态)
4 团队标签 队伍得分/击杀统计,共 1 个用例
5 战绩统计 比分板 + 个人战绩 + 实时更新,共 1 个用例(含 3 个子状态)
6 数值测试 🚫 已删(:游戏无队伍数量参数)

测试计划

测试方式 适用用例
🖐️ 手工测试 TM-01 ~ TM-05

详细分配

类别 手工测试 C++ CQTest
团队归属 (TM-01) 1 个全部(含 4 个子状态)
友军识别 (TM-02) 1 个全部(含 3 个子状态)
团队显示 (TM-03) 1 个全部(含 3 个子状态)
团队标签 (TM-04) 1 个全部
战绩统计 (TM-05) 1 个全部(含 3 个子状态)
数值测试 (NV-01) 🚫 已删

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 团队归属(1 个用例,含 4 个子状态)

TM-01 团队归属
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
TM-01-01 进入游戏时分配到队伍 玩家加入游戏 进入对局 玩家被分配到某个队伍(红队或蓝队) 🔴 高 🟠 验收(多人场景,手工)
TM-01-02 同一队伍有多个玩家 多个玩家加入 全部加入同一队伍 同队玩家共享相同的队伍标识 🔴 高 🟠 验收(多人场景,手工)
TM-01-03 切换队伍 游戏中 切换到另一队伍 玩家队伍标识更新,位置/出生点可能改变 🟡 中 🟠 验收(多人场景,手工)
TM-01-04 队伍人数平衡 两队人数不均 新玩家加入 新玩家分配到人数较少的一方 🟡 中 🟠 验收(平衡逻辑,手工)

2. 友军识别(1 个用例,含 3 个子状态)

TM-02 友军识别
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
TM-02-01 友军标记 队友在视野内 观察队友 队友头上有友军标记(颜色/图标区分) 🔴 高 🟠 验收(视觉,手工)
TM-02-02 敌军标记 敌人在视野内 观察敌人 敌人不同颜色 🔴 高 🟠 验收(视觉,手工)
TM-02-03 ⚠️ 友军伤害 对友军射击 射击队友 友军不受伤害子弹消失 🔴 高 🟠 验收(判定,手工;可考虑自动化)

⚠️ TM-02-03 风险:友军伤害开关异常可能导致友军误伤或自伤

3. 团队显示(1 个用例,含 3 个子状态)

TM-03 团队显示
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
TM-03-01 队伍颜色 游戏开始 观察各队伍 不同队伍有不同颜色标识(红/蓝等) 🔴 高 🟠 验收(视觉,手工)
TM-03-02 队伍名称 游戏中 查看比分板或队友头顶 显示队伍名称 🟡 中 🟠 验收(视觉,手工)
TM-03-03 换队后颜色更新 切换队伍后 切换到另一队 玩家颜色/标记更新为新队伍 🟡 中 🚫 删除(游戏无换队功能)

🚫 删除说明:TM-03-03(换队后颜色更新)依赖换队功能,TM-01 已删,游戏无换队入口,移除。

4. 团队标签(1 个用例)

TM-04 团队标签
编号 前置条件 测试步骤 预期结果 优先级 用例目的
TM-04 游戏中 查看比分板 显示各队伍的总击杀/得分 🟡 中 🟠 验收(视觉,手工)

5. 战绩统计(1 个用例,含 3 个子状态)

TM-05 战绩统计
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
TM-05-01 比分板显示 游戏中 按比分板键 显示双方队伍得分/击杀/死亡数据 🟡 中 🟠 验收(视觉,手工)
TM-05-02 个人战绩 游戏中 查看比分板 显示每个玩家的击杀/死亡/助攻 🟡 中 🟠 验收(视觉,手工)
TM-05-03 战绩实时更新 游戏中 击杀敌人后查看比分板 击杀数/得分实时更新 🟡 中 🟠 验收(视觉,手工)

6. 数值测试 — 🚫 已删

🚫 删除说明:游戏不存在队伍数量配置参数,NV-01-01~04 全部移除。


初始化链路系统 — 测试宪章

测试范围

# 测试项 说明
1 游戏启动 进程/Experience/网络同步,共 1 个用例(含 3 个子状态)
2 资源生成 Pawn/AbilitySet/武器/属性/拾取/特效,共 1 个用例(含 6 个子状态)
3 重生 重生链路 + 状态重置,共 1 个用例(含 2 个子状态)
4 输入绑定 PlayerController 绑定 + 输入初始化,共 1 个用例(含 2 个子状态)
5 断线重连 重连 + 状态恢复 + 资源防重复,共 1 个用例(含 3 个子状态)
6 数值边界 🤖 重生延迟边界值,共 1 个用例(含 3 个子状态)

全部自动化测试。目标:验证初始化链路完整跑通,所有必要资源存在且正确加载。


测试计划

测试方式 适用用例
🤖 C++ CQTest + Gauntlet 全部 6 个用例(含 19 个子状态)

详细分配

类别 手工测试 C++ CQTest
全部 (IN-01~05, NV-01) 6 个全部(含 19 个子状态)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 游戏启动(1 个用例,含 3 个子状态)

IN-01 游戏启动
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
IN-01-01 游戏进程启动 启动游戏 拉起游戏进程 进程正常启动,无崩溃,日志无错误 🔴 高 🔵 冒烟(环境基线)
IN-01-02 Experience 加载 选择游戏模式 触发 Experience 加载 Experience 资源加载完成,GameMode 初始化成功 🔴 高 🔵 冒烟(链路基线)
IN-01-03 网络同步就绪 客户端连接服务器 等待同步 客户端与服务器同步完成,GameState 正确复制 🔴 高 🔵 冒烟 + 🟢 回归(网络同步)

代码实现思路

1
2
3
4
5
6
7
环境: Gauntlet BootTest → 进程+日志+同步

IN-01(游戏启动):
[核心] 启动进程→存活无崩溃退出,日志无Error/Fatal
[核心] Experience加载→GameMode初始化成功
[核心] 客户端连服务端→GameState全量同步完成
框架: Gauntlet BootTest

2. 资源生成(1 个用例,含 6 个子状态)

IN-02 资源生成
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
IN-02-01 Pawn 资源加载 玩家加入 检查 Pawn 生成 Pawn Class 存在,生成无报错 🔴 高 🔵 冒烟(链路基线)
IN-02-02 AbilitySet 资源 Pawn 生成后 验证 AbilitySet 所有 GrantedAbilities 正确注册到 ASC 🔴 高 🔵 冒烟 + 🟢 回归(能力注册)
IN-02-03 默认武器资源 Pawn 生成后 验证武器 默认武器资源(Mesh/Texture)存在且加载 🔴 高 🔵 冒烟 + 🟢 回归(资源存在性)
IN-02-04 角色属性初始化 Pawn 生成后 验证属性 Health/MaxHealth 初始值为满值,无 NaN 🔴 高 🟢 回归(属性初始化)
IN-02-05 拾取武器资源 拾取武器后 验证生成 武器 Actor 生成,武器模型资源加载完成 🟡 中 🟢 回归(资源生成)
IN-02-06 特效资源加载 触发特效时 验证特效 粒子/音效资源已加载,播放无资源缺失 🟡 中 🟢 回归(资源存在性)

代码实现思路

1
2
3
4
5
6
7
8
9
10
环境: FMapTestSpawner(L_Expanse) → FindFirstPlayerPawn

IN-02(资源生成):
[核心] Pawn生成→Class非空,无报错
[核心] ASC→所有GrantedAbilities已注册
[核心] 默认武器Mesh/Texture已加载(IsPending=false)
[核心] LyraHealthComponent→Health=MaxHealth,无NaN
[扩展] 拾取武器→Actor生成,Mesh加载
[扩展] 触发特效→粒子/音效已加载,无MissingResource
数据源: ASC->GetGrantedAbilities(), HealthComponent->Health/MaxHealth

3. 重生(1 个用例,含 2 个子状态)

IN-03 重生
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
IN-03-01 重生链路 角色死亡 等待重生 重生后 Pawn 重新生成,链路完整跑通 🔴 高 🟢 回归(与 DT-06 重叠,建议并入)
IN-03-02 重生后状态重置 角色死亡 重生后检查 重生后 AbilitySet 重新授予,装备重置 🔴 高 🟢 回归(与 DT-06 重叠,建议并入)

代码实现思路

1
2
3
4
5
6
环境: FMapTestSpawner(L_Expanse) → 致死伤害+重生

IN-03(重生):
[核心] 致死伤害→死亡→重生→新Pawn生成+Possessed,日志无错误
[核心] 重生后→AbilitySet重新授予,默认装备重新绑定
数据源: Pawn->GetClass(), ASC->GetGrantedAbilities()

4. 输入绑定(1 个用例,含 2 个子状态)

IN-04 输入绑定
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
IN-04-01 PlayerController 绑定 Pawn 生成后 验证绑定 PlayerController 正确 Possess Pawn 🔴 高 🔵 冒烟(输入链路基线)
IN-04-02 输入组件初始化 Pawn 生成后 验证输入 输入组件(EnhancedInput)已初始化 🔴 高 🔵 冒烟(输入链路基线)

代码实现思路

1
2
3
4
5
6
环境: FMapTestSpawner(L_Expanse) → PC+Pawn

IN-04(输入绑定):
[核心] PC->GetPawn()非空,IsPossessing()=true
[核心] PlayerInputComponent→类型为EnhancedPlayerInput,映射上下文已添加
数据源: PC.GetPawn(), InputComponent->GetClass()

5. 断线重连(1 个用例,含 3 个子状态)

IN-05 断线重连
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
IN-05-01 断线后重连 游戏中断开网络 重新连接 客户端重连成功,重新进入游戏 🟡 中 🟢 回归(网络恢复)
IN-05-02 重连后状态恢复 重连成功 检查状态 角色 AbilitySet 重新授予,状态正确 🟡 中 🟢 回归(状态恢复)
IN-05-03 资源重复加载 多次重连 检查资源 资源不重复加载,内存无泄漏 🟡 中 🟢 回归(资源防重复)

代码实现思路

1
2
3
4
5
6
7
环境: Gauntlet(客户端+服务端)

IN-05(断线重连):
[核心] 模拟断网→重连成功,重新进入游戏
[核心] 重连后→AbilitySet重新授予,ASC能力数量与初始一致
[风险] 多次重连→资源不重复加载,内存无泄漏
框架: Gauntlet + 内存分析

6. 数值边界 🤖 自动化测试(1 个用例,含 3 个子状态)

NV-01 重生延迟参数验证(有效区间 [0, 60])
编号 细分状态 配置重生延迟 测试步骤 预期结果 风险关注 优先级 用例目的
NV-01-01 a 最小值 0 角色死亡 角色立即重生,链路无异常 除零:延迟为0导致Timer/FPS除零 🔴 高 🟢 回归(数值边界:重生延迟 0)
NV-01-02 (a+b)/2 中间值 30 角色死亡 等待约 30 秒后重生 🔴 高 🟢 回归(数值边界)
NV-01-03 b 最大值 60 角色死亡 等待约 60 秒后重生 🔴 高 🟢 回归(数值边界)

⚠️ NV-01-01 风险:重生延迟为 0 可能导致 Timer 异常或帧循环除零

代码实现思路

1
2
3
4
5
6
7
8
环境: FMapTestSpawner(L_Expanse) → GameMode重生配置

NV-01(重生延迟 [0,60]):
[Min] 0 → 角色立即重生,链路无异常
[Mid] 30 → 等待约30秒后重生
[Max] 60 → 等待约60秒后重生
[Risk] 除零:Timer/FPS
数据源: GameMode 重生延迟配置

状态系统 — 测试宪章

🚫 已砍(决策):状态系统不再单独测试。
血量/死亡相关验证并入事件测试(DT-01 血量归零触发死亡);本文档仅作历史参考,不实现 ST/NV CQTest。

测试范围

# 测试项 说明
1 生命属性 血量/最大血量、受伤/治疗,共 1 个用例(含 4 个子状态)
2 死亡状态机 🖐️ NotDead → DeathStarted → DeathFinished 流转,共 1 个用例(含 3 个子状态)
3 数值测试 🤖 初始血量 + 最大血量边界,共 2 个用例(含 7 个子状态)

测试计划

测试方式 适用用例
🖐️ 手工测试 ST-02(死亡状态机视觉验证)
🤖 C++ CQTest ST-01, NV-01 ~ NV-02

详细分配

类别 手工测试 C++ CQTest
生命属性 (ST-01) 1 个全部(含 4 个子状态)
死亡状态机 (ST-02) 1 个全部(含 3 个子状态)
数值测试 (NV) 2 个全部(含 7 个子状态)

1. 生命属性(1 个用例,含 4 个子状态)

ST-01 生命属性
编号 细分状态 前置条件 测试步骤 预期结果 优先级
ST-01-01 初始血量为满值 角色生成 读取血量 当前血量 = 最大血量 🔴 高
ST-01-02 受伤后血量减少 角色有血量 受到伤害 X 血量减少 X,不低于 0 🔴 高
ST-01-03 治疗后血量增加 角色血量不满 接受治疗 Y 血量增加 Y,不超过最大值 🔴 高
ST-01-04 治疗不超上限 角色接近满血 过量治疗 血量不超过最大血量 🟡 中

2. 死亡状态机 🖐️(1 个用例,含 3 个子状态)

ST-02 死亡状态机
编号 细分状态 前置条件 测试步骤 预期结果 优先级
ST-02-01 血量归零触发死亡 角色有血量 造成致命伤害 血量归零,角色进入死亡状态 🔴 高
ST-02-02 死亡后不可操作 角色死亡 尝试移动/射击 角色不能移动,不能操作 🔴 高
ST-02-03 死亡视觉反馈 角色死亡 观察 死亡动画/特效播放 🟡 中

3. 数值测试 🤖 自动化测试(2 个用例,含 7 个子状态)

NV-01 初始血量参数验证(有效区间 [0, MaxHP])
编号 细分状态 配置初始血量 测试步骤 预期结果 风险关注 优先级
NV-01-01 ⚠️ a-ε 低于最小 -1 角色生成 不崩溃,血量 clamp 到 0 或取绝对值 负血量→HUD 显示异常/治疗循环死锁 🔴 高
NV-01-02 a 最小值 0 角色生成 角色生成时血量为 0,直接死亡或特殊状态 除零:血量为0导致百分比计算除零 🔴 高
NV-01-03 (a+b)/2 中间值 50 角色生成 角色以 50 点血量生成 🔴 高
NV-01-04 b 最大值 100 角色生成 角色以最大血量 100 生成 🔴 高
NV-02 最大血量参数验证(有效区间 [1, 10000])
编号 细分状态 配置最大血量 测试步骤 预期结果 风险关注 优先级
NV-02-01 a 最小值 1 角色生成 角色以 1 点最大血量生成,血量 = 1 🟡 中
NV-02-02 (a+b)/2 中间值 5000 角色生成 角色以 5000 点最大血量生成 🔴 高
NV-02-03 b 最大值 10000 角色生成 角色以 10000 点最大血量生成 数值溢出→HUD 显示/伤害计算异常 🟡 中

⚠️ NV-01-01 风险:负血量可能导致 HUD 显示异常、治疗逻辑死循环
⚠️ NV-01-02 风险:血量为 0 可能导致百分比计算除零崩溃
⚠️ NV-02-03 风险:最大血量过高可能导致 HUD 显示或伤害计算数值溢出

代码实现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
环境: FMapTestSpawner(L_Expanse) → ULyraHealthComponent

NV-01(初始血量 [0,100]):
[Risk] a-ε=-1 → 不崩溃,被clamp到0或取绝对值
[Min] a=0 → 血量=0,角色直接进入死亡/特殊状态
[Mid] (a+b)/2=50 → 血量=50
[Norm] b=100 → 满血
数据源: HealthComponent->Health / ULyraHealthComponent 属性

NV-02(最大血量 [1,10000]):
[Min] 1 → Health=MaxHealth=1
[Mid] 5000 → MaxHealth=5000,正常显示
[Risk] 10000 → 不溢出,HUD和伤害计算正常
数据源: HealthComponent->MaxHealth / ULyraHealthComponent 属性

事件测试 — 测试宪章

测试范围

# 测试项 说明
1 死亡 🤖/🖐️ 状态机冻结/摄像机/状态标签/网络同步/边界,共 5 个用例(含 16 个子状态;DT-04 网络暂缓)
2 重生 🤖 重生后完整性 + 边界情况,共 2 个用例(含 7 个子状态)
3 散布系统 🤖 heat 累积冷却/散布映射/修正乘数/首发精度/曲线边界,共 6 个用例(含 20 个子状态,后续排期)

📦 交付范围:DT-01/02/03/05/06/07(DT-04 网络暂缓,SP 后续排期)。术语统一:死亡 / 淘汰 / 重生(Lyra 无”复活”,原”复活”表述全部改称”重生”)。


测试计划

测试方式 适用用例 说明
🖐️ 手工测试 DT-02-02(重生后摄像机恢复)、DT-05-04(Dash 中死亡) 视觉/手感判断
🤖 C++ CQTest DT-01(含 DT-02-01 相机切换)、DT-03、DT-05-01~03、DT-06、DT-07-01 单机死亡/重生链路(回归 CQTest,弃用蓝图功能测试)
🤖 C++ CQTest(后续) DT-04(网络同步)、SP-01 ~ SP-06 PIE Network + CQTest 实现

详细分配

类别 手工测试 蓝图功能测试 C++ CQTest
死亡 (DT) DT-02-02、DT-05-04 无(已弃用) DT-01(含 DT-02-01 相机切换)、DT-03、DT-05-01~03、DT-06、DT-07-01
散布 (SP) 6 个全部(后续排期)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 PIE)|🟠 验收(功能可用性/视觉手感,开发手测等价物)|🚫 已并入或删除(见《用例分级清单》)

1. 死亡

设计依据

Lyra 死亡状态机:NotDead → DeathStarted → DeathFinished → Actor销毁

1
2
3
4
5
6
7
HandleOutOfHealth() [Server]
→ GameplayEvent.Death
→ GA_Hero_Death 激活
→ StartDeath(): DeathState=DeathStarted, Status.Death.Dying, 广播 OnDeathStarted
→ 禁用移动/碰撞
→ FinishDeath(): DeathState=DeathFinished, Status.Death.Dead, 广播 OnDeathFinished
→ 销毁 Actor

死亡冻结范围:基础移动(WASD)、高阶移动(跳跃/蹲下/Dash)、碰撞体、交互操作、视角(look 输入)
死亡后切换 CM_ThirdPerson_Death,视角冻结(实测修正:原宪章”视角不冻结”有误)。

1.1 死亡状态机与移动冻结 (1 个用例,含 5 个子状态)

DT-01 死亡状态机与移动冻结
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
DT-01-01 ⚠️ 死亡后基础移动冻结 角色死亡 死亡后按方向键 角色不能移动,不能转向 🔴 高 🟢 回归(吸收 FR-01)
DT-01-02 ⚠️ 死亡后高阶移动冻结 角色死亡 死亡后依次尝试跳跃/蹲下/Dash 角色不能跳跃、不能蹲下、不能 Dash 🔴 高 🟢 回归
DT-01-03 ⚠️ 死亡后碰撞体冻结 角色死亡 死亡后其他角色/子弹与尸体碰撞 胶囊碰撞体为 NoCollision,无碰撞 🔴 高 🟢 回归(吸收 DT-05-02)
DT-01-04 ⚠️ 死亡后禁用所有交互 角色死亡 死亡后按开火/换弹/互动键 所有操作无效(Ability System Status.Death 阻断) 🔴 高 🟢 回归(吸收 WF-01-04)
DT-01-05 ⚠️ 死亡后视角冻结 + 相机切换 角色死亡 死亡后移动鼠标/右摇杆 视角冻结:look 输入不影响相机(死亡摄像机 CM_ThirdPerson_Death) 🟡 中 🟢 回归(视角冻结 + 相机模式切换校验)

⚠️ DT-01-01 / DT-01-04 风险:死亡后如果输入/交互未被完全禁用,玩家可能”死后开枪”或操作幽灵角色
⚠️ DT-01-03 风险:碰撞体必须设为 NoCollision,避免尸体阻挡其他玩家/子弹
⚠️ DT-01-05 风险:死亡摄像机(CM_ThirdPerson_Death)为固定视角,若 look 输入仍能转动相机说明冻结失效

1.2 死亡摄像机 (1 个用例,含 1 个子状态;DT-02-01 已并入 DT-01-05)

🤖 修正:相机模式切换(DT-02-01)视觉上无法判断是否换了摄像机,校验转 C++(死亡后相机模式栈顶层 = CM_ThirdPerson_Death,并入 DT-01-05);DT-02-02(重生后恢复)保留手工,位置/旋转/FOV/装备复位可观察。

DT-02 死亡摄像机
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
DT-02-01 死亡后摄像机切换 角色正常 击杀角色触发死亡 摄像机切换至 CM_ThirdPerson_Death 🔴 高 🚫 并入 DT-01-05(相机切换需校验相机模式栈,手工无法判断)
DT-02-02 重生后摄像机恢复 角色死亡后重生 重生后观察摄像机 摄像机切回默认模式,位置/旋转/FOV/装备 全部复位 🔴 高 🟠 验收(视觉,手工)

1.3 死亡状态标签 (1 个用例,含 3 个子状态)

DT-03 死亡状态标签
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
DT-03-01 死亡后 Status.Death.Dying 标签 角色受到致死伤害 StartDeath 后检查 AbilityTag 角色挂载 Status.Death.Dying 标签 🔴 高 🟢 回归(GAS 标签)
DT-03-02 死亡后 Status.Death.Dead 标签 角色死亡 FinishDeath 后检查 AbilityTag 挂载 Status.Death.Dead;Dying 与 Dead 并存至 Pawn 销毁(按代码修正:原”Dying 清除”有误) 🟡 中 🟢 回归(GAS 标签)
DT-03-03 死亡后 SurvivesDeath 技能保留 角色拥有 SurvivesDeath 技能 死亡后检查技能列表 标记 SurvivesDeath 的技能未被取消,其余被清除 🟡 中 🟢 回归(GAS 标签)

1.4 网络同步 (1 个用例,含 2 个子状态) — 暂缓

DT-04 死亡网络同步
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
DT-04-01 ⚠️ 服务端死亡→客户端死亡状态复制 PIE 多开 服务端击杀角色,观察客户端 客户端在合理延迟内复制 DeathState,死亡表现与服务端一致 🔴 高 🟢 回归(网络复制,后续排期)
DT-04-02 ⚠️ 客户端预测死亡 vs 服务器回滚冲突 PIE 多开 客户端本地预测死亡(如高延迟下),服务端判定未死 客户端回滚到非死亡状态,不遗留死亡标签/禁用状态 🔴 高 🟢 回归(预测回滚,后续排期)

⚠️ DT-04-01 风险:高延迟下 DeathState 复制延迟可能导致客户端角色已死亡但仍在行走
⚠️ DT-04-02 风险:客户端预测死亡后回滚,如果移动冻结/碰撞禁用未正确恢复,角色变成幽灵

代码实现思路

决策:改用轻量测试地图 L_ShooterTest_DeviceProperties(/ShooterTests/Maps),不用 L_Expanse(加载太重)。实现首步冒烟验证该地图角色具备 ASC/血量/死亡能力(GA_Hero_Death)完整链路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
环境: FMapTestSpawner(/ShooterTests/Maps, L_ShooterTest_DeviceProperties) → ULyraHealthComponent → ULyraCharacter

DT-01(死亡状态机与冻结):
[核心] DamageSelfDestruct() 触发真实死亡链路 → DeathState=DeathStarted
断言: 移动输入忽略 / 胶囊 NoCollision / 能力被取消 / 视角冻结
DT-03(死亡标签):
[核心] DeathStarted → Status.Death.Dying; DeathFinished → Status.Death.Dead
SurvivesDeath 能力(GA_AutoRespawn)保留,其余取消
DT-05(边界):
[核心] 死亡中二次致死(直接发 DamageGE) → 状态机幂等
空中/蹲下中死亡 → 正常进入死亡,不崩溃
DT-06(重生完整性):
[核心] 显式 RequestPlayerRestartNextFrame → 等新 Pawn
断言: 血量满 / 装备与初生一致 / GAS 无残留死亡标签 / 移动碰撞恢复
DT-07-01(重生后立即死亡):
[核心] 新 Pawn 出现即二次致死 → 正常进入死亡流程,不卡死
框架: CQTest + FMapTestSpawner(单机)

DT-04(死亡网络同步, 后续):
环境: FPIENetworkComponent(1服务端+2客户端PIE)
[核心] 服务端击杀 → 客户端DeathState最终与一致,碰撞/移动同步
[风险] 客户端预测死亡→服务端回滚 → 移动/碰撞恢复正常,无残留死亡标签
框架: CQTest + FPIENetworkComponent

1.5 边界情况 (1 个用例,含 4 个子状态;DT-05-05 已删)

DT-05 死亡边界情况
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
DT-05-01 ⚠️ 连续致死 角色第一次死亡中 死亡过程中再次受到致死伤害 状态机不异常(不应从 DeathStarted 重新触发) 🔴 高 🟢 回归(状态机幂等)
DT-05-02 空中死亡 角色在空中 空中受到致死伤害,观察下落 碰撞禁用后角色穿透地面?或有特殊坠落处理 🟡 中 🚫 并入 DT-01-03
DT-05-03 蹲下中死亡 角色蹲伏中 蹲伏状态受到致死伤害 碰撞体高度是否恢复为默认?还是保持蹲伏碰撞? 🟡 中 🟢 回归(碰撞残留)
DT-05-04 🖐️ Dash 中死亡 Dash 过程中 Dash 途中受到致死伤害 Dash 立即中断,进入正常死亡流程 🟡 中 🟠 验收(已决策手工)

⚠️ DT-05-01 风险:连续致死可能导致 Ability System 异常、双重广播 Elimination 消息
⚠️ DT-05-02 风险:空中死亡禁用碰撞后直接穿透地形,视觉效果异常
⚠️ DT-05-03 风险:蹲伏死亡碰撞体不恢复可能导致重生时卡在矮空间

2. 重生

设计依据

Lyra 重生流程(Lyra 无原地”复活”,Pawn 销毁后由 GameMode 重新生成):

1
2
3
死亡 → OnDeathFinished → DestroyDueToDeath → UninitAndDestroy (Actor 销毁/隐藏)
→ GameMode::RequestPlayerRestartNextFrame()
→ RestartPlayer() → Spawn 新 Actor → Possess → 重新初始化 Ability System

决策:DT-06/07 由测试显式调用 RequestPlayerRestartNextFrame(PC) 驱动重生(走生产 RestartPlayer 链路),不等待 Elimination 经验的重生计时器。

2.1 重生后完整性 (1 个用例,含 6 个子状态)

DT-06 重生后完整性
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
DT-06-01 ⚠️ 重生后血量重置 角色死亡后重生 检查当前血量 血量为满(MaxHealth),无残留伤害状态 🔴 高 🟢 回归(状态重置)
DT-06-02 ⚠️ 重生后装备重置 角色死亡后重生 检查武器/装备槽位 装备状态与初次生成时一致(无上一局残留) 🔴 高 🟢 回归(状态重置)
DT-06-03 ⚠️ 重生后技能/GAS 重置 角色死亡后重生 检查 Ability System 状态 Ability 标签恢复初始,无残留 Status.Death 标签 🔴 高 🟢 回归(状态重置)
DT-06-04 ⚠️ 重生后所有移动恢复 角色死亡后重生 重生后按方向键 + 跳跃/蹲下/Dash 基础移动和高阶移动全部恢复正常 🔴 高 🟢 回归(吸收 FR-02)
DT-06-05 ⚠️ 重生后碰撞体恢复 角色死亡后重生 重生后与其他角色碰撞 碰撞体恢复正常,可正常阻挡/被阻挡 🔴 高 🟢 回归(碰撞恢复)
DT-06-06 重生后交互恢复 角色死亡后重生 重生后按开火/换弹/互动键 所有交互操作恢复正常 🔴 高 🟢 回归(交互恢复)

⚠️ DT-06-01 / DT-06-02 / DT-06-03 风险:重生后如果血量/装备/技能未完全重置,可能出现”死后残留”状态
⚠️ DT-06-04 / DT-06-05 风险:移动冻结/碰撞禁用标志未清除,角色变幽灵

2.2 边界情况 (1 个用例,含 1 个子状态;DT-07-02/03 已删)

DT-07 重生边界情况
编号 细分状态 前置条件 测试步骤 预期结果 优先级 用例目的
DT-07-01 ⚠️ 重生后立即死亡 角色刚重生 重生瞬间再次受到致死伤害 正常进入死亡流程,不卡死 🔴 高 🟢 回归(重生→死亡时序)

⚠️ DT-07-01 风险:重生→死亡的快速转换可能导致 Ability System 或 Spawner 状态机未正确重置

🚫 删除说明:DT-05-05(死亡时被推开,碰撞已禁用前提不成立)、DT-07-02(复活点”安全区域”预期无依据)、DT-07-03(复活无敌帧,早期不确定内容)已删除。

🔧 勘误:DT-07-03 原删除理由”复活无敌帧,Lyra 无此设计”表述错误——游戏实际存在重生无敌(Gameplay.DamageImmunity 标签,由 ShooterCore GE_SpawnIn / GA_SpawnEffect 授予,ULyraHealthSet::PreGameplayEffectExecute 将非自杀伤害归零)。因此 DT-07-01”重生后立即死亡”在正常伤害下不可触发(无敌窗口覆盖重生初始化窗口),测试使用自杀伤害(DamageSelfDestruct,穿透无敌)驱动该场景;DT-06/07 发现的时序问题属测试侧 flake,非游戏 bug。

3. 散布系统

📦 后续排期:本次交付不含散布系统,用例保留。

设计依据

Lyra 使用 heat 曲线驱动散布系统,核心流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
AddSpread() [每射击一次]:
HeatPerShot = HeatToHeatPerShotCurve.Eval(CurrentHeat)
CurrentHeat = ClampHeat(CurrentHeat + HeatPerShot)
CurrentSpreadAngle = HeatToSpreadCurve.Eval(CurrentHeat)

UpdateSpread() [每帧]:
若 TimeSinceFired > SpreadRecoveryCooldownDelay:
CooldownRate = HeatToCoolDownPerSecondCurve.Eval(CurrentHeat)
CurrentHeat = ClampHeat(CurrentHeat - CooldownRate * DeltaSeconds)
CurrentSpreadAngle = HeatToSpreadCurve.Eval(CurrentHeat)

UpdateMultipliers() [每帧]:
CurrentSpreadAngleMultiplier = AimM × StandM × CrouchM × JumpM

热量范围由三条曲线 X 轴并集动态决定。初始热量为范围中点(50%),默认 HeatPerShot=1.0,CoolDownRate=2.0/s。

3.1 热量累积与冷却

编号 细分状态 前置条件 测试步骤 预期结果 风险关注 优先级 用例目的
SP-01-01 连续射击热量累积 装备武器 连续射击 10 发,每发后记录 CurrentHeat 每射击一次 CurrentHeat 增加 HeatPerShot(默认 1.0),热量严格递增 🔴 高 🟢 回归(热量累积)
SP-01-02 停止射击后冷却 热量处于高位 停火后等待数秒 CurrentHeat 以 CooldownRate(默认 2.0/s)递减至热量范围中点 🔴 高 🟢 回归(冷却)
SP-01-03 HeatPerShot=0 无限射击 配置 HeatPerShot=0 连续射击 20 发 CurrentHeat 不增加,不过热,散布不扩大 除零:Heat=0 导致冷却计算除零 🔴 高 ⚪ 回归·单元(clamp 校验)
SP-01-04 过热后强制冷却 热量接近最大值 继续射击触发过热 武器无法射击,CurrentHeat 强制冷却至安全值 过热状态机异常→永久锁定 🟡 中 🟢 回归(过热状态机)

3.2 散布与热量映射

编号 细分状态 前置条件 测试步骤 预期结果 风险关注 优先级 用例目的
SP-02-01 热量-散布曲线求值 装备武器 在热量范围多采样点检查 CurrentSpreadAngle CurrentSpreadAngle = HeatToSpreadCurve.Eval(CurrentHeat),一一对应 曲线求值异常→散布突变 🔴 高 ⚪ 回归·单元(曲线求值)
SP-02-02 最小热量时最小散布 热量降至最低 停火至热量稳定后射击 实际弹孔散布半径等于 HeatToSpreadCurve 在最小热量处的值 🟡 中 🟢 回归(弹孔实测)
SP-02-03 最大热量时最大散布 热量升至最高 连射至过热前射击 实际弹孔散布半径等于 HeatToSpreadCurve 在最大热量处的值 🟡 中 🟢 回归(弹孔实测)

3.3 散布修正乘数

编号 细分状态 前置条件 测试步骤 预期结果 风险关注 优先级 用例目的
SP-03-01 ⚠️ 瞄准修正 开启瞄准 瞄准状态下射击 散布受 SpreadAngleMultiplier_Aiming 修正,瞬瞄/取消不跳变 瞄准乘数未应用→散布不变 🔴 高 🟢 回归(乘数生效)
SP-03-02 ⚠️ 静止修正 角色静止 站立不动射击 散布受 SpreadAngleMultiplier_StandingStill 修正(默认 1.0x) 静止奖励未缓动→瞬变 🟡 中 🟢 回归(乘数生效)
SP-03-03 ⚠️ 蹲下修正 角色蹲下 蹲下射击 散布受 SpreadAngleMultiplier_Crouching 修正(默认 1.0x) 蹲下乘数未生效 🟡 中 🟢 回归(乘数生效)
SP-03-04 ⚠️ 跳跃修正 角色跳跃/下落 跳跃中射击 散布受 SpreadAngleMultiplier_JumpingOrFalling 修正(默认 1.0x) 空中精度过高→跳跃射击过于精准 🟡 中 🟢 回归(吸收 WF-01-03)
SP-03-05 多乘数组合 多种状态叠加 跳跃+瞄准中射击 所有乘数相乘:实际散布 = CurrentSpreadAngle × 各乘数之积 乘数为负/0→散布消失或反转 🔴 高 🟢 回归(乘数组合)
SP-03-06 状态切换过渡 状态 A → B 快速蹲起/收镜/停走 乘数平滑过渡(TransitionRate=5.0),散布连续变化 过渡过快→散布跳变 🟡 中 🟢 回归(过渡时序)

3.4 首发精度

编号 细分状态 前置条件 测试步骤 预期结果 风险关注 优先级 用例目的
SP-04-01 首发精度关闭 bAllowFirstShotAccuracy=false 静止后射第一发 散布正常,不受首发精度影响 🟡 中 🟢 回归(首发精度)
SP-04-02 首发精度开启 bAllowFirstShotAccuracy=true 静止到最小散布后射第一发 第一发 CurrentSpreadAngleMultiplier=0,完美精度 🔴 高 🟢 回归(首发精度)
SP-04-03 移动后首发精度失效 首发精度开启 移动后射击 移动破坏精度,第一发也有散布 移动后未重置→首发精度残留 🔴 高 🟢 回归(首发精度)

3.5 散布恢复冷却延迟

编号 细分状态 配置 SpreadRecoveryCooldownDelay 测试步骤 预期结果 风险关注 优先级 用例目的
SP-05-01 无冷却延迟 0.0 射击后立即观察冷却 射击后立即开始冷却,热量递减 🟡 中 🟢 回归(冷却时序)
SP-05-02 有冷却延迟 1.0 射击后等待 0.5 秒再等待 1.5 秒 前 0.5 秒不冷却,1.0 秒后开始冷却 延迟未生效→冷却提前/推迟 🟡 中 🟢 回归(冷却时序)
SP-05-03 ⚠️ 冷却延迟为负 -1.0 射击后观察冷却 不崩溃,冷却行为符合 clamp 后的设计预期 负值导致 Timer 异常 🟡 中 ⚪ 回归·单元(clamp 校验)

3.6 散布曲线边界值

编号 细分状态 配置 测试步骤 预期结果 风险关注 优先级 用例目的
SP-06-01 ⚠️ SpreadExponent 低于最小 SpreadExponent=0 射击 不崩溃,ClampMin=0.1 生效 ClampMin 未生效→除零/精度异常 🔴 高 ⚪ 回归·单元(ClampMin 校验)
SP-06-02 SpreadExponent 正常值 SpreadExponent=1.0 射击 弹孔在散布范围内均匀分布 🔴 高 🟢 回归(分布实测)
SP-06-03 SpreadExponent 极高值 SpreadExponent=10.0 射击 弹孔高度集中中心,散布范围缩小 🟡 中 🟢 回归(分布实测)

代码实现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
环境: FMapTestSpawner(L_Expanse) → ULyraRangedWeaponInstance

SP-01(热量累积/冷却):
[核心] 连射10发 → CurrentHeat 严格递增(增量=HeatPerShot)
停火 → CurrentHeat 以 CooldownRate 递减
[Risk] HeatPerShot=0 → 20发不增加不过热
[Risk] 过热 → 武器禁用,冷却后恢复
数据源: ULyraRangedWeaponInstance.CurrentHeat

SP-02(散布-热量映射):
[核心] CurrentSpreadAngle = HeatToSpreadCurve.Eval(CurrentHeat)
[Min/Max] 最小热量/最大热量时的实际弹孔散布半径匹配曲线值

SP-03(修正乘数):
[核心] 瞄准/静止/蹲下/跳跃各状态 → 对应乘数生效
多状态叠加 → AimM×StandM×CrouchM×JumpM
状态切换 → FInterpTo缓动,非跳变
数据源: CurrentSpreadAngleMultiplier

SP-04(首发精度):
[核心] bAllowFirstShotAccuracy=true且静止 → 第一发Multiplier=0(完美精度)
移动后 → 标志清除,第一发也有散布

SP-05(冷却延迟):
[核心] SpreadRecoveryCooldownDelay=0 → 射击后立即冷却
=1.0 → 1秒后才开始冷却
[Risk] =-1.0 → 不崩溃,被clamp

SP-06(曲线边界):
[Risk] SpreadExponent=0 → ClampMin=0.1生效,不除零
正常值1.0 / 极高值10.0 → 弹孔分布符合预期
数据源: SpreadExponent

性能测试总结报告:Trace 分析轮次

状态:本轮 trace 性能分析已关闭(PIE 手工分析阶段);打包版复测已完成,见 §7
编号:PERF-2026-0807-SUM

覆盖:三轮 Unreal Insights trace 全量分析(共 3348 帧)


1. 测试概况

  • 目的:通过 trace 定位 LyraStarterGame 在 PIE 环境下的帧时间与卡顿问题,为打包版验证与自动化回归建立基线。
  • 范围:帧时间 / 卡顿分析(游戏线程、渲染线程、RHI、GPU、任务图、GC)。网络与内存专项不在本轮(内存见后续分析)。
  • 数据量:三轮 trace 共 3348 帧、13000+ 计时器、事件级全量导出分析。
  • 结论一句话:真实可复现的问题有两个——VolumetricCloud 尖峰(渲染侧)GC 卡顿(内存侧);事件型大帧多为 PIE 编译/编辑器成本,非游戏本体问题。(打包版复测后修订:GC 降级 P2,新增画质切换 PSO 冻结 P2,见 §7)

2. 测试环境

机器 Ryzen 7 6800H / 16 线程 / 32GB / 核显(Radeon 680M,无独显)
引擎 / 项目 UE 5.8 / LyraStarterGame
运行方式 编辑器 PIE(定位阶段)
工具 stat unit、Unreal Insights、ProfileGPU、无界面 trace 导出
trace 20260807_153712(首轮)、20260807_182849(稳定段)、20260807_184543(活跃段)

3. 方法

  1. 交叉验证(stat unit / trace / ProfileGPU),冲突时解释而非挑一个信。
  2. 口径对齐(单位、Avg/Max/Sum、Count;I.Avg 为按实例平均,单帧成本以帧内聚合为准)。
  3. Incl/Excl 下钻到叶子。
  4. 环境与上下文控制(启动方式、画面设置、周围碰撞体、特效编译状态)。

4. 主要发现

4.1 性能现状

指标 稳定段 活跃玩法
帧时间中位数 25.3ms(≈40 FPS) 33.5ms(≈30 FPS)
超 33ms 预算 2% 53%
最大帧 97.4ms 186.9ms

4.2 真问题(P1,可复现)

问题 证据 建议
VolumetricCloud 尖峰 + 渲染背压 三轮一致;体积云 8.6ms 典型 / 31.8–99ms 尖峰;游戏线程 Sync_RenderingThread 44–99ms 转视角复现;体积云质量档 / 降分辨率;打包复测
GC 卡顿 三轮两轮;PIE 含 PyUtil ~40ms(Python)+ mark ~30ms P1 待验证(未排除):本机无法打包;建议禁用 Python 插件本地验证 PyUtil 部分

4.2b GC 卡顿证据(P1)

轮次 帧时间 ConditionalCollectGarbage 同帧其他
稳定段(182849)@315.37s 82.2ms 70ms BroadcastPreGarbageCollect 39ms
活跃段(184543)@1357.58s 102.7ms 83ms BroadcastPreGarbageCollect 44ms
首轮(153712) 最大 0.05ms 未出现显著 GC
  • 性质:垃圾回收暂停,属内存/对象生命周期问题,与 PF-11(逐帧内存门禁)直接相关。
  • 结论:三轮中两轮出现显著 GC(>70ms),首轮几乎无——与场景对象生命周期/录制时长相关,非偶发。
  • 状态已复测→ 降级 P2。打包版 mark 仅 2.5ms(PIE 83ms 放大 33 倍,PyUtil 消失坐实);完整 collect 最差 18.1ms、对象销毁 37ms。详见 §7。

4.2c GC 定位:内部阶段拆分(两轮一致)

GC 内部阶段 稳定段 活跃段 性质
PyUtil::CollectGarbage 39ms 40ms Python 运行时回收(PyUtil = PythonScriptPlugin)
FRealtimeGC_PerformReachabilityAnalysis 28ms 35ms UE 可达性分析(mark 阶段)
PerformReachabilityAnalysisOnObjectsInternal 23ms 33ms 对象引用扫描
BroadcastPreGarbageCollect 39ms 44ms GC 前广播(含 PyUtil)
  • 发现 1:GC 约一半时间(~40ms)来自 PyUtil::CollectGarbage——Unreal Python 插件在 PIE 里的对象回收,打包版无 Python 运行时,预期消失
  • 发现 2:剩余 ~30ms 为 UE mark 阶段(可达性分析),与 UObject 数量挂钩——打包版待确认的本体成本。
  • 结论:GC 严重度部分为 PIE 放大(Python ~40ms),但 GC 本身打包版仍存在;mark 阶段成本取决于 UObject 数量,须打包复测确认(剩余 ~30ms 且低频可接受,50ms+ 或高频则进入对象层定位)。(已复测:mark 实际 2.5ms,UObject 数量健康,无需对象层定位。)
  • 触发链FEngineLoop::Tick → UWorld_Tick 内的引擎自动 GC(非外部强制)。
  • 第三层定位工具stat gcobj listmemreport -fullstat llm、trace 开 MemAlloc/MemTag 通道用 Memory Insights 看分配来源。

4.3 上下文相关(P2)

问题 证据 状态
拾取帧 198.7ms(首轮)↔ 8ms(活跃段),成本随上下文变化 25 倍 固定上下文复现后评估
输入处理 85.8ms(ProcessInputStack 58ms,单次) 触发条件待复现

4.4 排除项(非问题标记)

说明
Niagara 即时编译(拾取 141ms / 手雷 125ms) PIE 特有、一次性、打包后消失
窗口 Resize 97.4ms PIE 偶发
Slate 编辑器 UI ~8ms/帧 PIE 特有
环境劣化 trace(>300ms) Rider 启动 + 内存压力所致

5. 结论

  1. 性能画像:GPU 侧体积云为最大单 pass(稳态 8.6ms、尖峰 99ms),渲染背压造成游戏线程帧尾等待;GC 造成周期性 82ms 级暂停;事件型卡顿由 PIE 编译主导。
  2. 真问题集中在渲染侧(体积云)与内存侧(GC)——两者均需打包 Development 版确认正式数字。(打包版复测已完成:GC 降级 P2;体积云 GPU 侧待 ProfileGPU 确认)
  3. PIE 测量只用于定位方向;正式性能数字以打包版为准(环境结论)。

6. 数据与文档档案

  • trace:%LOCALAPPDATA%\UnrealEngine\Common\UnrealTrace\Store\001\(153712 / 182849 / 184543)
  • 导出数据:%TEMP%\uitrace_export\(timerstats / game_events / gpu_events / timers)
  • 截图:体积云性能截图GPU体积云性能截图
  • 说明:三轮分析细节已整合入本文档;中间文档(14/15/16)已删除。

7. 打包版复测

7.1 环境与方法

运行方式 打包 Development 版 D:\UEP\Windows\LyraGame.exe(非 PIE)
trace D:\UEP\trace_pkg_gc.utrace(117.5s,cpu/gpu/frame 通道)
GC 间隔 20s(gc.TimeBetweenPurgingPendingKillObjects 20,加速复现)
操作 进图后反复拾取 / 进出关卡;期间切换画质、打开设置菜单
工具 无界面导出(ExportTimingEvents/ExportTimers/ExportTimerStatistics)+ 阈值扫描(GC 事件 ≥10ms、全事件 ≥50ms)

7.2 GC:PIE 放大坐实,降级 P2

指标 PIE(08-07) 打包版(本次) 结论
mark(ReachabilityAnalysis) ~30ms 2.5ms PIE 放大 33 倍;PyUtil(~40ms)在打包版消失,PIE 放大假设坐实
ConditionalCollectGarbage 最差 83ms 18.1ms(t=20.5s) 真实,降到半帧预算内
对象销毁(DestroyObjects)最差 37ms(t≈117s) 增量销毁簇内的真活
  • 状态修订:GC 从「P1 待验证」→ P2(真实存在、低频、最差 ≤18ms)。mark 实际 2.5ms 说明 UObject 数量健康,无需对象层定位。
  • 备注:增量 purge 的 53–105ms 长跨度多为跨帧等待(Excl≈0),真活在 DestroyObjects 切片(≤37ms)。

7.3 新发现:画质切换触发 PSO 编译冻结(操作关联,P2)

  • 现象:t≈16.7–18.3s 连续数帧 1463–1589ms(游戏冻结约 1.5s)。
  • 链路PSOPrecache: MissedFD3D12DynamicRHI::RHICreateComputePipelineState(958ms)→ RenderGraphExecute 966ms → GameThreadWaitForTask 1030ms。
  • 触发:用户该时刻切换画质(低配机默认最高画质卡顿,调低),渲染器重建虚拟阴影(Nanite VSM)计算管线,驱动现场编译。
  • 判定:操作关联(路径 C)的一次性 PSO 编译冻结;待复跑确认「每次切换画质是否复现」——复现则建议 PSO 预缓存 / 异步编译;仅首次则冷缓存一次性。与 PIE「拾取编译」同类:事件型编译冻结在打包版依旧存在,触发点不同。

7.4 新发现:首次打开设置菜单 653ms(操作关联)

  • GetSettingCollection 单次 653ms(t=34.1s,Excl=652ms = 纯工作):Lyra 设置注册表首次构建。
  • 用户确认该时刻在调设置。低频一次性;若每次打开设置均 653ms 则列入 P2。

7.5 待定位

  • FNodeClassRegistry::RegisterNodeInternal 800ms(t=58.9s)——触发点未确认(音频 / 动画 / 蓝图节点注册?)。

7.6 体积云:打包版 CPU 侧无大成本,GPU 侧待确认

  • CPU 导出无 VolumetricCloud pass 耗时(InitVolumetricCloudsForViews 4020 次累计 0.7ms)。
  • GPU 侧需游戏内 stat gpu / ProfileGPU 确认(5.8 headless 不支持 GPU 事件导出)。原 P1(VolumetricCloud 尖峰)打包复测仍开放。

7.7 结论更新

  1. GC 不再是主凶:PIE 放大 33 倍坐实,打包版真实代价 18ms(P2)。
  2. 打包版主要 UX 卡顿 = 操作触发的编译类冻结(改画质 1.5s、设置菜单 653ms)——事件型,可用 PSO 预缓存优化或确认复现条件。
  3. 体积云打包版数字待 ProfileGPU 确认。

本轮 trace 性能分析关闭;打包版复测已完成(见 §7)。后续以自动化回归(Gauntlet + 阈值基线)为下一阶段。


探索性测试

⏱ 90 分钟 · 手工 · 以玩家视角自由探索,发现组合场景下的 bug

测试范围

探索时重点关注 跨系统交互边界操作,而非单功能验证。

记录方式

发现 bug 时记录:

  1. 做了什么(一两句话描述操作)
  2. 实际结果(看到了什么)
  3. 预期结果(应该是什么)

EX-01 探索性测试宪章 — 地图互动效果

目标:以玩家视角探索互动测试地图上的互动效果,发现异常。

形式:90 分钟 · 纯手工 · 记录

纯手工:只用键鼠/手柄和肉眼观察,不用脚本、控制台、调试工具;需要工具确认的怀疑点记入 backlog。

记录:做了什么 / 实际结果 / 预期结果 / 环境与节奏;Bug 编号 EX-01-xx。


EX-01 会话记录 — 地图互动效果

会话: EX-01 | 形式:90 分钟 · 纯手工 | 主题:地图互动效果(弹射器、传送门、手雷)

要素清单

  • 弹射器-向上
  • 弹射器-方向
  • 传送门
  • 手雷(玩法建议对象)
  • 冲刺(叠加对象)
  • 跳跃(叠加对象)

要素×关系表

编号 操作 实际结果 预期结果 关系维度
EX-01-01 冲刺中踩弹射器 音效正常但弹射力度大幅削弱,几乎弹不起来 无论是否冲刺都应完整弹射 组合(冲刺×弹射器)
EX-01-02 侧移/倒退踩方向弹射器 按面朝方向弹射,与移动方向不一致 按移动/速度方向弹射 方向一致性(朝向×速度)
EX-01-03 向弹射器投掷手雷 手雷不触发弹射 玩法建议:手雷可触发,形成弹雷玩法 边界/扩展(非 Bug)
EX-01-04 跳跃进入传送门上半部分 不触发传送;只有人高以下有效 判定范围与可视门框全高一致 表现与判定一致性

覆盖清单

  • 基线行为:弹射器正常触发 ✅、传送门正常触发 ✅
  • 组合:冲刺×弹射器 ✅、跳跃×传送门 ✅
  • 方向:朝向×移动方向 ✅
  • 未覆盖:多人场景、手柄操作、不同地图的同类装置

相关发现(同日登记在系统测试类别)

编号 一句话
AM-03-06 无方向冲刺只在特定朝向触发,其余朝向无反应
AM-02-06 滞空时下蹲被静默忽略
AM-04-01 近战中射击被静默忽略
AU-05-01 俯视角下贴脸敌人枪声反而变沉闷
CM-06 瞄准中冲刺导致相机缩放、准星闪变

EX-01 探索小结 — 地图互动效果

会话: EX-01 | 地图互动效果 | 90 分钟 · 纯手工

发现列表

编号 类型 等级 标题
EX-01-01 Bug 🟡 中 冲刺中触发弹射器力度被削弱
EX-01-02 Bug(或设计确认) 🟡 中 方向弹射器按面朝方向而非速度方向弹射
EX-01-03 玩法建议 手雷可触发弹射装置
EX-01-04 Bug 🟡 中 传送门仅下半部分可触发传送

相关发现(同日登记):AM-03-06(无方向冲刺朝向缺口)、AM-02-06(滞空下蹲被忽略)、AM-04-01(近战射击被忽略)、AU-05-01(视角相关枪声)、CM-06(瞄准中冲刺视觉闪变)。

最有价值的发现

AM-03-06:无方向冲刺只在特定 1/4 朝向区间触发,其余朝向完全无效。核心操作在大部分朝向失效,多地图复现、与关卡几何无关——最早暴露”冲刺交互链路存在系统性问题”的证据。

系统性规律

冲刺交互断层:冲刺(根运动)与其它输入/系统叠加时缺少统一策略,表现为三类:

  1. 输入被静默忽略——进行中动作期间,被拒绝的输入直接丢弃(AM-03-05、AM-02-06、AM-04-01)
  2. 外力被覆盖——冲刺中弹射力度被根运动覆盖(EX-01-01)
  3. 残留视觉副作用——瞄准中冲刺引起相机缩放、准星闪变(CM-06)

建议后续推进”输入与能力叠加”的统一策略(排队 / 明确拒绝 / 优先级),并先修复 AM-03-06 的朝向缺口。

下轮方向

性能测试:按《性能测试可行性调研》推进 PF-01~12 × 3 地图 = 36 实例;先做骨架 + 冒烟管线(如 PF-02 × 一张地图)验证启动与采样链路,再铺全量。

遗留 backlog

  • EX-01-02 需设计确认:方向弹射器按面朝方向还是速度方向
  • EX-01-03 手雷弹射玩法建议
  • EX-01-04 传送门判定范围与模型对齐(修复时做)
  • EX-01-01 冲刺与弹射器的优先级处理(修复时做)

蓝图功能测试实战:WF-01-01 单发武器射击

⚠️ 已废弃(决策):蓝图功能测试已弃用,回归 C++ CQTest(重复性高的用例),其余用手工测试。
复杂逻辑(输入模拟 + 异步等待 + 状态断言)在蓝图中难以编写和维护。
本文档仅作历史参考,不再作为实施指南;WF-01/02 改用 Source/LyraGame/Tests/CQTest/WeaponSystem_Fire.spec.cpp(CQTest + InputTestActions)实现。

对应宪章:TestDocs/01-系统测试/01-武器与装备系统.md
参考资产:Plugins/ShooterTests/Content/Blueprint/B_Test_FireWeapon
参考地图:Plugins/ShooterTests/Content/Maps/L_ShooterTest_FT_SingleShot(已拆分为每个测试独立地图)


前置:打开参考资产

在开始之前,先在编辑器打开 B_Test_FireWeapon 蓝图,边看边做:

  1. 内容浏览器 → Plugins → ShooterTests → Content → Blueprint → B_Test_FireWeapon
  2. 右键 → **Run Functional Test…**(先跑一次看看效果)
  3. 双击打开蓝图,看它的 Event Start Test 实现

第一步:创建测试地图

操作 说明
文件 → 新建关卡 选择 空关卡
另存为 Content/FunctionalTests/FT_WeaponTest_Main
打开 World Settings 工具栏 → 设置 → World Settings
Default Gameplay Experience 设为 B_BasicShooterTest(路径:ShooterTests/Content/System/Experiences/
GameMode Override 设为 BP_LyraShooterGameMode(项目默认)
保存地图

参考 L_ShooterTest_FT_SingleShot 的 WorldSettings 配置。所有测试地图都从 _Template_ShooterTest 模板创建,模板含基础世界设置和共享设施(Floor/Light/PlayerStart/Observation)。


第二步:创建测试蓝图

操作 说明
内容浏览器 → Content/FunctionalTests/ 右键 → 蓝图类
父类选择 搜索 FunctionalTest
命名 FT_WF_01_01_SingleShot
打开蓝图编辑器

第三步:声明变量

在 MyBlueprint 面板中新增以下变量:

变量名 类型 用途
TestPawn Actor (或 Character) 缓存玩家 Pawn
QuickBar LyraQuickBarComponent 缓存快捷栏组件
WeaponItem LyraInventoryItemInstance 当前武器物品
InitialAmmo Integer 射击前的弹药数
DidFire Boolean (默认 false) 标记是否已执行射击,防重复

变量都加 Instance Editable(不需要),至少确保它们是 PublicPrivate


第四步:编写 OnPrepareTest

在事件图表中右键搜索 On Prepare Test 事件,添加:

1
2
3
4
5
6
Event OnPrepareTest:
┌─ Get Player Pawn (Index 0) ──→ Set TestPawn
├─ TestPawn ──→ Get Component By Class (ULyraQuickBarComponent) ──→ Set QuickBar
├─ QuickBar ──→ Get Active Slot Item ──→ Set WeaponItem
├─ WeaponItem ──→ Get Stat Tag Stack Count ("Lyra.ShooterGame.Weapon.MagazineAmmo") ──→ Set InitialAmmo
└─ Print String ("WF-01-01: 准备完成,初始弹药 = " + InitialAmmo)

关于 GameplayTag:在 Get Stat Tag Stack Count 的 Tag 引脚,右键 → “Create a Make Literal GameplayTag” → 填入 Lyra.ShooterGame.Weapon.MagazineAmmo


第五步:编写 IsReady

搜索 Is Ready 事件,添加返回值:

1
2
3
4
5
Event IsReady (return bool):
┌─ Is Valid (TestPawn) ──────────┐
├─ Is Valid (QuickBar) ──────────┤ AND → return
├─ Is Valid (WeaponItem) ────────┤
└─ InitialAmmo > 0 ──────────────┘

使用 AND 布尔节点 连接所有条件。


第六步:编写 StartTest

搜索 Start Test 事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Event StartTest:
┌─ Input Key (Player Controller = TestPawn 的 Controller,
│ Key = Left Mouse Button,
│ Event Type = Pressed)
├─ Delay (0.5 秒)
├─ [获取当前弹药进行比较]
│ ┌─ WeaponItem ──→ Get Stat Tag Stack Count ("Lyra.ShooterGame.Weapon.MagazineAmmo") → CurrentAmmo
│ ├─ Assert Value (Int):
│ │ Int A = CurrentAmmo
│ │ Int B = InitialAmmo - 1
│ │ Comparison Method = Are Equal
│ │ 消息 = "WF-01-01: 射击后弹药应减1"
│ │
│ ├─ Print String ("初始:" + InitialAmmo + " 当前:" + CurrentAmmo)
│ └─ Finish Test (Succeeded)

关于 Input Key:需要从 Pawn 获取 Controller,然后 Cast 到 PlayerController,再调用 Input Key

Input Key 节点详解

TestPawnGet ControllerCast To PlayerControllerInput Key

参数
Player Controller 转型后的 PlayerController
Key Left Mouse Button(搜索 “LeftMouseButton”)
Event Type Pressed

第七步:编译、放置、测试

编译

  • 点击 编译(Compile),检查没有节点警告

放置到地图

  1. 打开 FT_WeaponTest_Main 地图
  2. FT_WF_01_01_SingleShot 拖入场景(放在任意位置)
  3. 选中它,在 Details 面板检查:
    • Time Limit = 30 秒(默认)
    • Times Up Result = Failed

运行测试

方法一:直接运行

  • 选中场景中的 FT_WF_01_01_SingleShot → Details 面板 → Run Test

方法二:通过 Session Frontend

  • 菜单 → 工具 → Session Frontend → Automation 标签
  • 搜索 “FT_WeaponTest”
  • 勾选 → Start Tests

常见问题排查

症状 原因 解决
Pawn 为 null Experience 没加载完 IsReady 中加 Is Valid 检查,调大到 30 秒
弹药始终为 0 GameplayTag 名称不对 确认你用的是 Lyra.ShooterGame.Weapon.MagazineAmmo
射击没触发出子弹 Input Key 没连对 检查 Cast 到 PlayerController, 用鼠标左键
测试超时 Failed FinishTest 没被调用 确保 Delay 后执行了 FinishTest
测试一直 Running IsReady 永远返回 false 打印一下各 IsValid 结果

扩展:WF-01-02 连发射击

Input Key 改成 Hold(Event Type = Released 时标记完成),并且:

  • StartTest 中按射击键不松(Pressed)
  • Event Tick 中检查弹药的消耗量 > 1
  • 延迟 1 秒后松开(Released)
  • 断言弹药消耗了多发

核心要点总结

1
2
3
4
5
FunctionalTest 蓝图 = 一张地图上的裁判决 Actor

OnPrepareTest → 缓存引用
IsReady → 等游戏就绪
StartTest → 执行操作 + 断言 + FinishTest

打开项目自带的 L_ShooterTest_FT_SingleShot(或 B_Test_FireWeapon 蓝图)边看边做,它是你最好的老师。注意:原 L_ShooterTest_FireWeapon 地图已拆分为 6 个独立测试地图,每个地图仅含 1 个测试 Actor,避免各测试互相影响。


Bug 清单(汇总)

生成:| 数据源:TestDocs/Bug报告/ 全部 13 份报告
统计:共 13 项(Bug 12 · 玩法建议 1)| 🔴 阻塞 0 | 🟡 11(含严重 3)| 🟢 1 | ⚪ 建议 1


1. 按系统分布

系统 数量 编号
控制系统 5 AM-02-06、AM-03-05、AM-03-06、AM-04-01、NV-03-02
地图互动(探索性 EX-01) 4 EX-01-01 ~ EX-01-04
武器与装备 2 WF-02-04、装备动画
相机系统 1 CM-06
音频系统 1 AU-05-01

2. 按来源分布

来源 数量 编号
探索性测试 EX-01(含同日关联发现) 9 EX-01-01~04、AM-02-06、AM-03-06、AM-04-01、AU-05-01、CM-06
手工 Session(封板前) 3 AM-03-05、WF-02-04、装备动画
CQTest 1 NV-03-02

3. 清单

编号 标题 等级 系统 来源 关联 状态/处置
AM-03-06 无方向 Dash 仅在面向特定 1/4 方向时触发 🟡 严重 控制系统 EX-01 宪章 AM-03-06 / GA_Hero_Dash 待修复(探索小结列为优先项)
AM-03-05 Dash 中操作行为不一致(蹲下排队 / 切装打断 / 射击·跳跃·近战·手雷无效) 🟡 严重 控制系统 手工 Session 2 宪章 AM-03-05 / GA_Hero_Dash 待修复(输入策略需决策:全部忽略 / 排队 / 打断)
WF-02-04 射击途中换弹被静默拒绝 🟡 严重 武器 手工补验 WF-02-01 / WF-02-05 待修复(换弹应中断射击)
AM-02-06 滞空时按下蹲被忽略(应触发或排队到落地) 🟡 中 控制系统 EX-01 关联 宪章 AM-02 / AM-03-05 待修复(与 AM-03-05 输入策略同源)
AM-04-01 近战中输入射击被忽略(应排队或触发) 🟡 中 控制系统 EX-01 关联 GA_Melee / AM-03-05 待修复(输入策略统一;可补充 AM-04 系列宪章)
NV-03-02 GravityScale=0 悬浮时 ABP_Mannequin_Base 动画蓝图除零 🟡 中 控制系统 CQTest 宪章 NV-03-02 待修复(低优先,非阻塞 Warning,动画蓝图加 SafeDivide)
CM-06 瞄准中无方向 Dash 相机缩放与准心闪变(约 0.5s 恢复) 🟡 中 相机 EX-01 关联 CM-03 删除需重审 / AM-03-06 待修复(与 AM-03-06 同源,建议一并验证)
AU-05-01 贴脸时敌人枪声变沉闷(俯视角触发,听感像远处) 🟡 中 音频 EX-01 关联 AU-05 新增(可补宪章用例) 待修复(衰减按角色位置或钳制垂直分量)
EX-01-01 Dash 中触发弹射器时弹射力度被大幅削弱 🟡 中 地图互动 EX-01 B_Launcher_Up/Push / GA_Hero_Dash 待修复(弹射优先级高于 Dash / 先打断再弹射)
EX-01-02 方向弹射器按玩家朝向弹射,而非速度/移动方向 🟡 中 地图互动 EX-01 B_Launcher_Push 待设计确认(若”沿面朝方向”为预期则非 Bug)
EX-01-04 传送门仅下半部分可触发传送,与全高模型不符 🟡 中 地图互动 EX-01 B_Teleport 待修复(碰撞范围与可视模型对齐)
装备动画 装备时武器先显示后播放拿出动作(先出现→消失→再出现) 🟢 一般 武器 手工 Session 1 WA-01 待修复(mesh 显示与 Equip montage Notify 同步)

4. 备注

  • 无 🔴 阻塞级问题,不影响功能封板。
  • 输入策略类问题同源:AM-02-06、AM-03-05、AM-04-01、WF-02-04 均属”进行中动作期间的被拒绝输入”处理不一致(忽略 / 排队 / 打断三种策略混用),建议统一为一种策略后一并回归。
  • Dash 链路问题同源:AM-03-06(无方向 Dash 激活不稳定)、CM-06(瞄准中 Dash 相机闪变)、EX-01-01(Dash 中弹射力度被削)都与 GA_Hero_Dash 激活/根运动流程相关,建议修复时一起验证。
  • EX-01-02(弹射方向)待设计确认;EX-01-03(手雷弹射)为玩法建议,均入 backlog,不阻塞。

Bug 报告

标题: [地图互动] Dash 中触发弹射器时弹射力度被大幅削弱(音效正常)
等级: 🟡 中(非阻塞 · 组合边界场景)


环境: 单机 · 手工探索(探索性测试 EX-01)· 弹射器所在关卡(L_Expanse,含向上/方向两种弹射器;如实际为其他关卡请修正)

步骤:

  1. 在关卡中找到向上弹射器和方向弹射器
  2. 先正常行走/跑动踩上弹射器,记录基准弹射高度/距离(对照组)
  3. 按 Dash(冲刺)进入弹射器触发范围,观察弹射表现与音效
  4. 两种弹射器(向上 / 方向)各重复 2~3 次

实际: Dash 中触发弹射时,弹射音效正常播放,但弹射力度相比正常触发被大幅削弱:角色只有轻微抬升/位移,几乎仍按冲刺轨迹滑行,像是只被”蹭”了一下。

预期: 无论是否处于 Dash 状态,弹射器都应施加完整弹射力度;若设计上希望”Dash 中触发时减弱”,也应与音效反馈一致,而不是听感”弹了”、实际”没弹起来”。

关联: 探索性测试 EX-01(地图互动) / B_Launcher_UpB_Launcher_Push(ShooterCore 蓝图,重叠时 LaunchCharacter) / GA_Hero_Dash(蓝图 GA,根运动 ApplyRootMotionConstantForce + Dash 蒙太奇) / GCNL_Launcher_Activate(弹射音效)


推测: 弹射器在重叠时基于角色当前速度(GetLastUpdateVelocity)计算 LaunchVelocity 并调用 LaunchCharacter;而 Dash 期间角色处于根运动驱动(ApplyRootMotionConstantForce + 根运动蒙太奇),根运动每帧覆盖/重新计算 VelocityLaunchCharacter 写入的弹射速度在下一个移动 tick 即被根运动覆盖,因此弹射力度被削弱至几乎无效。音效走独立的 GameplayCue(GCNL_Launcher_Activate),不受移动结果影响,所以音效正常。

建议方向(供修复参考):在弹射器触发时若检测到角色处于根运动/Dash,可先打断 Dash 再施加弹射;或在 Dash GA 的交互策略中明确”弹射器优先级高于 Dash”(沿用 AM-03-05 的输入策略统一思路)。


Bug 报告

标题: [地图互动] 方向弹射器按玩家朝向弹射,而非速度/移动方向
等级: 🟡 中(非阻塞 · 交互方向与运动方向不一致)


环境: 单机 · 手工探索(探索性测试 EX-01)· 方向弹射器所在关卡(L_Expanse,如实际为其他关卡请修正)

步骤:

  1. 找到方向弹射器
  2. 面朝一个方向(如正北)但向另一方向跑动/侧移(如正东),踩上弹射器
  3. 观察弹射方向;再对照”面朝与移动方向一致”时的弹射方向
  4. 再试倒退跑(面朝弹射器、背向移动方向踩上)

实际: 弹射方向始终是玩家面朝方向,与移动/速度方向无关;侧移、倒退时,弹射方向与运动方向明显不一致,玩家会被推向面朝方向而非前进方向。

预期: 方向弹射应按玩家当前移动/速度方向弹射(或至少与移动输入方向一致),侧移、倒退时弹射方向应与运动方向一致。

关联: 探索性测试 EX-01(地图互动) / B_Launcher_Push(ShooterCore 蓝图)


推测: B_Launcher_Push 的 XY 弹射方向直接取 GetActorForwardVector(面朝方向),未使用角色当前速度方向(GetLastUpdateVelocity / GetLastMovementInputVector),因此仅在”面朝与移动同向”时表现正确,侧移/倒退时弹射方向错误。若产品设计确实要求”沿面朝方向弹射”,则本项降级为设计确认而非 Bug;否则应改用速度/输入方向作为弹射方向。


玩法建议(非 Bug)

标题: [地图互动] 玩法建议:手雷也可触发弹射装置
类型: 玩法建议 · 优先级:低(进 backlog)


背景: 探索性测试 EX-01(地图互动)期间,弹射装置目前只对玩家角色生效;建议让手雷也能触发弹射,形成”弹雷”玩法。

建议内容:

  1. 手雷落在弹射装置触发范围内时,同样被施加弹射力度(对投掷物施加速度/冲量,而非仅对玩家 LaunchCharacter
  2. 衍生玩法:被弹射的手雷保留原有引爆逻辑(定时 / 落地 / 二次引爆),可把雷弹到高处或掩体后
  3. 一致性要求:手雷弹射的方向与力度规则应与玩家一致,避免重蹈 EX-01-01(Dash 中力度被削弱)、EX-01-02(按朝向而非速度方向弹射)的问题
  4. 网络注意:手雷弹射应由服务端权威判定并复制结果,保证客户端表现一致

关联: 探索性测试 EX-01(地图互动) / B_Launcher_UpB_Launcher_Push(弹射装置) / GA_GrenadeB_Grenade(ShooterCore 手雷投掷)


Bug 报告

标题: [地图互动] EX-01-04 传送门仅下半部分可触发传送(有效高度约为人高),与模型全高表现不符
等级: 🟡 中(非阻塞 · 表现与判定不一致)


环境: 单机 · 手工探索(探索性测试 EX-01)· 传送门所在关卡(L_Expanse,如实际为其他关卡请修正)

步骤:

  1. 找到传送门(可视模型为全高门框)
  2. 从下半部分正常走入,确认触发传送(对照组)
  3. 跳跃 / 从高处进入传送门上半部分(高于人高),观察是否触发
  4. 在传送门不同高度(底部、人高附近、门框顶部)各尝试一次并记录

实际: 传送门只有下半部分(约人高以下)能触发传送;上半部分穿过不触发,与可视门框的全高表现不符——看起来”门很大,实际只有半截能用”,玩家跳跃进入上半部分会被视觉误导。

预期: 传送判定范围应与可视模型一致(全高都可触发),或将可视模型缩小到与实际触发范围一致;至少不应让玩家从视觉上产生”全高可通行”的误解。

关联: B_Teleport(ShooterCore 蓝图:BoxComponent 重叠 + K2_TeleportTo) / 传送门视觉参数(TeleporterHeight / TeleporterWidth 材质参数) / 探索性测试 EX-01


推测: B_Teleport 的传送判定依赖 BoxComponent 的碰撞范围,而可视门框高度由 TeleporterHeight / TeleporterWidth 参数(材质/网格)控制,两者没有对齐——Box 高度约等于角色高度(覆盖下半部分),材质模型却绘制成更高的门框。修复方向:让碰撞范围与可视模型一致(以模型为准调整 Box),或缩小模型匹配现有触发范围。


Bug 报告

标题: [控制系统] AM-02-06 滞空时按下蹲被忽略(应触发或排队到落地)
等级: 🟡 中(非阻塞 · 输入策略不一致)


环境: 单机 · 手工探索(发现于探索性测试 EX-01 期间,非地图互动类)· 控制系统

步骤:

  1. 角色站立,按跳跃进入滞空
  2. 滞空期间按下蹲键
  3. 保持到落地,观察滞空期间及落地后的角色状态

实际: 滞空时按下蹲被完全忽略:滞空期间无任何反应,落地后也不会自动蹲下(输入被静默丢弃)。

预期: 与 AM-03-05 已确认的”蹲下排队”策略一致——滞空时按下蹲应排队,落地后自动蹲下;或明确为滞空立即改变姿态。当前”静默忽略”与 Dash 中蹲下可排队的行为矛盾(同一蹲下输入,两种策略)。

关联: 宪章 AM-02(蹲下) / AM-02-04 蹲跳(空中状态相关) / AM-03-05(Dash 中蹲下排队,输入策略不一致) / 探索性测试 EX-01(发现于该轮,归属控制系统)


推测: 蹲下能力在滞空时被激活条件或 BlockTags 拒绝,一次性触发输入被静默丢弃、不排队;而 Dash 中蹲下能排队,是因为按住类(held)输入状态在输入句柄中保留,Dash 结束、BlockTags 移除后被激活(与 AM-03-05 根因同源)。建议统一”被拒绝输入”的策略:排队(落地后执行)或给出明确反馈,不做静默丢弃。


Bug 报告

标题: [控制系统] AM-03-05 Dash 中操作行为不一致(蹲下排队延迟执行 / 切换装备打断 Dash / 射击·跳跃·近战·手雷无效)
等级: 🟡 严重


环境: 单机 · 手工测试(Dash 为蓝图 GA,见宪章 AM-03 决策)· 不限帧率

步骤:

  1. 角色正常站立,按 Dash 键触发冲刺(GA_Hero_Dash
  2. Dash 过程中依次输入:蹲下键、切换装备键(QuickBar)、射击键、跳跃键、近战键、手雷键
  3. 分别观察各输入在 Dash 过程中的表现,以及 Dash 结束后的后续状态

实际: 同类”Dash 中操作”输入被以三种不同策略处理,行为互相矛盾:

输入 实际表现
蹲下 排队:Dash 过程中不生效,Dash 结束后角色自动蹲下(延迟执行)
切换装备 打断:直接中止 Dash,冲刺被打断
射击 / 跳跃 / 近战 / 手雷 忽略:Dash 过程中输入完全无效,Dash 结束后也不补执行

预期: Dash 期间的操作策略应遵循统一的设计语言,而非三种策略混用。参照宪章 AM-03-05 预期”Dash 过程中不能进行其他操作(或可提前取消)”,应明确并统一为以下策略之一:

  • 方案 A:Dash 期间所有输入一律忽略(与现有射击/跳跃/近战/手雷表现一致,蹲下与切装也应忽略)
  • 方案 B:Dash 期间所有输入一律排队,Dash 结束后按序执行(与现有蹲下表现一致)
  • 方案 C:Dash 期间所有输入一律打断 Dash(与现有切换装备表现一致)

关联: 宪章 AM-03-05(Dash 中操作)/ GA_Hero_Dash(蓝图 GA)/ IA_Ability_Dash


推测: Dash 为蓝图 GA(Plugins/GameFeatures/ShooterCore/Content/Game/Dash/GA_Hero_Dash),输入经 Lyra GAS 输入标签分发(ULyraAbilitySystemComponent::ProcessAbilityInput),三种行为很可能来自输入策略差异:

  • 蹲下为按住保持类输入(WhileInputActive / Held 输入),Dash 激活期间被 BlockTags 阻止激活,但 held 输入状态在 InputHeldSpecHandles 中持续保留,Dash 结束、BlockTags 移除后即被激活 → 表现为”排队,Dash 结束后蹲下”;
  • 切换装备走 QuickBar/装备流程,不属于被 BlockTags 阻止的 GA 激活路径,直接取消了 Dash → 表现为”打断”;
  • 射击/跳跃/近战/手雷为一次性触发输入(OnInputTriggered),Dash 期间被 BlockTags 拒绝后输入即丢失、不保留 → 表现为”完全无效”。

建议:在 Dash GA(或 AbilityTagRelationshipMapping)中统一 Dash 期间的输入语义(全部忽略 / 全部排队 / 全部打断),并对被拒绝的触发类输入做丢弃或排队策略的一致处理。


Bug 报告

标题: [控制系统] AM-03-06 无方向 Dash 仅在面向特定 1/4 方向时触发,其余朝向无效
等级: 🟡 严重(非阻塞 · 核心操作在大部分朝向失效)


环境: 单机 · 手工探索(发现于探索性测试 EX-01 期间,归属控制系统)· 多个地图验证(L_Expanse / L_FiringRange_WP 等)

步骤:

  1. 角色静止站立,不按任何方向键
  2. 依次面向不同方向(建议按 45° 步进转一整圈,至少覆盖 4 个象限)
  3. 每种朝向按 Dash 键,观察是否触发冲刺
  4. 换其他地图重复验证,排除地图几何因素

实际: 无方向 Dash 只在面向地图某一 1/4 方向区间时触发;面向其它方向按 Dash 无反应(不冲刺、无动画、无冷却表现)。多个地图验证结果一致,与关卡几何无关。

预期: 按 AM-03-06 定义——角色静止、不按方向键时按 Dash,应朝当前面向方向冲刺,与面向方向无关。

关联: 宪章 AM-03-06(无移动时 Dash) / GA_Hero_Dash(蓝图 GA) / AM-03-05(Dash 输入策略不一致)


推测: GA_Hero_Dash 在无移动输入时通过 GetLastMovementInputVector(静止时为 0)+ BiasForwardMovement 将方向偏置到面朝方向,再经 SelectDirectionalMontage 选择 Dash 蒙太奇(Fwd / Bwd / Left / Right)。问题大概率出在”零输入偏置/方向选择”环节:面朝方向在某些世界象限下,偏置后的向量落入方向判定的无效区间(如象限/点积判断只对某一象限成立),导致选不出合法蒙太奇或激活失败。多个地图一致说明是角色空间/能力逻辑问题,而非关卡因素。需在蓝图里检查 BiasForwardMovementSelectDirectionalMontage 对零输入、各世界朝向的处理。


Bug 报告

标题: [控制系统] AM-04-01 近战中输入射击被忽略(应排队或触发)
等级: 🟡 中(非阻塞 · 输入策略不一致)


环境: 单机 · 手工探索(发现于探索性测试 EX-01 期间,归属控制系统)· 持有武器

步骤:

  1. 持有武器,发动近战(GA_Melee)
  2. 近战动作期间按射击键
  3. 观察近战期间及近战结束后的表现(是否触发、是否排队补发)

实际: 近战中按射击被静默忽略:近战期间无任何反应,近战结束后也不会补发射击(输入丢失)。

预期: 与项目输入策略统一——近战期间射击输入应排队(结束后按序执行)或立即触发;至少不应静默丢弃。参照 AM-03-05(Dash 中射击同样被忽略)与 AM-02-06(滞空蹲被忽略),三处”进行中动作期间的被拒绝输入”应统一处理策略。

关联: GA_Melee(ShooterCore 近战能力) / AM-03-05(Dash 中射击被忽略) / AM-02-06(滞空蹲被忽略) / 探索性测试 EX-01

📌 编号说明:控制系统宪章现有 AM-01~03,本报告新增 AM-04(近战动作)系列;如需入宪章,可补充 AM-04-01”近战中射击输入”用例。


推测: 近战为激活类能力(GA_Melee),激活期间通过 BlockTags 阻止射击能力激活;射击为一次性触发输入,被拒绝后输入即丢失、不保留(与 AM-03-05 中 Dash 期间射击被忽略的机制相同)。而”蹲下”类按住输入因 held 状态保留表现为排队——当前项目里被拒绝输入存在忽略/排队/打断三种处理,建议统一为一种策略(全部排队或全部忽略 + 明确反馈)。


Bug 报告

标题: [音频系统] AU-05-01 贴脸时敌人枪声变沉闷,如同远处枪声
等级: 🟡 中(非阻塞 · 听觉定位/距离信息反馈异常)


环境: 单机 · 手工探索(发现于探索性测试 EX-01 期间,归属音频系统)· 存在敌人开火的场景

触发条件: 摄像机处于俯视角(拉高俯视)时触发;水平视角下表现正常。

步骤:

  1. 让敌人(AI / 另一玩家)开枪
  2. 站在敌人正前方极近距离(贴脸,<1m)听枪声
  3. 逐步后退(约 1m、5m、10m、20m)对比同一枪声的音量与音色
  4. 不同武器(步枪 / 手枪 / 霰弹枪)各测一次
  5. 切换摄像机视角(水平 ↔ 俯视角)在相同横向距离下重复对比

实际: 摄像机为俯视角时,贴脸(横向极近)的敌人枪声反而沉闷、发闷,听感像远处枪声;切换为水平视角后,相同横向距离下枪声恢复正常。问题随摄像机俯仰/高度变化,与横向距离不成单调关系。

预期: 相同横向距离下,枪声听感应与摄像机视角无关:越近越响、高频越清晰;不应出现”俯视角下贴脸反而像远处”的表现。

关联: 音频系统 AU-01~04(现有宪章无距离衰减用例,本报告新增 AU-05 系列) / Content\Audio\Sounds\Weapons\*(Noise-Close/Distant、Punch-Close/Distant/Far 分层资产) / Content\Audio\AttenuationPresets\ATT_Rifle / ATT_Pistol / ATT_Shotgun / 探索性测试 EX-01

📌 编号说明:音频宪章现有 AU-01~04,本报告新增 AU-05(距离衰减/枪声分层);如需入宪章,可补充 AU-05-01”敌人枪声距离衰减”用例。


推测: 触发条件指向”音频距离按听者(摄像机)位置计算”:俯视角时摄像机被拉高,与敌人的三维距离被垂直分量放大——即使横向贴脸,计算距离也很大,于是衰减 + 距离低通(LPF)按远距离处理,听感变沉闷;水平视角时摄像机与敌人横向距离小,表现正常。建议方向:游戏内关键音效(枪声)的距离衰减改用角色位置计算,或对距离做钳制/忽略垂直分量;同时核对 ATT_Rifle / ATT_Pistol / ATT_Shotgun 的 LPF 参数是否放大了这一效应。


Bug 报告

标题: [相机系统] 瞄准时使用无方向 Dash,摄像机出现无意义缩放且准星变化(约 0.5s 后恢复)
等级: 🟡 中(非阻塞 · 视觉异常)


环境: 单机 · 手工探索(发现于探索性测试 EX-01 期间,归属相机系统)· 持有武器并进入瞄准状态

步骤:

  1. 持有武器,进入瞄准状态(ADS)
  2. 静止不按方向键,按 Dash(无方向 Dash)
  3. 观察 Dash 触发瞬间的摄像机缩放与准星变化
  4. 在不同朝向重复(关联 AM-03-06:Dash 触发/不触发两种情况分别观察)
  5. 记录变化时长与恢复情况

实际: 瞄准中按无方向 Dash,摄像机瞬间出现无意义的 FOV 缩放、准星随之变化,约 0.5 秒后恢复瞄准状态。变化表现为”闪变一下又回来”,没有产生有效的冲刺或视觉信息。

预期: 瞄准状态下 Dash 不应改变摄像机与准星;若 Dash 与瞄准确有交互设计,变化应保持一致且有明确反馈。当前”闪变后恢复”属于无意义的视觉抖动。

关联: 相机系统 CM-03(FOV/变焦,被删) / AM-03-06(无方向 Dash 激活不稳定,两者高度相关) / HD-01(准星) / 探索性测试 EX-01

⚠️ 与宪章的冲突:03-相机系统.md 的 CM-03 删除说明称”游戏无瞄准变焦/变焦恢复实现”,但本发现证明瞄准状态存在可观察的相机变焦行为。若 ADS 变焦是预期功能,本报告成立;若 ADS 本不该有变焦,则说明存在隐藏的相机变焦路径,CM-03 的删除决定需要重审。


推测: 无方向 Dash 激活时短暂打断/重入了瞄准(Targeting)状态:Dash 激活触发的标签/能力取消使相机从瞄准 FOV 闪回默认 FOV,随后瞄准状态恢复再切回,形成约半秒的缩放脉冲;准星随 FOV 缩放同步变化。与 AM-03-06(无方向 Dash 激活流程不干净)同源,建议修复 Dash 激活/取消流程时一并验证相机状态。


Bug 报告

标题: [控制系统] NV-03-02 GravityScale=0 悬浮时 ABP_Mannequin_Base 动画蓝图除零
等级: 🟡 中(非阻塞 · LogScript Warning,未崩溃)


环境: 单机 · L_Expanse · CQTest(无人值守低帧率)

步骤:

  1. 运行 CQTest NV_03_02_GravityZero(ControlSystem.spec.cpp)
  2. 测试设置 CharacterMovement->GravityScale = 0 后跳跃
  3. 角色以恒定 Z 速度悬浮(无重力不下落)

实际: ABP_Mannequin_Base::UpdateJumpFallData 每帧输出两次 Divide by zero: Divide_DoubleDouble(LogScript Warning,调用栈:UpdateJumpFallData ← BlueprintThreadSafeUpdateAnimation)。物理引擎未除零、角色正常悬浮、无崩溃。

预期: 角色悬浮(垂直速度恒定)时动画蓝图不应除零。宪章 NV-03-02 风险关注为”除零:物理引擎除零崩溃”——实测物理引擎安全,但动画蓝图层存在除零,属于次要系统对极端输入(无重力悬浮)不健壮。

关联: 宪章 NV-03-02(重力参数验证 a 最小值) / ControlSystem.spec.cpp


推测: UpdateJumpFallData 内存在基于”跳跃高度差/时间差”等量的除法;GravityScale=0 时角色 Z 速度恒定(不衰减),某些差值计算为 0,导致除数 0。建议在动画蓝图中对除数为 0 的情况做保护(如 SafeDivide 或 clamp),或在计算前判断垂直速度是否变化。


Bug 报告

标题: [武器] WF-02-04 射击途中换弹被静默拒绝

等级: 🟡 严重


环境: 单机 · FT_WeaponTest_Main · 不限帧率

步骤:

  1. 持有步枪,按住射击键持续开火
  2. 保持射击的同时按换弹键

实际: 换弹指令被忽略,射击继续正常进行,无换弹动画、弹药不恢复,停止射击换弹动作不执行

预期: 射击被打断→执行换弹(依测试宪章 WF-02-04 定义)

关联: WF-02-01 / WF-02-05


推测: GA_Weapon_Fire 处于 Active 状态时阻挡了 GA_Weapon_Reload_Rifle 的激活请求。换弹 Ability 应具有更高优先级以中断射击。


Bug 报告

标题: [武器] 装备时武器先出现在手中,后播放拿出动画(mesh 显示与 Equip 动画时序错位)

等级: 🟢 一般


环境: 单机 · 武器测试场景(FT_WeaponTest_Main / L_Expanse)· 不限帧率

步骤:

  1. 获得武器(AddItemDefinition(ID_Rifle) 加入背包)
  2. 装备武器(QuickBar 槽位设为 active,触发 Equip)
  3. 观察装备瞬间的角色动画与武器 mesh 显示

实际: 装备瞬间武器 mesh 立即出现在角色手中 → 之后才播放”拿出武器”动画(Equip montage)→ 动画播放期间武器从手中消失 → 动画播完后武器再次出现。整体呈现”先出现 → 消失 → 再出现”的闪烁。

预期: 先播放拿出动画,动画播放到”武器被握住”的节点时武器 mesh 才显示,之后持续保持在手中(一次性出现,无闪烁)。

附加材料:

  • 录像: /Game/Cinematics/Takes/Takes_20260801/Scene_1_10(Take Recorder 录制,可回放观察时序)

关联测试用例:

  • WA-01(武器动画 · 装备武器动画:拾取或切换到武器 → 切换/装备动画播放,角色正确持枪)
  • 该缺陷直接导致 WA-01 预期”角色正确持枪”在装备瞬间不成立(武器先显示→消失→再出现)

推测: 装备流程中 ULyraEquipmentInstance 的装备 actor(B_Pistol / B_Shotgun 等)在 EquipItemmesh 立即设为可见,而 Equip montage(拿出动画)的播放与”武器 attach 到手上”的时机在其后。缺少同步机制:montage 开始前应隐藏 mesh(或延迟 spawn/attach),并在 montage 的动画 Notify(如”握住武器”节点)触发时才显示 mesh。

avatar
Kurami Wan
软件工程专业,游戏爱好者。记录游戏系统分析、测试方法与技术探索。
Follow Me
公告
Java, javascript知识收集更新
目录
  1. 1. LyraStarterGame 测试文档
    1. 1.1. 00-总纲
    2. 1.2. 01-系统测试
    3. 1.3. 02-专项测试
    4. 1.4. 03-实施
    5. 1.5. Bug报告
    6. 1.6. LyraStarterGame 测试总纲
      1. 1.6.1. 测试系统清单(共 9 个;状态系统已移除)
      2. 1.6.2. 测试原则
    7. 1.7. 用例分级清单 — 工作流主清单
      1. 1.7.1. 怎么用
      2. 1.7.2. 分级与工作流映射
      3. 1.7.3. 阶段一 · 每次提交 — ⚪ 回归·单元
      4. 1.7.4. 阶段二 · 每次合并 / 构建 — 🔵 冒烟
      5. 1.7.5. 阶段三 · 每次合并 / 里程碑 — 🟢 回归
        1. 1.7.5.1. 武器与装备(宪章)
        2. 1.7.5.2. 控制(宪章)
        3. 1.7.5.3. 前端页面(宪章)
        4. 1.7.5.4. 初始化链路(宪章)
        5. 1.7.5.5. 事件测试(宪章)
        6. 1.7.5.6. 性能门禁(宪章,发布前执行)
      6. 1.7.6. 阶段四 · 里程碑 — 🟠 验收(手工)
        1. 1.7.6.1. Session 1 核心视觉(武器)
        2. 1.7.6.2. Session 2 移动体验(控制 + 相机)
        3. 1.7.6.3. Session 3 UI 与信息(HUD + 队伍 + 音频)
        4. 1.7.6.4. 事件补充
      7. 1.7.7. 阶段五 · 发布前 — 🟣 探索
      8. 1.7.8. 当前待办(快照)
      9. 1.7.9. 变更记录(取舍决策存档)
        1. 1.7.9.1. 01 武器与装备
        2. 1.7.9.2. 02 控制
        3. 1.7.9.3. 03 相机 | 04 HUD | 05 前端 | 07 队伍
        4. 1.7.9.4. 08 初始化 | 10 事件
    8. 1.8. 🚫 已废弃 13 — 测试辅助 API 设计方案
      1. 1.8.1. 1. 背景与动机
        1. 1.8.1.1. 1.1 问题
        2. 1.8.1.2. 1.2 设计理念
        3. 1.8.1.3. 1.3 架构分层
        4. 1.8.1.4. 1.4 设计目标
      2. 1.8.2. 2. 模块结构
        1. 1.8.2.1. 2.1 模块信息
        2. 1.8.2.2. 2.2 Gameplay 查询特殊归属
        3. 1.8.2.3. 2.3 文件组织
        4. 1.8.2.4. 2.4 编译控制
      3. 1.8.3. 3. 函数库 API 参考
        1. 1.8.3.1. 3.1 ULyraTestInputLibrary — 输入模拟
        2. 1.8.3.2. 3.2 ULyraTestWorldLibrary — 世界查询
        3. 1.8.3.3. 3.3 ULyraTestGameplayLibrary — Gameplay 系统查询
        4. 1.8.3.4. 3.4 ULyraTestUtilityLibrary — 工具函数
      4. 1.8.4. 4. 版本记录
    9. 1.9. 🚫 已废弃 15 — Lua Test DSL 设计方案(基于 LuaMachine)
      1. 1.9.1. 1. 背景与动机
        1. 1.9.1.1. 1.1 现状
        2. 1.9.1.2. 1.2 动机
        3. 1.9.1.3. 1.3 设计目标
      2. 1.9.2. 2. 架构分层
        1. 1.9.2.1. 2.1 三层职责
        2. 1.9.2.2. 2.2 与现有体系的关系
        3. 1.9.2.3. Phase 依赖关系
      3. 1.9.3. 3. 实施阶段
        1. 1.9.3.1. 3.1 Phase 1 — LuaMachine 插件集成与基础设施
        2. 1.9.3.2. 3.2 Phase 2 — Test DSL 语言设计与 Runtime 实现
        3. 1.9.3.3. 3.3 Phase 3 — C++ ↔ Lua 桥接层(手动注册)
        4. 1.9.3.4. 3.4 Phase 4 — 测试运行器与 Automation 集成
        5. 1.9.3.5. 3.5 Phase 5 — 工具链与 CI/CD 集成
        6. 1.9.3.6. 3.6 Phase 6 — 蓝图 → Lua 迁移与用户手册
        7. 1.9.3.7. 3.7 执行模式:PIE vs Automation 双入口
      4. 1.9.4. 4. 模块结构
        1. 1.9.4.1. 4.1 文件组织
        2. 1.9.4.2. 4.2 模块依赖
      5. 1.9.5. 5. 关键设计决策
      6. 1.9.6. 6. 风险与缓解
      7. 1.9.7. 7. 与 UnrealMCP 的协同
      8. 1.9.8. 8. 版本记录
    10. 1.10. 🚫 已废弃 16 — Lua DSL API 参考
      1. 1.10.1. 1. 快速开始
      2. 1.10.2. 2. DSL 语法规范
        1. 1.10.2.1. 2.1 TestSuite 定义
        2. 1.10.2.2. 2.2 Test 定义
        3. 1.10.2.3. 2.3 生命周期钩子
        4. 1.10.2.4. 2.4 参数化测试
        5. 1.10.2.5. 2.5 标签过滤
      3. 1.10.3. 3. 命名空间 API 完整参考
        1. 1.10.3.1. 3.1 ctx.actor — 世界/场景查询与操作
        2. 1.10.3.2. 3.2 ctx.ability — GAS 通用操作
        3. 1.10.3.3. 3.3 ctx.state — 数据层通用原语
        4. 1.10.3.4. 3.4 ctx.asset — 资产操作
        5. 1.10.3.5. 3.5 ctx.input — 输入模拟
        6. 1.10.3.6. 3.6 ctx.cheat — 作弊与状态覆盖
        7. 1.10.3.7. 3.7 ctx.config — 配置
      4. 1.10.4. 4. 顶层方法(保留在 ctx)
        1. 1.10.4.1. 4.1 流程控制
        2. 1.10.4.2. 4.2 断言
        3. 1.10.4.3. 4.3 其他
      5. 1.10.5. 5. Helpers 层 API
        1. 1.10.5.1. 5.1 StateHelper
        2. 1.10.5.2. 5.2 WeaponHelper
        3. 1.10.5.3. 5.3 CombatHelper
        4. 1.10.5.4. 5.4 TeamHelper
        5. 1.10.5.5. 5.5 InventoryHelper
      6. 1.10.6. 6. 调用约定速查
      7. 1.10.7. 7. 类型转换说明
        1. 1.10.7.1. Lua ↔ UE 类型映射
      8. 1.10.8. 8. 内部 API(不对外暴露)
      9. 1.10.9. 9. 已知限制与计划
      10. 1.10.10. 10. 版本记录
    11. 1.11. 17 — Lua 测试框架搭建复盘
      1. 1.11.1. 0. 一句话结论
      2. 1.11.2. 1. 背景与目标(当初为什么搭 Lua 测试框架)
      3. 1.11.3. 2. 实际发生的时间线
      4. 1.11.4. 3. 放弃原因复盘(核心)
        1. 1.11.4.1. 3.1 原因一:对 Lua DSL 设计缺乏深入调研
        2. 1.11.4.2. 3.2 原因二:抽象层设计没有把握,Lua API 层直接连接 C++ 桥接层
        3. 1.11.4.3. 3.3 原因三:Lua 脚本混入大量引擎层逻辑,未实现”简化测试”的初衷
        4. 1.11.4.4. 3.4 工程事实(加速决策的最后一根稻草)
      5. 1.11.5. 4. 根源分析(为什么会出现这个结果)
      6. 1.11.6. 5. 做对了什么(保留价值)
      7. 1.11.7. 6. 如果重来:三条可执行建议
      8. 1.11.8. 7. 结论与当前状态
        1. 1.11.8.1. 当前测试策略(决策,见 00-测试总纲.md)
        2. 1.11.8.2. Lua 框架资产状态
      9. 1.11.9. 8. 证据索引
    12. 1.12. Lyra 测试总结报告
      1. 1.12.1. 1. 结论
      2. 1.12.2. 2. 自动化结果(96/96)
      3. 1.12.3. 3. 手工与视觉验证
      4. 1.12.4. 4. 测试侧修复与硬化(本周期)
      5. 1.12.5. 5. 已知问题(随封板发布,无 🔴 阻塞)
      6. 1.12.6. 6. 已知缺口 / 封板后 backlog
      7. 1.12.7. 7. 交付物与记录
    13. 1.13. 武器与装备系统 — 测试宪章
      1. 1.13.1. 测试范围
      2. 1.13.2. 测试计划
        1. 1.13.2.1. 详细分配
      3. 1.13.3. 测试细则
        1. 1.13.3.1. 1. 武器装备(5 个用例,含 5 个子状态)w
          1. 1.13.3.1.1. 切换动作
            1. 1.13.3.1.1.1. EQ-01 初始武器状态
            2. 1.13.3.1.1.2. EQ-02 武器切换
          2. 1.13.3.1.2. 装备动作
        2. 1.13.3.2. 2. 武器功能(2 个用例,含 8 个子状态)
          1. 1.13.3.2.1. 射击测试
            1. 1.13.3.2.1.1. WF-01 射击
          2. 1.13.3.2.2. 换弹测试
            1. 1.13.3.2.2.1. WF-02 换弹
        3. 1.13.3.3. 3. 武器数值(4 个用例,含 15 个子状态)— 自动化测试
          1. 1.13.3.3.1. WS-01 距离衰减参数验证(有效区间 [0, 1])
          2. 1.13.3.3.2. WS-02 射速参数验证(有效区间 [0, 1.0],项目 fireDelayTimeSecs=0.1)
          3. 1.13.3.3.3. WS-03 弹药量参数验证(以步枪标配 30 发弹匣为例,有效区间 [0, 30])
          4. 1.13.3.3.4. WS-04 部位伤害倍率验证(有效区间 [0, 10])
        4. 1.13.3.4. 4. 武器动画(3 个用例)
    14. 1.14. 控制系统 — 测试宪章
      1. 1.14.1. 测试范围
      2. 1.14.2. 测试计划
        1. 1.14.2.1. 详细分配
      3. 1.14.3. 1. 基础移动(3 个用例,含 7 个子状态)
        1. 1.14.3.0.1. MV-01 方向移动
        2. 1.14.3.0.2. MV-02 ⚠️ 停止移动
        3. 1.14.3.0.3. MV-03 移动中转向
    15. 1.14.4. 2. 高阶移动(3 个用例,含 16 个子状态)
      1. 1.14.4.0.1. AM-01 跳跃
      2. 1.14.4.0.2. AM-02 蹲下
      3. 1.14.4.0.3. AM-03 Dash 🖐️ 手工测试(决策:Dash 为蓝图 GA,自动化成本高,转手工)
  2. 1.14.5. 3. 视角控制 🖐️ 手工测试(1 个用例,含 4 个子状态)
    1. 1.14.5.0.1. LC-01 视角控制
  • 1.14.6. 4. 动画测试 🖐️ 手工测试(1 个用例,含 5 个子状态)
    1. 1.14.6.0.1. AN-01 移动动画验证
  • 1.14.7. 5. 数值测试 🤖/🖐️ 边界值分析(5 个参数组,含 19 个子状态)
    1. 1.14.7.0.1. NV-01 跳跃速度参数验证(有效区间 [0, 420],项目默认值)
    2. 1.14.7.0.2. NV-02 Dash 冷却参数验证 🖐️ 手工测试(有效区间 [0, 30],项目值见 GE_HeroDash_Cooldown)
    3. 1.14.7.0.3. NV-03 重力参数验证(有效区间 [0, 10])
    4. 1.14.7.0.4. NV-04 视角灵敏度参数验证 🖐️ 手工测试(有效区间 [0.01, 5.0])
    5. 1.14.7.0.5. NV-05 MaxWalkSpeed 参数验证 🤖 CQTest(有效区间 [0, 600],项目默认值)
  • 1.15. 相机系统 — 测试宪章
    1. 1.15.1. 测试范围
    2. 1.15.2. 测试计划
      1. 1.15.2.1. 详细分配
    3. 1.15.3. 1. 第三人称跟随(1 个用例,含 4 个子状态)
      1. 1.15.3.0.1. CM-01 第三人称跟随
  • 1.15.4. 2. 碰撞避免(1 个用例,含 4 个子状态)
    1. 1.15.4.0.1. CM-02 碰撞避免
  • 1.15.5. 3. FOV 与变焦 — 🚫 已删
  • 1.15.6. 4. 模式切换与过渡 — 🚫 已删
  • 1.15.7. 5. 蹲下偏移(1 个用例,含 2 个子状态)
    1. 1.15.7.0.1. CM-05 蹲下偏移
  • 1.15.8. 6. 数值测试 🤖 自动化测试(2 个用例,含 9 个子状态)
    1. 1.15.8.0.1. NV-01 FOV 参数验证(有效区间 [5.0, 170.0],项目 clamp 值)
    2. 1.15.8.0.2. NV-02 俯仰限幅参数验证(有效区间 [-89.0, 89.0],项目定义值)
    3. 1.15.8.0.3. NV-03 混合时间参数验证 — 🚫 已删
  • 1.16. HUD — 测试宪章
    1. 1.16.1. 测试范围
    2. 1.16.2. 测试计划
      1. 1.16.2.1. 详细分配
    3. 1.16.3. 1. 准星(1 个用例,含 5 个子状态)
      1. 1.16.3.0.1. HD-01 准星显示
  • 1.16.4. 2. 命中标记(1 个用例,含 5 个子状态)
    1. 1.16.4.0.1. HD-02 命中标记
  • 1.16.5. 3. 伤害数字(1 个用例,含 4 个子状态)
    1. 1.16.5.0.1. HD-03 伤害数字
  • 1.16.6. 4. HUD 布局(1 个用例,含 3 个子状态)
    1. 1.16.6.0.1. HD-04 HUD 布局
  • 1.16.7. 5. 游戏内信息(1 个用例,含 5 个子状态)
    1. 1.16.7.0.1. HD-05 游戏内信息
  • 1.17. 前端页面 — 测试宪章
    1. 1.17.1. 测试范围
    2. 1.17.2. 测试计划
      1. 1.17.2.1. 详细分配
    3. 1.17.3. 1. 加载动画 — 🚫 已删
    4. 1.17.4. 2. 主菜单(1 个用例,含 5 个子状态)
      1. 1.17.4.0.1. FE-02 主菜单
  • 1.17.5. 3. 设置界面(1 个用例,含 4 个子状态)
    1. 1.17.5.0.1. FE-03 设置界面
  • 1.17.6. 4. 确认弹窗(1 个用例,含 3 个子状态)
    1. 1.17.6.0.1. FE-04 确认弹窗
  • 1.17.7. 5. 加载画面(1 个用例,含 3 个子状态)
    1. 1.17.7.0.1. FE-05 加载画面
  • 1.17.8. 6. ESC 暂停菜单(1 个用例,含 3 个子状态)
    1. 1.17.8.0.1. FE-06 暂停菜单
  • 1.18. 音频系统 — 测试宪章
    1. 1.18.1. 测试范围
    2. 1.18.2. 测试计划
      1. 1.18.2.1. 详细分配
    3. 1.18.3. 1. 音量设置(1 个用例,含 5 个子状态)
      1. 1.18.3.0.1. AU-01 音量设置
  • 1.18.4. 2. 音频输出(1 个用例,含 2 个子状态)
    1. 1.18.4.0.1. AU-02 音频输出
  • 1.18.5. 3. HDR 音频(1 个用例,含 2 个子状态)
    1. 1.18.5.0.1. AU-03 HDR 音频
  • 1.18.6. 4. 加载画面音频(1 个用例,含 2 个子状态)
    1. 1.18.6.0.1. AU-04 加载画面音频
  • 1.18.7. 5. 数值测试 🤖 自动化测试(2 个用例,含 5 个子状态)
    1. 1.18.7.0.1. NV-01 音量参数验证(有效区间 [0, 1.0])
    2. 1.18.7.0.2. NV-02 各通道音量独立调节
  • 1.19. 队伍系统 — 测试宪章
    1. 1.19.1. 测试范围
    2. 1.19.2. 测试计划
      1. 1.19.2.1. 详细分配
    3. 1.19.3. 1. 团队归属(1 个用例,含 4 个子状态)
      1. 1.19.3.0.1. TM-01 团队归属
  • 1.19.4. 2. 友军识别(1 个用例,含 3 个子状态)
    1. 1.19.4.0.1. TM-02 友军识别
  • 1.19.5. 3. 团队显示(1 个用例,含 3 个子状态)
    1. 1.19.5.0.1. TM-03 团队显示
  • 1.19.6. 4. 团队标签(1 个用例)
    1. 1.19.6.0.1. TM-04 团队标签
  • 1.19.7. 5. 战绩统计(1 个用例,含 3 个子状态)
    1. 1.19.7.0.1. TM-05 战绩统计
  • 1.19.8. 6. 数值测试 — 🚫 已删
  • 1.20. 初始化链路系统 — 测试宪章
    1. 1.20.1. 测试范围
    2. 1.20.2. 测试计划
      1. 1.20.2.1. 详细分配
    3. 1.20.3. 1. 游戏启动(1 个用例,含 3 个子状态)
      1. 1.20.3.0.1. IN-01 游戏启动
  • 1.20.4. 2. 资源生成(1 个用例,含 6 个子状态)
    1. 1.20.4.0.1. IN-02 资源生成
  • 1.20.5. 3. 重生(1 个用例,含 2 个子状态)
    1. 1.20.5.0.1. IN-03 重生
  • 1.20.6. 4. 输入绑定(1 个用例,含 2 个子状态)
    1. 1.20.6.0.1. IN-04 输入绑定
  • 1.20.7. 5. 断线重连(1 个用例,含 3 个子状态)
    1. 1.20.7.0.1. IN-05 断线重连
  • 1.20.8. 6. 数值边界 🤖 自动化测试(1 个用例,含 3 个子状态)
    1. 1.20.8.0.1. NV-01 重生延迟参数验证(有效区间 [0, 60])
  • 1.21. 状态系统 — 测试宪章
    1. 1.21.1. 测试范围
    2. 1.21.2. 测试计划
      1. 1.21.2.1. 详细分配
    3. 1.21.3. 1. 生命属性(1 个用例,含 4 个子状态)
      1. 1.21.3.0.1. ST-01 生命属性
  • 1.21.4. 2. 死亡状态机 🖐️(1 个用例,含 3 个子状态)
    1. 1.21.4.0.1. ST-02 死亡状态机
  • 1.21.5. 3. 数值测试 🤖 自动化测试(2 个用例,含 7 个子状态)
    1. 1.21.5.0.1. NV-01 初始血量参数验证(有效区间 [0, MaxHP])
    2. 1.21.5.0.2. NV-02 最大血量参数验证(有效区间 [1, 10000])
  • 1.22. 事件测试 — 测试宪章
    1. 1.22.1. 测试范围
    2. 1.22.2. 测试计划
      1. 1.22.2.1. 详细分配
    3. 1.22.3. 1. 死亡
      1. 1.22.3.1. 设计依据
      2. 1.22.3.2. 1.1 死亡状态机与移动冻结 (1 个用例,含 5 个子状态)
        1. 1.22.3.2.1. DT-01 死亡状态机与移动冻结
      3. 1.22.3.3. 1.2 死亡摄像机 (1 个用例,含 1 个子状态;DT-02-01 已并入 DT-01-05)
        1. 1.22.3.3.1. DT-02 死亡摄像机
      4. 1.22.3.4. 1.3 死亡状态标签 (1 个用例,含 3 个子状态)
        1. 1.22.3.4.1. DT-03 死亡状态标签
      5. 1.22.3.5. 1.4 网络同步 (1 个用例,含 2 个子状态) — 暂缓
        1. 1.22.3.5.1. DT-04 死亡网络同步
      6. 1.22.3.6. 1.5 边界情况 (1 个用例,含 4 个子状态;DT-05-05 已删)
        1. 1.22.3.6.1. DT-05 死亡边界情况
      7. 1.22.3.7. 2. 重生
      8. 1.22.3.8. 设计依据
      9. 1.22.3.9. 2.1 重生后完整性 (1 个用例,含 6 个子状态)
        1. 1.22.3.9.1. DT-06 重生后完整性
      10. 1.22.3.10. 2.2 边界情况 (1 个用例,含 1 个子状态;DT-07-02/03 已删)
        1. 1.22.3.10.1. DT-07 重生边界情况
    4. 1.22.4. 3. 散布系统
      1. 1.22.4.1. 设计依据
      2. 1.22.4.2. 3.1 热量累积与冷却
      3. 1.22.4.3. 3.2 散布与热量映射
      4. 1.22.4.4. 3.3 散布修正乘数
      5. 1.22.4.5. 3.4 首发精度
      6. 1.22.4.6. 3.5 散布恢复冷却延迟
      7. 1.22.4.7. 3.6 散布曲线边界值
  • 1.23. 性能测试总结报告:Trace 分析轮次
    1. 1.23.1. 1. 测试概况
    2. 1.23.2. 2. 测试环境
    3. 1.23.3. 3. 方法
    4. 1.23.4. 4. 主要发现
      1. 1.23.4.1. 4.1 性能现状
      2. 1.23.4.2. 4.2 真问题(P1,可复现)
      3. 1.23.4.3. 4.2b GC 卡顿证据(P1)
      4. 1.23.4.4. 4.2c GC 定位:内部阶段拆分(两轮一致)
      5. 1.23.4.5. 4.3 上下文相关(P2)
      6. 1.23.4.6. 4.4 排除项(非问题标记)
    5. 1.23.5. 5. 结论
    6. 1.23.6. 6. 数据与文档档案
    7. 1.23.7. 7. 打包版复测
      1. 1.23.7.1. 7.1 环境与方法
      2. 1.23.7.2. 7.2 GC:PIE 放大坐实,降级 P2
      3. 1.23.7.3. 7.3 新发现:画质切换触发 PSO 编译冻结(操作关联,P2)
      4. 1.23.7.4. 7.4 新发现:首次打开设置菜单 653ms(操作关联)
      5. 1.23.7.5. 7.5 待定位
      6. 1.23.7.6. 7.6 体积云:打包版 CPU 侧无大成本,GPU 侧待确认
      7. 1.23.7.7. 7.7 结论更新
  • 1.24. 探索性测试
    1. 1.24.1. 测试范围
    2. 1.24.2. 记录方式
  • 1.25. EX-01 探索性测试宪章 — 地图互动效果
  • 1.26. EX-01 会话记录 — 地图互动效果
    1. 1.26.1. 要素清单
    2. 1.26.2. 要素×关系表
    3. 1.26.3. 覆盖清单
    4. 1.26.4. 相关发现(同日登记在系统测试类别)
  • 1.27. EX-01 探索小结 — 地图互动效果
    1. 1.27.1. 发现列表
    2. 1.27.2. 最有价值的发现
    3. 1.27.3. 系统性规律
    4. 1.27.4. 下轮方向
    5. 1.27.5. 遗留 backlog
  • 1.28. 蓝图功能测试实战:WF-01-01 单发武器射击
    1. 1.28.1. 前置:打开参考资产
    2. 1.28.2. 第一步:创建测试地图
    3. 1.28.3. 第二步:创建测试蓝图
    4. 1.28.4. 第三步:声明变量
    5. 1.28.5. 第四步:编写 OnPrepareTest
    6. 1.28.6. 第五步:编写 IsReady
    7. 1.28.7. 第六步:编写 StartTest
      1. 1.28.7.1. Input Key 节点详解
    8. 1.28.8. 第七步:编译、放置、测试
      1. 1.28.8.1. 编译
      2. 1.28.8.2. 放置到地图
      3. 1.28.8.3. 运行测试
    9. 1.28.9. 常见问题排查
    10. 1.28.10. 扩展:WF-01-02 连发射击
    11. 1.28.11. 核心要点总结
  • 1.29. Bug 清单(汇总)
    1. 1.29.1. 1. 按系统分布
    2. 1.29.2. 2. 按来源分布
    3. 1.29.3. 3. 清单
    4. 1.29.4. 4. 备注
    5. 1.29.5. Bug 报告
    6. 1.29.6. Bug 报告
    7. 1.29.7. 玩法建议(非 Bug)
    8. 1.29.8. Bug 报告
    9. 1.29.9. Bug 报告
    10. 1.29.10. Bug 报告
    11. 1.29.11. Bug 报告
    12. 1.29.12. Bug 报告
    13. 1.29.13. Bug 报告
    14. 1.29.14. Bug 报告
    15. 1.29.15. Bug 报告
    16. 1.29.16. Bug 报告
    17. 1.29.17. Bug 报告
  • 最新文章
    网站资讯
    文章数目 :
    12
    本站总字数 :
    31.9k
    本站访客数 :
    本站总访问量 :
    最后更新时间 :