LyraStarterGame 测试文档
本文档来自 LyraStarterGame(UE 5.8)项目的完整测试记录(TestDocs),随博客文章《对UE的教学项目进行测试实录》一起发布。目录可点击跳转。
00-总纲
- 00-测试总纲
- 用例分级清单
- 🚫 已废弃 13-测试辅助API设计方案
- 🚫 已废弃 15-UnLuaTestDSL设计方案
- 🚫 已废弃 16-LuaDSL-API参考
- 17-Lua测试框架复盘
- 20-测试总结报告
01-系统测试
02-专项测试
03-实施
Bug报告
- 00-Bug清单
- EX-01-01-弹射器Dash中触发弹射力度被削弱
- EX-01-02-方向弹射器按玩家朝向弹射而非速度方向
- EX-01-03-玩法建议-手雷可触发弹射装置
- EX-01-04-传送门仅下半部分可触发传送
- AM-02-06-滞空时按下蹲被忽略
- AM-03-05-Dash中操作行为不一致
- AM-03-06-无方向Dash仅在特定朝向触发
- AM-04-01-近战中输入射击被忽略
- AU-05-01-贴脸时敌人枪声变沉闷
- CM-06-瞄准时无方向Dash相机缩放与准心闪变
- NV-03-02-GravityScale0悬浮时动画蓝图除零
- WF-02-04-射击途中拒绝换弹指令(非阻塞)
- 装备动画-武器先显示后播放拿出动作
LyraStarterGame 测试总纲
本文件记录测试系统的划分、范围定义和讨论结论。
📌 当前状态:自动化 CQTest 96/96 全绿,手工 Session 1~4 完成,无 🔴 阻塞 Bug,
已按”功能封板”口径封板。详见《Lyra 测试总结报告》。
🚫 决策:状态系统(09)不需要,已从测试范围移除(血量/死亡相关验证并入事件测试 DT-01)。
测试系统清单(共 9 个;状态系统已移除)
| 编号 | 系统名称 |
|---|---|
| 01 | 武器与装备系统 |
| 02 | 控制系统 |
| 03 | 相机系统 |
| 04 | HUD |
| 05 | 前端页面 |
| 06 | 音频系统 |
| 07 | 队伍系统 |
| 08 | 初始化链路系统 |
| 10 | 性能测试 |
测试原则
- 黑盒测试为主 — 除非明确标注为自动化测试,否则一律从玩家视角描述
- 不单独测 GAS — Gameplay Ability System 不作为独立系统,相关功能归入各系统内部细则
- 网络测试 — 多线程/复制场景作为每个系统的细则考虑,不设单独模块
- 边界值测试 — 数值测试包含 0 / 负值 / 正常值 三种边界情况
- 测试方式选择(决策) — 仅两种方式:
- C++ CQTest:重复性高的用例(数值边界、状态流转、输入模拟、回归验证)
- 手工测试:其余需要视觉/手感/听感判断的用例
- 弃用蓝图功能测试(FunctionalTest 蓝图)— 复杂逻辑在蓝图中难以编写和维护,效率低,回归 C++ CQTest 实现
用例分级清单 — 工作流主清单
现改为按 分级 → 工作流阶段 组织的可勾选清单:每个用例一行,勾掉的项是已完成的,未勾的项就是当前阻塞 / 待办。
取舍决策的完整存档移入文末 变更记录,不再作为正文。
📌 基线:自动化 CQTest 96/96 全绿,手工 Session 1~4 完成,无 🔴 阻塞 Bug。下方勾选状态即该快照,后续以本清单推进。
怎么用
- 分级 = 工作流阶段:⚪(每次提交)→ 🔵(每次合并 / 构建)→ 🟢(合并 / 里程碑)→ 🟠(里程碑)→ 🟣(发布前)。
- 对应阶段触发时,逐项执行并勾掉通过的用例;未勾的项就是当前阻塞。
- 与 测试工作流清单 配合使用:本清单回答”每个用例属于哪一级、在哪实现、当前状态”;工作流清单回答”什么时机跑哪些用例”。
- 状态图例:✅ 已通过 | ⏳ 待执行 / 排期 | 🔴 阻塞 | 🚫 已并入 / 删除(见变更记录)。
分级与工作流映射
| 分级 | 触发时机 | 失败意味着 | 谁执行 |
|---|---|---|---|
| ⚪ 回归·单元 | 每次提交 / 构建 | 数据 / 配置被改坏 | 开发(可挂 pre-commit) |
| 🔵 冒烟 | 每次合并后 | 环境或基础链路断了(停止后续) | CI |
| 🟢 回归 | 每次合并 / 里程碑 | 功能被改坏 | CI / QA |
| 🟠 验收 | 里程碑 / 发布前 | 体验不达标,不能发版 | QA |
| 🟣 探索 | 验收通过后 / 发布前 | 未知组合问题,需回流 | QA |
阶段一 · 每次提交 — ⚪ 回归·单元
改动涉及哪个模块就勾哪组;全量可挂 pre-commit。落点均为单元级断言(NewObject / LoadObject,不走 PIE)。
- 武器数值:WS-01 距离衰减、WS-02 射速配置、WS-03 弹匣容量读取、WS-04-01/02/04 倍率校验|
WeaponSystem_Numerics.spec.cpp✅ - 控制数值:NV-01-01/02 / NV-03-01/02 / NV-05-01/02 clamp 校验|
ControlSystem.spec.cpp✅ - 相机数值:NV-01 / NV-02 FOV clamp / 俯仰限幅|
CameraSystem_Numeric.spec.cpp✅ - 音频数值:NV-01 / NV-02 音量边界 / 通道独立|
AudioSystem_Numeric.spec.cpp✅ - 事件数值:SP-01-03 / SP-02-01 / SP-05-03 / SP-06-01 除零与 clamp 风险点|
SpreadSystem_Numeric.spec.cpp✅
注: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-01
12 帧时间 / 内存 / 网络延迟|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-01
03/-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 | C++ (LyraTestUtilities / LyraGame) 蓝图 (FunctionalTest + ECF) |
1.3 架构分层
| 层 | 负责 | 技术方案 |
|---|---|---|
| 流程控制 | 循环、等待、条件判断 | EnhancedCodeFlow(While True Execute、Delay、协程) |
| Lyra 专有查询 | 弹药/血量/武器/标签 | LyraTestGameplayLibrary(LyraGame 内部) |
| 输入模拟 | 注入增强输入 | LyraTestInputLibrary |
| 世界/工具 | 获取 Actor、传送、移动 | LyraTestWorldLibrary / UtilityLibrary |
1.4 设计目标
- 最小侵入 — 不修改 LyraGame 核心逻辑,不碰已有头文件
- 蓝图优先 — 所有函数标记
BlueprintCallable,以Lyra|Test|分类出现 - 只读优先 — 查询操作不修改游戏状态
- 条件编译 — 仅在非 Shipping 构建中编译
2. 模块结构
2.1 模块信息
| 属性 | 值 |
|---|---|
| 模块名 | LyraTestUtilities |
| 类型 | Runtime(仅非 Shipping 构建) |
| 路径 | Source/LyraTestUtilities/ |
| 依赖 | Core Engine EnhancedInput AIModule |
2.2 Gameplay 查询特殊归属
ULyraTestGameplayLibrary 位于 LyraGame 内部(非 LyraTestUtilities),原因:
- 需要访问 Lyra 内部类(
ULyraQuickBarComponent、ULyraInventoryItemInstance等) - 这些类没有
LYRAGAME_API导出宏,外部模块无法链接 - 作为 LyraGame 的新建文件存在,不修改已有代码
路径:
Source/LyraGame/Public/TestUtilities/LyraTestGameplayLibrary.hSource/LyraGame/Private/TestUtilities/LyraTestGameplayLibrary.cpp
2.3 文件组织
1 | Source/LyraTestUtilities/ |
2.4 编译控制
在 LyraGame.Target.cs 的 ApplySharedLyraTargetSettings() 中条件添加:
1 | if (!bIsShipping && !bIsTest) |
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 | While True Execute |
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 | While True Execute (ECF) |
等待条件示例(用 ECF 协程):
1 | ECF Coroutine: |
注意事项:
- 需要玩家 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 | ┌──────────────────────────────────────┐ |
- Lua DSL(80%):大部分功能测试、集成测试、回归测试 → 热重载、文本 diff、CI 友好
- C++ CQTest(20%):需要编译期检查的底层单元测试、纯数值/数学验证
- 蓝图:不再推荐用于编写新测试;已有蓝图测试逐步迁移到 Lua
1.3 设计目标
- Lua 为主力 — 80% 测试用 Lua DSL 编写,蓝图不再推荐用于新测试
- 复用已有 C++ 轮子 — 通过 LuaMachine 手动注册绑定调用 LyraTestUtilities 全部 API(12 个函数,胶水代码量可控)
- DSL 声明式 + 简洁 — 测试用例结构清晰,接近自然语言,降低 Lua 学习成本
- 热重载 — 修改
.lua文件后lua.reload()即可生效 - 统一测试入口 — 注册到 UE Automation Framework,与 CQTest 同一面板
- CI 友好 — 命令行可执行、JSON 报告输出、非零退出码
- 零侵入 — 作为独立插件存在,不影响 LyraGame 核心逻辑
2. 架构分层
1 | ┌──────────────────────────────────────────────────────────┐ |
2.1 三层职责
| 层 | 负责 | 技术 |
|---|---|---|
| 测试编写层 | 用户编写测试用例 | .lua 文本文件(DSL)为主 + .spec.cpp(CQTest 底层) |
| LuaMachine 桥接层 | Lua 调用 UE C++ API | LuaMachine 手动注册 + ULuaTestBridge 薄封装 |
| C++ 轮子层 | 核心测试能力 | LyraTestUtilities + CQTest + Automation Framework |
2.2 与现有体系的关系
1 | 现有(即将被替代):C++ → LyraTestUtilities → BP FunctionalTest → ECF(流程控制) |
替代路线:
🔄 新测试 → 一律用 Lua DSL 编写(除非需要编译期检查 → CQTest)
🔄 已有 BP FunctionalTest → 按优先级逐步迁移到 Lua DSL(Phase 6)
🔄 蓝图仅保留作为 Lua DSL 尚不支持的兜底方案
不碰:
❌ LyraGame 核心逻辑
❌ CQTest 已有测试(C++ 底层测试保持不动)
Phase 依赖关系
1 | Phase 1 (LuaMachine 集成) ──┬──→ Phase 2 (DSL Runtime) ──┬──→ Phase 4 (Automation 集成) |
- 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表达式、复合赋值等
- 每个需要暴露给 Lua 的 C++ 函数需要在派生类中通过
- 编写 Smoke 验证用例:
- 创建
ULuaTestRunnerActor,在BeginPlay中创建 Lua 状态并执行测试脚本 - Lua 侧调用 C++ 注册的
PrintString输出日志
- 创建
- 验证热重载:通过自定义控制台命令
lua.reload重新加载脚本后执行新代码 - 编写
ULuaTestBridgeC++ 基类,作为所有 Lua 测试的入口点。提供:static void RegisterAll(LuaState)— 在所有模式下注册 C++ API 到 LuaULuaTestRunner(派生 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 | local suite = TestSuite("武器系统", { |
验收标准:
| 编号 | 验收项 | 验证方式 |
|---|---|---|
| ✅ 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 内部 |
| 维护前景 | 已停滞 | 活跃维护 |
工作项:
- 编写
ULuaTestBridgeC++ 类(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↔ LuastringFGameplayTag↔ LuastringTSubclassOf<AActor>↔ Luastring(资产路径)- UObject 指针 → Lua 侧用整数 handle 或轻量 userdata
- 实现 Latent Action 协程桥接:
ctx:Wait(seconds)→ Luacoroutine.yield+ C++FTimerHandleresumectx:WaitFrames(n)→ Luacoroutine.yield+ C++FTickableGameObject计数 resumectx: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_FireWeapon、FT_WF_01_01_SingleShot - 第二批:中等复杂度 —
WF_01_02_MultiShot、B_Test_AutoRun - 第三批:复杂集成测试 — 动画测试、网络复制测试
- 第一批(Phase 6 内完成):简单 Smoke 测试 —
- 编写《蓝图 → 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 | Content/Scripts/Tests/Weapon/single_shot_spec.lua |
GameFeature 懒加载处理策略:
ShooterTests 及其他 GameFeature 插件默认处于 Registered 状态(不自动激活)。Lua 测试在执行前需要确保依赖的 GameFeature 已加载:
- PIE 模式:
ULuaFunctionalTest放置在 ShooterTests 关卡中时,关卡本身会触发 Experience 加载,GameFeature 自动激活。重写IsReady()等待 GameFeatureActive状态。 - Automation 模式:
FLuaTestAutomationProvider在RunTest()前调用UGameFeaturesSubsystem::LoadAndActivateGameFeaturePlugin()显式激活依赖的 GameFeature。每个TestSuite通过options.depends_on = {'ShooterCore', 'ShooterTests'}声明所依赖的 GameFeature 插件名。 - 超时保护:GameFeature 加载超时(默认 10 秒),超时后跳过依赖该 Feature 的测试并标记为 Skipped。
4. 模块结构
4.1 文件组织
1 | LyraStarterGame/ |
4.2 模块依赖
1 | LuaTestFramework (C++ Plugin) |
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 加载完成;由 ULuaFunctionalTest 的 IsReady 保证 |
| Lua 热重载在运行中的协程行为不确定 | 🟡 中 | Phase 1 充分验证;热重载仅在测试未运行状态下生效;运行中的测试不受影响 |
7. 与 UnrealMCP 的协同
本项目已集成 UnrealMCP v0.9.1(Plugins/UnrealMCP/),其 AutomationTestToolset 可发现和执行 UE Automation 测试。
Lua Test DSL 注册到 Automation Framework 后:
1 | AI Agent (Reasonix/Copilot/Cursor) |
这意味着:所有现有 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 | -- Content/Scripts/Tests/Weapon/default_weapon_spec.lua |
调用约定:每个 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 | function suite:test_<name>(ctx:TestContext) |
- 方法名
test_xxx自动识别为测试用例 - 不以
test_开头的方法不会被当作测试执行 - 所有
test_*方法按声明顺序执行
2.3 生命周期钩子
1 | function suite:before_all(ctx) end -- Suite 开始时执行一次 |
执行顺序:
1 | before_all |
2.4 参数化测试
1 | function suite:test_each_reload(ctx) |
2.5 标签过滤
1 | -- 定义时打标签 |
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 | ctx:expect(1 + 1):to_equal(2) |
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 | -- 获取玩家控制器/Pawn |
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 | local loc = ctx.actor:GetActorLocation(actor) |
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-*.md 和 docs/adr/0002-*.md。
17 — Lua 测试框架搭建复盘
状态:复盘完成
结论:Lua(LuaMachine)测试框架方案已放弃,测试体系回归 C++ CQTest + 手工测试。
本文档是 15-UnLuaTestDSL设计方案、16-LuaDSL-API参考、docs/adr/0001-*/0002-*的收尾记录,仅作历史参考。
0. 一句话结论
在 LyraStarterGame 中用 Lua 搭建测试框架的尝试,最终在决策窗口终止。放弃的直接原因是:
- 对 Lua DSL 设计缺乏深入调研 —— 没有先弄清”DSL 要隐藏什么、暴露什么、写给谁”,就进入了实现;
- 对抽象层设计没有把握 —— Lua API 层直接连接 C++ 桥接层,没有形成有语义厚度的抽象;
- 测试中的 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-测试总纲.md、docs/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 | -- 透传:Lua 命名空间 = C++ 桥接函数换个名字 |
后果:
- 约定为内部 API 的
_Call / GetProperty / SetProperty / FindComponent被各命名空间普遍使用,测试可见路径上到处是字符串函数名、类名字符串; - 引擎概念原样穿过抽象层:魔法数字(
ECC_WorldStatic = 0、IE_Pressed = 1、IE_Axis = 2)、硬编码 Tag("Lyra.ShooterGame.Weapon.MagazineAmmo")、console 命令字符串("cheat SetHealth %f"); - API 表面远大于实现:多个命名空间函数是 stub(返回 0 / nil / 空表),”抽象层”实际是”未完成的桥接层的门面”。
本质:抽象层不是”把 C++ 函数包一层”,而是”把引擎概念消化成领域语义”。这一步没有做,抽象层就等于零。
3.3 原因三:Lua 脚本混入大量引擎层逻辑,未实现”简化测试”的初衷
现象:测试脚本要求编写者具备完整的 UE 引擎知识,复杂度并不比 C++ 低:
single_shot_spec.lua 的 before_each 需要 15+ 行引擎层准备逻辑:
1 | function suite:before_each(ctx) |
Helper 层也没有把抽象下沉到业务语义,而是用引擎概念组合:
1 | -- StateHelper / WeaponHelper 内部: |
结论:编写者需要同时懂 Lua、UE 反射体系、Lyra 内部组件——比直接写 C++ 多了一层翻译成本,还丢了 C++ 的编译期检查。这与弃用蓝图的理由完全同构:”复杂逻辑(输入模拟 + 异步等待 + 状态断言)在蓝图中难以编写和维护”——换成 Lua 并没有解决这个问题,只换了一种写法。
3.4 工程事实(加速决策的最后一根稻草)
- PIE 集成断裂:handoff 文档确认,测试在 Automation 中”跑通”了,但 PIE 全程未启动;
EditorContext下CurrentWorld == nullptr,依赖 World 的桥接函数静默返回假值。测试验证的是 Lua 语法,不是集成行为。 而这正是 Lua 方案最大的卖点(替代蓝图做集成测试)。 - 双语言断层成本:类名/函数名/属性名以字符串形式在运行时才报错;C++ 桥接改一处,Lua 文档、API 参考、类型转换要同步多处;调试需要跨 Lua 协程与 UE Tick 两套执行模型。
- 对比实验:同一时期的 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. 如果重来:三条可执行建议
- 先做最小验证,再决定引入新语言。用 3 个”金样用例”(覆盖输入模拟 + 异步等待 + 状态断言)分别用 C++ CQTest 和候选 DSL 各写一遍,对比编写时间、调试体验、维护成本后再立项。本次的结果大概率在立项阶段就会被否决。
- 真要做 DSL,先定义语义模型。DSL 的语法应该描述游戏行为(”给武器上弹””射击””等待弹药变化””角色复活”),而不是引擎 API(GetPC / FindComponent / GetStatTag)。用目标用例倒推语法,先让最少的桥接跑通金样用例,再决定是否扩 API。
- **抽象层必须验收”语义厚度”**。禁止透传和 stub 进入公开 API 表面;以”新测试编写者写出第一个用例的时间与行数、新人是否需要懂引擎概念”作为抽象层是否成立的验收标准。
7. 结论与当前状态
当前测试策略(决策,见 00-测试总纲.md)
1 | 仅两种方式: |
框架分工: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.cpp、docs/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-01
03、装备 EQ-0105):✅ 完成 - Session 2(控制动画 AN-01、视角 LC-01、相机 CM-01/02/05):✅ 完成
- Session 3(HUD HD-01
05、队伍 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-01
04 / 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-01 |
| 探索性测试 | 余 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 | 环境: FMapTestSpawner(L_Expanse) → QuickBarComponent / EquipmentManagerComponent |
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 | 环境: FMapTestSpawner(L_Expanse) → QuickBarComponent / 武器实例 |
🔧 实际落点:上述思路由蓝图功能测试落地(资产见 §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 | 环境: FMapTestSpawner(L_Expanse) / LoadObject 读 DataAsset |
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.MovementStoppedTag 的添加/移除。
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 | 环境: FMapTestSpawner(L_Expanse) → CharacterMovement / ULyraSettingsShared |
相机系统 — 测试宪章
测试范围
| # | 测试项 | 说明 |
|---|---|---|
| 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 | 环境: FMapTestSpawner(L_Expanse) → 获取 PlayerCameraManager / UCameraComponent |
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 | FE-02(主菜单): |
3. 设置界面(1 个用例,含 4 个子状态)
FE-03 设置界面
| 编号 | 细分状态 | 前置条件 | 测试步骤 | 预期结果 | 优先级 | 用例目的 |
|---|---|---|---|---|---|---|
| FE-03-01 | 设置页面布局 | 进入设置 | 浏览各设置标签页 | 画面/音频/控制等标签页正常切换 | 🟡 中 | 🟢 回归(设置流程) |
| FE-03-02 | 修改设置 | 设置界面中 | 修改一个设置项 | 设置值即时更新或显示已更改标记 | 🟡 中 | 🟢 回归(设置流程) |
| FE-03-03 | 保存设置 | 已修改设置 | 应用/保存 | 设置保存,下次启动时仍然有效 | 🔴 高 | 🟢 回归(设置持久化) |
| FE-03-04 | 取消设置 | 已修改设置 | 不保存并退出 | 设置恢复到修改前的状态 | 🟡 中 | 🟢 回归(设置流程) |
代码实现思路:
1 | FE-03(设置界面): |
4. 确认弹窗(1 个用例,含 3 个子状态)
FE-04 确认弹窗
| 编号 | 细分状态 | 前置条件 | 测试步骤 | 预期结果 | 优先级 | 用例目的 |
|---|---|---|---|---|---|---|
| FE-04-01 | 退出游戏确认 | 主菜单中 | 选择退出游戏 | 弹出确认弹窗,有确认/取消选项 | 🔴 高 | 🟢 回归(弹窗流程) |
| FE-04-02 | 确认弹窗 — 确认 | 确认弹窗已显示 | 点击确认 | 执行对应操作(退出游戏) | 🔴 高 | 🟢 回归(弹窗流程) |
| FE-04-03 | 确认弹窗 — 取消 | 确认弹窗已显示 | 点击取消 / 按 ESC | 弹窗关闭,不执行操作 | 🟡 中 | 🟢 回归(弹窗流程) |
代码实现思路:
1 | FE-04(确认弹窗): |
5. 加载画面(1 个用例,含 3 个子状态)
FE-05 加载画面
| 编号 | 细分状态 | 前置条件 | 测试步骤 | 预期结果 | 优先级 | 用例目的 |
|---|---|---|---|---|---|---|
| FE-05-01 | 进入游戏时加载画面 | 选择游戏模式后 | 等待加载 | 显示加载画面(有提示/进度) | 🔴 高 | 🔵 冒烟 + 🟢 回归(加载链路) |
| FE-05-02 | 加载完成后进入游戏 | 加载画面中 | 加载完成 | 加载画面消失,进入游戏 | 🔴 高 | 🔵 冒烟 + 🟢 回归(加载链路) |
| FE-05-03 | 加载画面期间输入 | 加载画面中 | 按任意键 | 加载不受影响,继续执行 | 🟡 中 | 🟢 回归(加载中输入) |
代码实现思路:
1 | FE-05(加载画面): |
6. ESC 暂停菜单(1 个用例,含 3 个子状态)
FE-06 暂停菜单
| 编号 | 细分状态 | 前置条件 | 测试步骤 | 预期结果 | 优先级 | 用例目的 |
|---|---|---|---|---|---|---|
| FE-06-01 | 暂停菜单 | 游戏中 | 按 ESC | 弹出暂停菜单 | 🔴 高 | 🟢 回归(暂停流程) |
| FE-06-02 | 继续游戏 | 暂停菜单中 | 点击继续 | 暂停菜单关闭,返回游戏 | 🔴 高 | 🟢 回归(暂停流程) |
| FE-06-03 | 从暂停菜单回到大厅 | 暂停菜单中 | 选择回到大厅 | 退出当前对局,回到主菜单 | 🟡 中 | 🟢 回归(退出流程) |
代码实现思路:
1 | FE-06(ESC暂停): |
音频系统 — 测试宪章
测试范围
| # | 测试项 | 说明 |
|---|---|---|
| 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 | 环境: 单元测试,无需PIE → ULyraSettingsLocal(UPROPERTY Config) |
队伍系统 — 测试宪章
测试范围
| # | 测试项 | 说明 |
|---|---|---|
| 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 | 环境: 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 | 环境: FMapTestSpawner(L_Expanse) → FindFirstPlayerPawn |
3. 重生(1 个用例,含 2 个子状态)
IN-03 重生
| 编号 | 细分状态 | 前置条件 | 测试步骤 | 预期结果 | 优先级 | 用例目的 |
|---|---|---|---|---|---|---|
| IN-03-01 | 重生链路 | 角色死亡 | 等待重生 | 重生后 Pawn 重新生成,链路完整跑通 | 🔴 高 | 🟢 回归(与 DT-06 重叠,建议并入) |
| IN-03-02 | 重生后状态重置 | 角色死亡 | 重生后检查 | 重生后 AbilitySet 重新授予,装备重置 | 🔴 高 | 🟢 回归(与 DT-06 重叠,建议并入) |
代码实现思路:
1 | 环境: FMapTestSpawner(L_Expanse) → 致死伤害+重生 |
4. 输入绑定(1 个用例,含 2 个子状态)
IN-04 输入绑定
| 编号 | 细分状态 | 前置条件 | 测试步骤 | 预期结果 | 优先级 | 用例目的 |
|---|---|---|---|---|---|---|
| IN-04-01 | PlayerController 绑定 | Pawn 生成后 | 验证绑定 | PlayerController 正确 Possess Pawn | 🔴 高 | 🔵 冒烟(输入链路基线) |
| IN-04-02 | 输入组件初始化 | Pawn 生成后 | 验证输入 | 输入组件(EnhancedInput)已初始化 | 🔴 高 | 🔵 冒烟(输入链路基线) |
代码实现思路:
1 | 环境: FMapTestSpawner(L_Expanse) → PC+Pawn |
5. 断线重连(1 个用例,含 3 个子状态)
IN-05 断线重连
| 编号 | 细分状态 | 前置条件 | 测试步骤 | 预期结果 | 优先级 | 用例目的 |
|---|---|---|---|---|---|---|
| IN-05-01 | 断线后重连 | 游戏中断开网络 | 重新连接 | 客户端重连成功,重新进入游戏 | 🟡 中 | 🟢 回归(网络恢复) |
| IN-05-02 | 重连后状态恢复 | 重连成功 | 检查状态 | 角色 AbilitySet 重新授予,状态正确 | 🟡 中 | 🟢 回归(状态恢复) |
| IN-05-03 | 资源重复加载 | 多次重连 | 检查资源 | 资源不重复加载,内存无泄漏 | 🟡 中 | 🟢 回归(资源防重复) |
代码实现思路:
1 | 环境: 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 | 环境: FMapTestSpawner(L_Expanse) → 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 | 环境: FMapTestSpawner(L_Expanse) → 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 | HandleOutOfHealth() [Server] |
死亡冻结范围:基础移动(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 | 环境: FMapTestSpawner(/ShooterTests/Maps, L_ShooterTest_DeviceProperties) → ULyraHealthComponent → ULyraCharacter |
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 | 死亡 → OnDeathFinished → DestroyDueToDeath → UninitAndDestroy (Actor 销毁/隐藏) |
决策: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标签,由 ShooterCoreGE_SpawnIn/GA_SpawnEffect授予,ULyraHealthSet::PreGameplayEffectExecute将非自杀伤害归零)。因此 DT-07-01”重生后立即死亡”在正常伤害下不可触发(无敌窗口覆盖重生初始化窗口),测试使用自杀伤害(DamageSelfDestruct,穿透无敌)驱动该场景;DT-06/07 发现的时序问题属测试侧 flake,非游戏 bug。
3. 散布系统
📦 后续排期:本次交付不含散布系统,用例保留。
设计依据
Lyra 使用 heat 曲线驱动散布系统,核心流程:
1 | AddSpread() [每射击一次]: |
热量范围由三条曲线 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 | 环境: FMapTestSpawner(L_Expanse) → ULyraRangedWeaponInstance |
性能测试总结报告: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. 方法
- 交叉验证(stat unit / trace / ProfileGPU),冲突时解释而非挑一个信。
- 口径对齐(单位、Avg/Max/Sum、Count;I.Avg 为按实例平均,单帧成本以帧内聚合为准)。
- Incl/Excl 下钻到叶子。
- 环境与上下文控制(启动方式、画面设置、周围碰撞体、特效编译状态)。
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 gc、obj list、memreport -full、stat 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. 结论
- 性能画像:GPU 侧体积云为最大单 pass(稳态 8.6ms、尖峰 99ms),渲染背压造成游戏线程帧尾等待;GC 造成周期性 82ms 级暂停;事件型卡顿由 PIE 编译主导。
- 真问题集中在渲染侧(体积云)与内存侧(GC)——两者均需打包 Development 版确认正式数字。(打包版复测已完成:GC 降级 P2;体积云 GPU 侧待 ProfileGPU 确认)
- 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: Missed→FD3D12DynamicRHI::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::RegisterNodeInternal800ms(t=58.9s)——触发点未确认(音频 / 动画 / 蓝图节点注册?)。
7.6 体积云:打包版 CPU 侧无大成本,GPU 侧待确认
- CPU 导出无 VolumetricCloud pass 耗时(
InitVolumetricCloudsForViews4020 次累计 0.7ms)。 - GPU 侧需游戏内
stat gpu/ ProfileGPU 确认(5.8 headless 不支持 GPU 事件导出)。原 P1(VolumetricCloud 尖峰)打包复测仍开放。
7.7 结论更新
- GC 不再是主凶:PIE 放大 33 倍坐实,打包版真实代价 18ms(P2)。
- 打包版主要 UX 卡顿 = 操作触发的编译类冻结(改画质 1.5s、设置菜单 653ms)——事件型,可用 PSO 预缓存优化或确认复现条件。
- 体积云打包版数字待 ProfileGPU 确认。
本轮 trace 性能分析关闭;打包版复测已完成(见 §7)。后续以自动化回归(Gauntlet + 阈值基线)为下一阶段。
探索性测试
⏱ 90 分钟 · 手工 · 以玩家视角自由探索,发现组合场景下的 bug
测试范围
探索时重点关注 跨系统交互 和 边界操作,而非单功能验证。
记录方式
发现 bug 时记录:
- 做了什么(一两句话描述操作)
- 实际结果(看到了什么)
- 预期结果(应该是什么)
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 朝向区间触发,其余朝向完全无效。核心操作在大部分朝向失效,多地图复现、与关卡几何无关——最早暴露”冲刺交互链路存在系统性问题”的证据。
系统性规律
冲刺交互断层:冲刺(根运动)与其它输入/系统叠加时缺少统一策略,表现为三类:
- 输入被静默忽略——进行中动作期间,被拒绝的输入直接丢弃(AM-03-05、AM-02-06、AM-04-01)
- 外力被覆盖——冲刺中弹射力度被根运动覆盖(EX-01-01)
- 残留视觉副作用——瞄准中冲刺引起相机缩放、准星闪变(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 蓝图,边看边做:
- 内容浏览器 → Plugins → ShooterTests → Content → Blueprint →
B_Test_FireWeapon - 右键 → **Run Functional Test…**(先跑一次看看效果)
- 双击打开蓝图,看它的
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(不需要),至少确保它们是 Public 或 Private。
第四步:编写 OnPrepareTest
在事件图表中右键搜索 On Prepare Test 事件,添加:
1 | Event OnPrepareTest: |
关于 GameplayTag:在
Get Stat Tag Stack Count的 Tag 引脚,右键 → “Create a Make Literal GameplayTag” → 填入Lyra.ShooterGame.Weapon.MagazineAmmo
第五步:编写 IsReady
搜索 Is Ready 事件,添加返回值:
1 | Event IsReady (return bool): |
使用 AND 布尔节点 连接所有条件。
第六步:编写 StartTest
搜索 Start Test 事件:
1 | Event StartTest: |
关于 Input Key:需要从 Pawn 获取 Controller,然后 Cast 到
PlayerController,再调用Input Key。
Input Key 节点详解
TestPawn → Get Controller → Cast To PlayerController → Input Key:
| 参数 | 值 |
|---|---|
| Player Controller | 转型后的 PlayerController |
| Key | Left Mouse Button(搜索 “LeftMouseButton”) |
| Event Type | Pressed |
第七步:编译、放置、测试
编译
- 点击 编译(Compile),检查没有节点警告
放置到地图
- 打开
FT_WeaponTest_Main地图 - 把
FT_WF_01_01_SingleShot拖入场景(放在任意位置) - 选中它,在 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 | FunctionalTest 蓝图 = 一张地图上的裁判决 Actor |
打开项目自带的 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,含向上/方向两种弹射器;如实际为其他关卡请修正)
步骤:
- 在关卡中找到向上弹射器和方向弹射器
- 先正常行走/跑动踩上弹射器,记录基准弹射高度/距离(对照组)
- 按 Dash(冲刺)进入弹射器触发范围,观察弹射表现与音效
- 两种弹射器(向上 / 方向)各重复 2~3 次
实际: Dash 中触发弹射时,弹射音效正常播放,但弹射力度相比正常触发被大幅削弱:角色只有轻微抬升/位移,几乎仍按冲刺轨迹滑行,像是只被”蹭”了一下。
预期: 无论是否处于 Dash 状态,弹射器都应施加完整弹射力度;若设计上希望”Dash 中触发时减弱”,也应与音效反馈一致,而不是听感”弹了”、实际”没弹起来”。
关联: 探索性测试 EX-01(地图互动) / B_Launcher_Up、B_Launcher_Push(ShooterCore 蓝图,重叠时 LaunchCharacter) / GA_Hero_Dash(蓝图 GA,根运动 ApplyRootMotionConstantForce + Dash 蒙太奇) / GCNL_Launcher_Activate(弹射音效)
推测: 弹射器在重叠时基于角色当前速度(GetLastUpdateVelocity)计算 LaunchVelocity 并调用 LaunchCharacter;而 Dash 期间角色处于根运动驱动(ApplyRootMotionConstantForce + 根运动蒙太奇),根运动每帧覆盖/重新计算 Velocity,LaunchCharacter 写入的弹射速度在下一个移动 tick 即被根运动覆盖,因此弹射力度被削弱至几乎无效。音效走独立的 GameplayCue(GCNL_Launcher_Activate),不受移动结果影响,所以音效正常。
建议方向(供修复参考):在弹射器触发时若检测到角色处于根运动/Dash,可先打断 Dash 再施加弹射;或在 Dash GA 的交互策略中明确”弹射器优先级高于 Dash”(沿用 AM-03-05 的输入策略统一思路)。
Bug 报告
标题: [地图互动] 方向弹射器按玩家朝向弹射,而非速度/移动方向
等级: 🟡 中(非阻塞 · 交互方向与运动方向不一致)
环境: 单机 · 手工探索(探索性测试 EX-01)· 方向弹射器所在关卡(L_Expanse,如实际为其他关卡请修正)
步骤:
- 找到方向弹射器
- 面朝一个方向(如正北)但向另一方向跑动/侧移(如正东),踩上弹射器
- 观察弹射方向;再对照”面朝与移动方向一致”时的弹射方向
- 再试倒退跑(面朝弹射器、背向移动方向踩上)
实际: 弹射方向始终是玩家面朝方向,与移动/速度方向无关;侧移、倒退时,弹射方向与运动方向明显不一致,玩家会被推向面朝方向而非前进方向。
预期: 方向弹射应按玩家当前移动/速度方向弹射(或至少与移动输入方向一致),侧移、倒退时弹射方向应与运动方向一致。
关联: 探索性测试 EX-01(地图互动) / B_Launcher_Push(ShooterCore 蓝图)
推测: B_Launcher_Push 的 XY 弹射方向直接取 GetActorForwardVector(面朝方向),未使用角色当前速度方向(GetLastUpdateVelocity / GetLastMovementInputVector),因此仅在”面朝与移动同向”时表现正确,侧移/倒退时弹射方向错误。若产品设计确实要求”沿面朝方向弹射”,则本项降级为设计确认而非 Bug;否则应改用速度/输入方向作为弹射方向。
玩法建议(非 Bug)
标题: [地图互动] 玩法建议:手雷也可触发弹射装置
类型: 玩法建议 · 优先级:低(进 backlog)
背景: 探索性测试 EX-01(地图互动)期间,弹射装置目前只对玩家角色生效;建议让手雷也能触发弹射,形成”弹雷”玩法。
建议内容:
- 手雷落在弹射装置触发范围内时,同样被施加弹射力度(对投掷物施加速度/冲量,而非仅对玩家
LaunchCharacter) - 衍生玩法:被弹射的手雷保留原有引爆逻辑(定时 / 落地 / 二次引爆),可把雷弹到高处或掩体后
- 一致性要求:手雷弹射的方向与力度规则应与玩家一致,避免重蹈 EX-01-01(Dash 中力度被削弱)、EX-01-02(按朝向而非速度方向弹射)的问题
- 网络注意:手雷弹射应由服务端权威判定并复制结果,保证客户端表现一致
关联: 探索性测试 EX-01(地图互动) / B_Launcher_Up、B_Launcher_Push(弹射装置) / GA_Grenade、B_Grenade(ShooterCore 手雷投掷)
Bug 报告
标题: [地图互动] EX-01-04 传送门仅下半部分可触发传送(有效高度约为人高),与模型全高表现不符
等级: 🟡 中(非阻塞 · 表现与判定不一致)
环境: 单机 · 手工探索(探索性测试 EX-01)· 传送门所在关卡(L_Expanse,如实际为其他关卡请修正)
步骤:
- 找到传送门(可视模型为全高门框)
- 从下半部分正常走入,确认触发传送(对照组)
- 跳跃 / 从高处进入传送门上半部分(高于人高),观察是否触发
- 在传送门不同高度(底部、人高附近、门框顶部)各尝试一次并记录
实际: 传送门只有下半部分(约人高以下)能触发传送;上半部分穿过不触发,与可视门框的全高表现不符——看起来”门很大,实际只有半截能用”,玩家跳跃进入上半部分会被视觉误导。
预期: 传送判定范围应与可视模型一致(全高都可触发),或将可视模型缩小到与实际触发范围一致;至少不应让玩家从视觉上产生”全高可通行”的误解。
关联: B_Teleport(ShooterCore 蓝图:BoxComponent 重叠 + K2_TeleportTo) / 传送门视觉参数(TeleporterHeight / TeleporterWidth 材质参数) / 探索性测试 EX-01
推测: B_Teleport 的传送判定依赖 BoxComponent 的碰撞范围,而可视门框高度由 TeleporterHeight / TeleporterWidth 参数(材质/网格)控制,两者没有对齐——Box 高度约等于角色高度(覆盖下半部分),材质模型却绘制成更高的门框。修复方向:让碰撞范围与可视模型一致(以模型为准调整 Box),或缩小模型匹配现有触发范围。
Bug 报告
标题: [控制系统] AM-02-06 滞空时按下蹲被忽略(应触发或排队到落地)
等级: 🟡 中(非阻塞 · 输入策略不一致)
环境: 单机 · 手工探索(发现于探索性测试 EX-01 期间,非地图互动类)· 控制系统
步骤:
- 角色站立,按跳跃进入滞空
- 滞空期间按下蹲键
- 保持到落地,观察滞空期间及落地后的角色状态
实际: 滞空时按下蹲被完全忽略:滞空期间无任何反应,落地后也不会自动蹲下(输入被静默丢弃)。
预期: 与 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 决策)· 不限帧率
步骤:
- 角色正常站立,按 Dash 键触发冲刺(
GA_Hero_Dash) - Dash 过程中依次输入:蹲下键、切换装备键(QuickBar)、射击键、跳跃键、近战键、手雷键
- 分别观察各输入在 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 等)
步骤:
- 角色静止站立,不按任何方向键
- 依次面向不同方向(建议按 45° 步进转一整圈,至少覆盖 4 个象限)
- 每种朝向按 Dash 键,观察是否触发冲刺
- 换其他地图重复验证,排除地图几何因素
实际: 无方向 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)。问题大概率出在”零输入偏置/方向选择”环节:面朝方向在某些世界象限下,偏置后的向量落入方向判定的无效区间(如象限/点积判断只对某一象限成立),导致选不出合法蒙太奇或激活失败。多个地图一致说明是角色空间/能力逻辑问题,而非关卡因素。需在蓝图里检查 BiasForwardMovement 与 SelectDirectionalMontage 对零输入、各世界朝向的处理。
Bug 报告
标题: [控制系统] AM-04-01 近战中输入射击被忽略(应排队或触发)
等级: 🟡 中(非阻塞 · 输入策略不一致)
环境: 单机 · 手工探索(发现于探索性测试 EX-01 期间,归属控制系统)· 持有武器
步骤:
- 持有武器,发动近战(GA_Melee)
- 近战动作期间按射击键
- 观察近战期间及近战结束后的表现(是否触发、是否排队补发)
实际: 近战中按射击被静默忽略:近战期间无任何反应,近战结束后也不会补发射击(输入丢失)。
预期: 与项目输入策略统一——近战期间射击输入应排队(结束后按序执行)或立即触发;至少不应静默丢弃。参照 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 期间,归属音频系统)· 存在敌人开火的场景
触发条件: 摄像机处于俯视角(拉高俯视)时触发;水平视角下表现正常。
步骤:
- 让敌人(AI / 另一玩家)开枪
- 站在敌人正前方极近距离(贴脸,<1m)听枪声
- 逐步后退(约 1m、5m、10m、20m)对比同一枪声的音量与音色
- 不同武器(步枪 / 手枪 / 霰弹枪)各测一次
- 切换摄像机视角(水平 ↔ 俯视角)在相同横向距离下重复对比
实际: 摄像机为俯视角时,贴脸(横向极近)的敌人枪声反而沉闷、发闷,听感像远处枪声;切换为水平视角后,相同横向距离下枪声恢复正常。问题随摄像机俯仰/高度变化,与横向距离不成单调关系。
预期: 相同横向距离下,枪声听感应与摄像机视角无关:越近越响、高频越清晰;不应出现”俯视角下贴脸反而像远处”的表现。
关联: 音频系统 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 期间,归属相机系统)· 持有武器并进入瞄准状态
步骤:
- 持有武器,进入瞄准状态(ADS)
- 静止不按方向键,按 Dash(无方向 Dash)
- 观察 Dash 触发瞬间的摄像机缩放与准星变化
- 在不同朝向重复(关联 AM-03-06:Dash 触发/不触发两种情况分别观察)
- 记录变化时长与恢复情况
实际: 瞄准中按无方向 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(无人值守低帧率)
步骤:
- 运行 CQTest
NV_03_02_GravityZero(ControlSystem.spec.cpp) - 测试设置
CharacterMovement->GravityScale = 0后跳跃 - 角色以恒定 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 · 不限帧率
步骤:
- 持有步枪,按住射击键持续开火
- 保持射击的同时按换弹键
实际: 换弹指令被忽略,射击继续正常进行,无换弹动画、弹药不恢复,停止射击换弹动作不执行
预期: 射击被打断→执行换弹(依测试宪章 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)· 不限帧率
步骤:
- 获得武器(
AddItemDefinition(ID_Rifle)加入背包) - 装备武器(QuickBar 槽位设为 active,触发 Equip)
- 观察装备瞬间的角色动画与武器 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 等)在 EquipItem 时 mesh 立即设为可见,而 Equip montage(拿出动画)的播放与”武器 attach 到手上”的时机在其后。缺少同步机制:montage 开始前应隐藏 mesh(或延迟 spawn/attach),并在 montage 的动画 Notify(如”握住武器”节点)触发时才显示 mesh。


