LyraStarterGame 测试文档

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

00-总纲

01-系统测试

02-专项测试

03-实施

Bug报告

执行计划


LyraStarterGame 测试总纲

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

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

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

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

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

测试原则

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

LyraStarterGame 测试计划

本文档是”测试宪章”与”测试实施”之间的桥梁:从宪章定下”测什么”出发,规划”按什么顺序实现、每个阶段重点做什么、做到什么程度算完成”。

── 2025-07 实践经验 ──
武器切换测试告诉我们:在 Lyra 的 CQTest + FMapTestSpawner 框架下,所有测试本质是集成测试
HUD 报错不是测试设计有误,而是集成测试的固有噪音。见第 8 节《测试设计原则》。

文档关系

1
2
3
4
5
6
00-测试总纲.md          ← 系统划分 + 测试原则(定"格局")
00-测试计划.md ← 本文件 ← 实现优先级 + 路线图 + 质量门禁(定"节奏")
01-系统测试/*.md ← 各系统测试宪章(定"用例")
02-专项测试/*.md ← 性能 + 探索性(定"用例")
03-实施/12-测试实施计划.md ← 环境搭建 + 工具选择 + Bug 模板(定"怎么干")
Source/LyraGame/Tests/ ← 自动化实现(CQTest / Gauntlet / Spec)

1. 范围与统计

11 个测试系统,共计 88 个测试用例(含236个子状态) + 探索性测试 6 轮,覆盖自动化(CQTest / Gauntlet / AutomationDriver)与手工验证。

系统 用例数 自动化 手工 推荐工具 实现状态
01 武器与装备 14(含28子状态) 21 12 CQTest + 手工 ✅ EQ/WS 已 CQTest;WF 已用蓝图功能测试完成(2026-08-03,与宪章 CQTest 决策不符,先这么用)
02 控制系统 13(含46子状态) 30 18 CQTest ✅ CQTest 30/30(2026-08-03)
03 相机系统 8(含27子状态) 0 23 手工 ✅ 数值 9/9(NV-01/02 单元,2026-08-03);功能手工完成(Session 2)
04 HUD 5(含22子状态) 0 22 手工 ✅ 手工完成(Session 3,2026-08-03)
05 前端页面 5(含18子状态) 18 0 Spec + Gauntlet ⏳ Elimination 菜单已实现
06 音频系统 6(含16子状态) 0 16 手工 ✅ 数值 8/8(2026-08-03)+ 手工完成(Session 3)
07 队伍系统 5(含14子状态) 0 14 手工 ✅ 手工完成(Session 3,2026-08-03)
08 初始化链路 6(含19子状态) 19 0 CQTest + Gauntlet ✅ 5/5(ShooterMaps 5 地图,2026-08-03 回归)
09 状态系统 4(含14子状态) 10 3 🚫 不需要(2026-08-02 决策)
10 事件测试 13(含46子状态) 46 0 CQTest + PIENetworkComponent ✅ 13/13(DT-01/03/05/06/07,2026-08-04 修复后复跑;DT-04 网络暂缓 → 已知缺口)
11 性能测试 12 12 0 Gauntlet ❌ 已知缺口(可行性调研:本机 36 实例需 3~6h,封板日不做全量)
探索性测试 6 轮 6 轮 手工 ❌ 已知缺口(封板后 backlog)
合计 84(含238子状态) 148 91 + 6轮

2. 实现优先级

按风险等级和系统间依赖排序:

优先级 系统 理由
P0 基石 初始化链路、前端页面 游戏必须能启动、能导航到对局,后续所有测试依赖
P1 核心 武器与装备 → 控制系统 核心玩法闭环:拿起武器 → 移动操作 → 造成伤害 → 死亡/重生
P2 表现 相机 → HUD → 队伍 → 音频 核心玩法可用后的体验验证,大量手工判断
P3 压轴 性能 → 探索性 需要其他系统稳定后才有效果

3. 实现路线图

阶段一:框架搭建(基石)

目标:确保自动化测试基础设施跑通,游戏能从头启动到进入对局。

事项 内容 对应用例
初始化链路 补全 IN-01~05、NV-01 的 CQTest / Gauntlet 实现 19 用例
前端页面 补全 FE-02~06 的 Spec 自动化,覆盖菜单→加载→ESC暂停 18 用例

实现参考

1
2
3
4
5
6
7
已有 ─ Gauntlet:
Source/LyraGame/Tests/LyraTestControllerBootTest.h/.cpp
→ 扩展 IsBootProcessComplete() 加入 Experience/GameState 校验

已有 ─ Spec (AutomationDriver):
Source/LyraGame/Tests/MenuStartElimination.spec.cpp
→ 以此为模板扩展其他 FE 用例

完成标准

  • FMapTestSpawner 加载 L_Expanse 地图稳定通过
  • Experience 链路(GameMode → Pawn → AbilitySet → 默认武器)验证完成
  • 主菜单 → 选择模式 → 加载 → 进入游戏 全流程自动化可回归
  • 无 🔴 阻塞 Bug

阶段二:核心自动化

目标:核心玩法闭环全部可自动化验证。

事项 内容 对应用例
武器功能/数值 WF-0102(射击/换弹) + WS-0105(散布/衰减/射速/弹药/部位伤害) 21 用例
控制数值 MV-0103 + AM-0103 + NV-01~04 10 主用例(含38子状态)
状态系统(自动) ST-01 + NV-01~02 3 主用例(含11子状态) 🚫 已砍

实现参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CQTest(用于 WF-01~02 输入模拟、WS-01~05 数值、控制数值精确验证):
参考: Source/LyraGame/Tests/CQTest/WeaponSystem_Equipment.spec.cpp
→ 展示了 CQTest + FMapTestSpawner 的标准模式:
1. BEFORE_EACH() 中 FMapTestSpawner 加载真实地图
2. TestCommandBuilder 的 StartWhen / Then 异步模式
3. 通过 FindFirstPlayerPawn() 获取 Pawn
4. 通过 FindComponentByClass<> 获取关键组件
输入模拟: TestCommandBuilder + InputTestActions(按下/按住/松开)替代蓝图 InputKey

关键类路径(用于 CQTest 查找组件):
- 武器装备: ULyraEquipmentManagerComponent, ULyraQuickBarComponent,
ULyraInventoryManagerComponent
- 移动控制: ULyraCharacterMovementComponent,
UCharacterMovementComponent::MaxWalkSpeed / JumpZVelocity
- 生命周期: FMapTestSpawner 自动处理 Pawn Possess 和输入初始化

完成标准

  • 全部用例编写完成并通过(武器 WF-0102+WS-0105 / 控制 MV-0103+AM-0103+NV-01~04)
  • 武器切换、射击、换弹、移动、跳跃、Dash、死亡冻结 均可脚本化验证
  • 边界值覆盖到数值测试用例(选择性边界分析 B={a-ε, a, (a+b)/2, b})
  • 无 🔴 阻塞 Bug

阶段二点五:死亡事件与网络测试

目标:死亡/复活完整链路 + 客户端—服务端网络同步验证。

事项 内容 对应用例
死亡单机 状态机、移动/碰撞/交互冻结、摄像机切换、边界情况 DT-01~05(17 子状态)
死亡网络 PIE 多开验证 DeathState 复制、预测回滚冲突 DT-04(2 子状态)
复活 血量/装备/技能重置、碰撞/输入恢复、立即死亡 DT-06~07(9 子状态)

实现参考

1
2
3
4
5
6
7
8
9
10
已有 ─ CQTest:
Source/LyraGame/Tests/InitChain/ShooterMapsInit.spec.cpp
→ FMapTestSpawner + TestCommandBuilder 模式

新工具 ─ CQTest PIENetworkComponent:
头文件: Engine/Source/Developer/CQTest/Public/Components/PIENetworkComponent.h
→ NETWORK_TEST_CLASS + FPIENetworkComponent
→ WithClients(2) 创建服务端 + 2 客户端
→ ThenServer / ThenClients 分别断言服务端和客户端状态
→ 死亡状态通过 ULyraHealthComponent::DeathState 验证复制

完成标准

  • 死亡单机(DT-01~05,17子状态)全部通过
  • 死亡网络(DT-04,2子状态)至少实现基础验证
  • 复活(DT-06~07,9子状态)全部通过
  • 无 🔴 阻塞 Bug

阶段三:边界值与手工测试

目标:补完需要人工判断的用例,聚焦视觉/手感/听感/跨系统交互。

事项 内容 对应用例 方式
武器动画 WA-0103 + EQ-0105(含25子状态) 手工
控制动画/视角 LC-01 + AN-01 手工
相机功能/数值 CM-01/02/05 + NV-01~02(FOV clamp/俯仰限幅) 手工
HUD HD-01~05 手工
音频功能/数值 AU-0104 + NV-0102(音量边界值) 手工
队伍功能 TM-01~05 手工
状态机视觉 ST-02 手工 🚫 已砍

手工测试执行优先级(在一次手工测试 Session 中按此顺序执行):

1
2
3
Session 1(核心视觉): 武器动画(WA) → 装备动作(EQ-01~05)
Session 2(移动体验): 控制动画(AN-01) → 视角(LC-01) → 相机(CM-01/02/05)
Session 3(UI & 信息): HUD(HD-01~05) → 队伍(TM-02~05) → 音频(AU-01~04)

手工测试参考 03-实施/12-测试实施计划.md 第 3 节的 Bug 报告模板记录发现。

完成标准

  • 边界值自动化用例(NV-0102 相机 / NV-0102 音频)编写完成并通过
  • 全部手工用例执行完成
  • 无 🔴 阻塞 Bug
  • 每个手工 Session 的 Bug 全部登记到 TestDocs/Bug报告/

阶段四:压测与探索性测试

目标:在系统稳定后,验证性能边界和跨系统组合 Bug。

事项 内容 对应用例
性能测试 PF-01~12 × 3 地图(帧率/延迟/逐帧检测) 36 实例
探索性测试 6 轮 × 90 分钟,跨系统自由探索 6 轮

性能测试实现参考

1
2
3
4
5
6
7
8
9
10
Gauntlet:
Source/LyraGame/Tests/LyraTestControllerBootTest.h/.cpp
→ 参考 UGauntletTestController 模式:
1. 扩展 UGauntletTestController 子类
2. 在 OnTick() 中采集帧数据(FPlatformTime::Seconds / FStats)
3. 使用 Console Command 设置 t.MaxFPS / net.PacketSimulationSettings
4. 多地图参数通过 -map 命令行传入

Unreal Insights:
→ 运行测试时附加 -trace 参数,生成 .utrace 文件用于逐帧分析

完成标准

  • 全部 36 个性能测试实例通过
  • 60fps 下平均帧时间 ≤ 16ms,99% 帧 ≤ 30ms
  • 延迟 50~500ms 范围内无断线或操作卡死
  • 5 分钟内无持续内存增长
  • 探索性 6 轮全部完成,发现 Bug 清零

4. 自动化实现指引

4.1 测试框架选择

框架 文件位置 适用场景 入口
CQTest Source/LyraGame/Tests/CQTest/ 单进程、游戏内组件精确验证、数值边界/循环遍历、输入模拟、网络同步 TEST_CLASS + FMapTestSpawner
Spec (AutomationDriver) Source/LyraGame/Tests/ UI 交互流程、菜单导航、前端页面 BEGIN_DEFINE_SPEC
Gauntlet Source/LyraGame/Tests/ 多进程、启动链路、性能逐帧采集 UGauntletTestController 子类

4.2 CQTest 标准模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 文件: Source/LyraGame/Tests/CQTest/系统名.spec.cpp
// 参考: WeaponSystem_Equipment.spec.cpp (379 行)

#include "CQTest.h"
#include "Components/MapTestSpawner.h"

#if WITH_DEV_AUTOMATION_TESTS

TEST_CLASS(FMySystemSpec, "Project.MySystem")
{
TUniquePtr<FMapTestSpawner> MapSpawner;
APawn* PlayerPawn = nullptr;

BEFORE_EACH()
{
MapSpawner = MakeUnique<FMapTestSpawner>(
TEXT("/ShooterMaps/Maps"), TEXT("L_Expanse"));
MapSpawner->AddWaitUntilLoadedCommand(TestRunner);
}

TEST_METHOD(Case_01_Description)
{
TestCommandBuilder
.StartWhen([this]() {
PlayerPawn = MapSpawner->FindFirstPlayerPawn();
return PlayerPawn != nullptr;
})
.Then([this]()
{
// Act + Assert
ASSERT_THAT(AreEqual(expected, actual));
});
}
};
#endif

4.3 CQTest 标准模式(含输入模拟)

蓝图功能测试(FunctionalTest 蓝图)已弃用(2026-08 决策),输入模拟类用例统一用 CQTest + InputTestActions 实现。

1
2
3
4
5
6
7
// 输入模拟: 按下/按住/松开
TestCommandBuilder
.StartWhen([this]() { return MapSpawner->FindFirstPlayerPawn() != nullptr; })
.Then([this]()
{
// InputTestActions 注入输入 → 等待 → 断言
});

4.4 各系统实现映射

系统 框架 文件/蓝图名 用例对应 状态
武器-装备 C++ CQTest WeaponSystem_Equipment.spec.cpp EQ-01~05 ✅ 已完成
武器-功能 🟦 蓝图功能测试(实际执行)/ C++ CQTest(宪章) ShooterTests/Content/Blueprint/weapon/WF_*.uasset(WF-01-0104、WF-02-0103) WF-01~02 ✅ 蓝图完成(2026-08-03 决策:与宪章 CQTest 不符,先按蓝图执行;CQTest 仅 WF-01-04 有 WeaponSystem_Firing.spec.cpp
武器-数值 C++ CQTest WeaponSystem_Numerics.spec.cpp WS-01~04 ✅ 已完成(2026-08-03 基线回归 21/21)
武器-动画 手工 WA-01~03 ✅ 手工完成(Session 1)
控制系统 C++ CQTest ControlSystem.spec.cpp MV-0103, AM-0103 ✅ 30/30(2026-08-03)
控制数值 C++ CQTest 并入 ControlSystem.spec.cpp NV-01/03/05 ✅ 已完成
相机功能 手工 CM-01/02/05 ✅ 手工完成(Session 2)
相机数值 C++ CQTest CameraSystem_Numeric.spec.cpp NV-01~02 ✅ 9/9(2026-08-03)
HUD 手工 HD-01~05 ✅ 手工完成(Session 3)
前端页面 Spec + Gauntlet MenuStartElimination.spec.cpp FE-02~06 ⏳ 部分已有
音频 手工 AU-01~04 ✅ 手工完成(Session 3)
音频数值 C++ CQTest AudioSystem_Numeric.spec.cpp NV-01~02 ✅ 8/8(2026-08-03)
队伍 手工 TM-01~05 ✅ 手工完成(Session 3)
队伍数值 C++ CQTest TeamSystem_Numeric.spec.cpp(新建) NV-01 🚫 已删(2026-08-03:无队伍数量参数)
初始化链路 C++ CQTest + Gauntlet InitChain/ShooterMapsInit.spec.cpp + LyraTestControllerBootTest IN-01~05 ✅ 5/5(2026-08-03 回归)
状态系统 C++ CQTest StatusSystem.spec.cpp(新建) ST-01, NV-01~02 🚫 不需要(2026-08-02)
事件-死亡 C++ CQTest + PIENetwork EventSystem_Death.spec.cpp DT-01~07 ✅ 13/13(DT-04 网络暂缓 → 已知缺口)
性能测试 Gauntlet LyraTestController* 扩展 PF-01~12 ❌ 已知缺口(可行性调研,封板后 backlog)

4.4 已有实现覆盖

文件 覆盖用例 类型
Tests/CQTest/WeaponSystem_Equipment.spec.cpp EQ-01~05(5主用例) CQTest
Plugins/GameFeatures/ShooterTests/Content/Blueprint/weapon/WF_*.uasset + 对应地图 WF-01-0104、WF-02-0103 🟦 蓝图功能测试(2026-08-03 实际执行;与宪章 CQTest 决策不符,先这么用)
Tests/MenuStartElimination.spec.cpp FE-02~06(部分) Spec
Tests/LyraTestControllerBootTest.h/.cpp 启动链路(部分) Gauntlet
Tests/LyraTestControllerStartEliminationTest.h/.cpp Elimination 启动(部分) Gauntlet
Tests/LyraInventoryItemInstance.spec.cpp 库存物品实例(辅助) Spec
Tests/GameplayTagStack.spec.cpp GameplayTag 堆栈(辅助) Spec
Tests/InitChain/ShooterMapsInit.spec.cpp IN-01~02(部分) — 5地图加载+装备+AI CQTest
Tests/EventSystem_Death.spec.cpp(计划) DT-01~07 — 死亡/复活/网络同步 CQTest + PIENetworkComponent

5. 手工测试执行清单

优先级 🔴 高(核心功能,必须通过)

编号 用例名 系统 步骤摘要
EQ-01 初始武器状态 武器 进入游戏 → 检查初始武器
EQ-02 武器切换(含滚轮/数字等5种状态) 武器 滚轮/数字键 → 切换武器
EQ-03 装备武器 武器 走近武器 → 拾取
CM-01 相机跟随角色移动 相机 移动角色 → 相机平滑跟随
CM-06 贴墙时相机推近 相机 靠墙 → 相机推近不穿模
CM-10 默认 FOV 相机 进入游戏 → 80° 视角 🚫 已删(无可调 FOV)
CM-13 默认→能力相机切换 相机 瞄准 → 平滑过渡 🚫 已删(无相机切换)
HD-01 准星显示 HUD 持有武器 → 准星可见
HD-07 命中敌人显示标记 HUD 命中 → 标记出现
HD-18 血条显示 HUD 进入游戏 → 血条可见
HD-19 受伤后血条变化 HUD 受伤 → 血条减少
HD-21 当前武器信息 HUD 持有武器 → 武器名/弹药显示
LC-01 鼠标水平视角 控制 移动鼠标 → 水平旋转
LC-02 鼠标垂直视角 控制 移动鼠标 → 垂直调整
TM-05 友军标记 队伍 观察队友 → 友军标记
TM-06 敌军标记 队伍 观察敌人 → 敌军标记
TM-07 友军伤害 队伍 射队友 → 无伤害/受限

优先级 🟡 中(重要体验,尽力通过)

编号范围 系统 内容
WF-02-04/05 武器 换弹/射击互斥(2026-08-03 决策:手工补验)。① 射击中按换弹键 → 射击被打断、执行换弹;② 换弹中按射击键 → 换弹被打断、弹药不恢复、换弹动画停止
EQ-02-0305, EQ-0405 武器 快速切换/空槽位/满槽/冷却
WA-01~03 武器 装备/换弹/射击动画
AN-01 控制 移动/跳跃/蹲下/Dash/切换动画
LC-01(全部) 控制 手柄视角/反转/灵敏度
CM-01/02/05(全部) 相机 旋转/碰撞恢复/蹲下偏移
HD-01~05(全部) HUD 全部手工用例
AU-01~04 音频 音量/输出/HDR/加载音频
TM-01~05 队伍 归属/显示/标签/比分
ST-02 状态 死亡状态机视觉 🚫 已砍

6. 质量门禁

📌 封板状态(2026-08-04):按”功能封板”口径(2026-08-03),阶段一三与阶段二点五的
单机部分已满足——自动化 96/96 全绿、手工 Session 1
3 完成、Bug 已登记、无 🔴 阻塞 Bug;
阶段二点五的 DT-04 网络与阶段四(性能 36 实例 / 探索性 6 轮)列入《已知缺口 / 封板后 backlog》,不阻塞封板。

阶段一

  • FMapTestSpawner 加载 L_Expanse 成功,无崩溃
  • Experience → GameMode → Pawn → AbilitySet 链路完整
  • 初始化链路 6 个主用例(含19子状态)全部通过
  • 主菜单 → 选模式 → 加载 → 进游戏 全部可自动化
  • 前端页面 5 个主用例(含18子状态)全部通过

阶段二

  • 武器功能/数值(WF-0102 + WS-0105)全部通过
  • 控制数值(MV-0103 + AM-0103 + NV-01~04)全部通过
  • 状态系统(ST-01 + NV-01~02)全部通过(已砍,2026-08-02 决策)
  • 边界值(0 / 负 / 正常)全部覆盖
  • 无 🔴 阻塞 Bug

阶段二点五

  • 死亡单机(DT-01~05,17子状态)全部通过
  • 死亡网络(DT-04,2子状态)通过
  • 复活(DT-06~07,9子状态)全部通过
  • DeathState 在 PIE 多开中复制一致
  • 无 🔴 阻塞 Bug

阶段三

  • 相机/音频 边界值自动化(NV-0102/NV-0102)全部通过(9/9、8/8)
  • 手工用例 Session 1~3 全部执行完毕(2026-08-03)
  • 手工发现的 Bug 登记到 TestDocs/Bug报告/(4 份)
  • 无 🔴 阻塞 Bug

阶段四

  • 性能测试 36 实例全部通过
  • 60fps 帧时间 ≤ 16ms,99% ≤ 30ms
  • 500ms 延迟不导致断线
  • 5min 帧内存无持续增长
  • 探索性 6 轮完成,无残留 Bug

7. 文件索引

文档 路径
测试总纲 TestDocs/00-总纲/00-测试总纲.md
本计划 TestDocs/00-总纲/测试计划.md
武器与装备宪章 TestDocs/01-系统测试/01-武器与装备系统.md
控制系统宪章 TestDocs/01-系统测试/02-控制系统.md
相机系统宪章 TestDocs/01-系统测试/03-相机系统.md
HUD 宪章 TestDocs/01-系统测试/04-HUD.md
前端页面宪章 TestDocs/01-系统测试/05-前端页面.md
音频系统宪章 TestDocs/01-系统测试/06-音频系统.md
队伍系统宪章 TestDocs/01-系统测试/07-队伍系统.md
初始化链路系统宪章 TestDocs/01-系统测试/08-初始化链路系统.md
状态系统宪章 🚫(已砍,历史参考) TestDocs/01-系统测试/09-状态系统.md
事件测试宪章 TestDocs/01-系统测试/10-事件测试.md
性能测试宪章 TestDocs/02-专项测试/11-性能测试.md
探索性测试 TestDocs/02-专项测试/12-探索性测试.md
测试实施指南 TestDocs/03-实施/13-测试实施计划.md
测试工作流清单 TestDocs/03-实施/测试工作流清单.md
CQTest 参考实现 Source/LyraGame/Tests/CQTest/WeaponSystem_Equipment.spec.cpp
Spec 参考实现 Source/LyraGame/Tests/MenuStartElimination.spec.cpp
Gauntlet 参考实现 Source/LyraGame/Tests/LyraTestControllerBootTest.h/.cpp

8. 测试设计原则(2025-07 实践经验)

8.1 认清你用的测试层级

1
2
3
单元测试 ─────── NewObject 组件,直接调函数,mock 依赖
集成测试 ─────── FMapTestSpawner 加载真实地图,完整游戏环境跑
端到端测试 ───── Gauntlet 从启动菜单开始,模拟全流程

CQTest + FMapTestSpawner 99% 的用例都是集成测试。接受它的特征:

特征 影响
会启动完整的游戏堆栈 HUD / 网络 / 动画系统都在跑
HUD 报错是固有噪音 不是测试设计问题,用 CreateWeaponItem() 提供完整数据即可
Server RPC 在同一帧执行 不需要 Until 等待(单机 PIE 有权限)
测试覆盖了整个系统 断言只测关心的部分,其他部分是”附带运行的”

8.2 用例划分应在”功能路径”层面,不在”系统”层面

1
2
3
4
5
6
7
✅ 正确的视角:
"玩家进入游戏 → 持有初始手枪" ← 一条功能路径
"玩家按数字键 2 → 切换到槽位 2 武器" ← 一条功能路径

❌ 错误的视角:
"武器系统 → 装备模块 → 切换功能" ← 系统内部解构
"HUD 系统 → 准星模块 → 准星显示" ← 系统内部解构

宪章中的系统划分(武器/控制/HUD)是给手工测试用的。自动化测试的用例划分应当以”玩家操作路径”为单位。

8.3 使用正确类型的物品实例

场景 做法
测试槽位管理(不渲染) NewObject<ULyraInventoryItemInstance>()(裸实例)
测试装备切换(会触发 HUD) InventoryManager->AddItemDefinition(ItemDef)(真实武器)

判断依据:该物品是否会被 HUD / 能力系统 / 动画系统读到。会 → 用真实武器。不会 → 用裸实例。

8.4 测试方式选择:CQTest vs 手工(2026-08 决策)

弃用蓝图功能测试。 实践结论:复杂逻辑(输入模拟 + 异步等待 + 状态断言)在 FunctionalTest 蓝图中难以编写和维护,调试效率低,无法帮助提高测试效率。

1
2
3
4
5
6
C++ CQTest(重复性高的需求):
输入模拟(按键/鼠标)→ 等待 → 状态断言:WF 射击/换弹、MV 移动/跳跃/蹲下、AM Dash
组件函数直接调用 → 数值断言:EQ 装备切换、WS 数值边界、NV 边界值

手工测试(其余):
需要视觉/手感/听感判断:动画、相机、HUD、音频、队伍、探索性

选择标准:用例是否会被反复回归执行 → 是 → CQTest;否 → 手工测试。

8.5 降低集成测试噪音的方法

  1. 提供完整数据 — 武器测试用 CreateWeaponItem(),避免裸实例触发 HUD 报错
  2. 创建极简 Experience — 减少不必要的子系统加载(如无 HUD 的测试 Experience)
  3. 收窄断言范围 — 只断言你关心的值,日志中的无关错误作为噪音接受

用例分级清单 — 集成测试取舍

2026-08-03 依据《测试计划》§8.4 选择标准(是否会被反复回归执行)与”失败价值”筛选问题产出:

“这个用例失败时,除了’功能坏了’,还能告诉我什么?”

覆盖范围:全部系统宪章(01~08、10、性能测试)。
09 状态系统已移出测试范围(2026-08-02 决策);探索性测试为自由探索,不适用四类标签。

分类说明

分类 含义 处置
保留为集成 失败能定位链路断裂、有回归价值、或是下游用例基线 留在 CQTest 集成实现
合并 与另一用例断言重叠,或本身是另一个用例的子状态 并入目标用例,宪章去掉独立条目
降级为单元 纯配置 / 数值 / 曲线读取,不依赖完整游戏栈 用 NewObject / LoadObject 直接断言,不走 FMapTestSpawner
保持手工 需要视觉 / 手感 / 听感判断 维持手工测试,不占用集成成本
删除 断言被其他用例完全覆盖,无独立信息量 从宪章移除

01 武器与装备系统

用例 处理 理由
EQ-01 初始武器状态 合并 → 进图冒烟 单断言(已装备实例 > 0),本质是 FMapTestSpawner 健康检查,并入”Pawn 生成”冒烟,不配独立用例
EQ-02-01 滚轮轮切 保留为集成 切换链路回归;与 -02 合为一个”切换入口”用例的两个子状态
EQ-02-02 数字键切换 合并 → EQ-02 与 -01 同属切换功能,断言结构相同(槽位索引变化)
EQ-02-03 切空槽 保留为集成 边界行为(无反应保持当前),断言廉价且防回归
EQ-02-04 快速切换 保留为集成 并发 / 时序风险,回归重灾区
EQ-02-05 切换冷却 保留为集成 状态机时序,防”无冷却连切”回归
EQ-03 装备武器 保留为集成 拾取链路(Spawner → 重叠 → 快捷栏),是后续所有武器用例的前提
EQ-04 重复装备 合并 → EQ-03 同一拾取入口的防重入分支,作为 EQ-03 子状态
EQ-05 槽满拾取 合并 → EQ-03 同上,拾取三态(正常 / 重复 / 满槽)一个用例覆盖
WF-01-01 单发射击 保留为集成 弹药 -1 + 出弹,核心回归 + 弹药链路冒烟
WF-01-02 连发射击 合并 → WS-03 “按住持续出弹”与弹药递减断言重叠,打空行为由 WS-03 覆盖
WF-01-03 移动射击 保留为集成(低优先) 移动中射击功能正常;散布放大部分并入 SP-03-04,避免重复
WF-01-04 射击中被打断 合并 → DT-01-04 “死亡中断射击”是死亡禁交互的子集,DT-01-04 已覆盖
WF-02-01 自动换弹 保留为集成 弹药 0 → 自动换弹是独立状态机路径
WF-02-02 主动换弹 保留为集成 换弹主路径;吸收 -03 满弹匣分支
WF-02-03 满弹匣换弹 合并 → WF-02-02 同一入口的边界分支(无变化),并入作为子状态
WF-02-04 射击中换弹 保留为集成(与 -05 合成一例) 互斥时序风险,两方向合成”换弹射击互斥”用例
WF-02-05 换弹中射击 合并 → WF-02-04 同上,与 -04 合为一个用例的两个子状态
WS-01 距离衰减 降级为单元 GetDistanceAttenuation 是曲线求值,NewObject 武器实例即可;如需端到端只保留 1 个集成冒烟
WS-02 射速 降级为单元 LoadObject 读 fireDelayTimeSecs 配置,无需 PIE(宪章代码思路已注明)
WS-03 弹药量 降级为单元 + 并入 WF-01-01 容量配置读取为单元;递减行为并入 WF-01-01 集成断言
WS-04 部位伤害 降级为单元(-03 保留集成) 倍率数据校验(≥0、≤10)为单元;-03 实际爆头 2 倍伤害保留集成(伤害链路)
WA-01~03 动画 保持手工 视觉 / 手感判断

📌 2026-08-03 实际执行:WF-01~02 全组已用蓝图功能测试完成(ShooterTests/Blueprint/weapon/),未按上表”保留为集成”走 CQTest。与宪章决策不符,先按蓝图执行(记录偏差)。WS-03 递减断言由蓝图 WF_01_01/02 覆盖;CQTest 侧 WeaponSystem_Firing.spec.cpp 仅覆盖 WF-01-04,与蓝图重复,作补充保留。

02 控制系统

用例 处理 理由
MV-01 方向移动 保留为集成(四方向合并循环) 输入 → 移动链路冒烟 + 回归;-01~04 在代码里用循环遍历,不写四个重复断言
MV-01-05 斜向移动 保留为集成 作为 MV-01 子状态
MV-02 停止移动 合并 → MV-01 “松键后速度归零”是 MV-01 的隐含断言;风险点(快速反向滑行)作为 MV-01 附加断言
MV-03 移动中转向 保留为集成 视角 → 移动方向转向链路
AM-01-01 平地跳跃 保留为集成 跳跃链路基线,同时承载 NV-01 的跳高断言
AM-01-02 移动中跳跃 保留为集成 子状态
AM-01-03 蹲跳 删除(并入 AM-02-04) 与 AM-02-04 完全重复,保留蹲下用例内的那一份
AM-01-04 连续跳跃 保留为集成 落地时机边界,回归价值高
AM-01-05 空中移动 保留为集成 空中控制子状态
AM-02-01/02 蹲下/站起 保留为集成(合成一例往返) 碰撞高度变化断言,一个用例两个子状态
AM-02-03 蹲下行进 保留为集成 速度受限断言,子状态
AM-02-04 蹲跳 保留为集成 吸收 AM-01-03
AM-02-05 矮通道 保持手工 几何 / 视觉判断
AM-03 Dash 全部 保持手工 蓝图 GA,已决策转手工
LC-01 视角控制 保持手工 手感判断
AN-01 动画 保持手工 视觉判断
NV-01 跳跃速度 合并 → AM-01 中间值 / 最大值 = 跳高实测,并入 AM-01;a-ε / 0 风险点降级为单元 clamp 校验
NV-02 Dash 冷却 保持手工 已决策
NV-03 重力 合并 → AM-01 下落行为并入跳跃用例;负重力 / 除零风险点降级为单元
NV-04 灵敏度 保持手工 手感判断
NV-05 MaxWalkSpeed 合并 → MV-01 半速 / 全速断言并入方向移动;负值 / 0 风险点降级为单元
FR-01/02 死亡冻结 删除 与 DT-01(移动冻结)、DT-06-04(重生恢复)断言重复,实现 DT 后移除

10 事件测试

用例 处理 理由
DT-01 死亡状态机与冻结 保留为集成 死亡状态机核心链路,吸收 FR-01
DT-02 死亡摄像机 合并 → DT-01-05(-01 相机切换);-02 保持手工 相机模式切换需校验模式栈,手工无法判断(2026-08-03);重生恢复可观察
DT-03 死亡状态标签 保留为集成 GAS 标签断言,回归价值高
DT-04 网络同步 保留为集成(后续排期) 复制 / 回滚是高风险区,暂缓但不可删
DT-05-01 连续致死 保留为集成 状态机幂等,防双重广播
DT-05-02 空中死亡 合并 → DT-01-03 与”死亡后碰撞禁用”同链路的变体,并入碰撞冻结用例
DT-05-03 蹲伏中死亡 保留为集成 碰撞高度残留风险
DT-05-04 Dash 中死亡 保持手工 已决策
DT-06 重生完整性 保留为集成 状态重置回归核心,吸收 FR-02
DT-07-01 重生后立即死亡 保留为集成 时序风险(重生 → 死亡快速转换)
SP-01 热量累积 / 冷却 保留为集成 数值状态机核心;HeatPerShot=0 除零风险点降级为单元 clamp 校验
SP-02-01 曲线求值 降级为单元 曲线求值无需游戏栈
SP-02-02/03 弹孔散布 保留为集成 实际弹孔分布需要 PIE
SP-03 修正乘数 保留为集成 状态组合 + 过渡(吸收 WF-01-03 散布断言)
SP-04 首发精度 保留为集成 状态标志回归
SP-05 冷却延迟 保留为集成(-03 降级单元) 时序断言保留;负值 clamp 校验为单元
SP-06 曲线边界 -01 降级为单元,-02/03 保留集成 ClampMin 校验为单元;分布实测为集成

其它系统(03~08、性能)

系统 主要用例目的 说明
03 相机系统 CM-01/02/05 🟠 验收;NV-01/02 ⚪ 回归·单元 数值边界自动化,功能靠视觉/手感(CM-03/CM-04 已删:无可调 FOV / 无相机切换;NV-03 已删)
04 HUD 全部 🟠 验收 纯视觉判断,无自动化
05 前端页面 FE-02/05 🔵 冒烟;其余 🟢 回归 启动/加载链路为冒烟,流程类为回归(FE-01 已删:与 FE-05 重复)
06 音频系统 AU-01~04 🟠 验收;NV-01/02 ⚪ 回归·单元 听感手工,音量边界走单元
07 队伍系统 TM-01~05 🟠 验收 多人场景手工(NV-01 已删:游戏无队伍数量参数)
08 初始化链路 IN-01/02/04 🔵 冒烟;IN-03/05、NV-01 🟢 回归 链路基线为主;IN-03 与 DT-06 重叠,建议并入
性能测试 PF-01~12 全部 🟢 回归 性能门禁:帧时间 / 内存 / 网络延迟

明细汇总(01/02/10 三系统)

系统 保留集成 合并 降级单元 保持手工 删除
01 武器与装备 8 7 4 3 0
02 控制系统 4 5 3(部分子状态) 6 2
10 事件测试 11 1 4(部分子状态) 2 0

落地建议

  1. 宪章加”用例目的”列 ✅ 已完成(全部系统宪章,2026-08-03):为每个用例标注 冒烟 / 回归 / 回归·单元 / 验收。
  2. 实现时按清单合并:MV-01 四方向循环、EQ-03 拾取三态、WF-02-04/05 互斥一例,先合并再写代码,避免重复断言堆量。
  3. NV / WS 风险点统一走单元:负值 / 零值 clamp 校验全部降级为 NewObject / LoadObject 直接断言,只在行为类用例(AM-01、MV-01)里保留实测断言。
  4. FR-01/02 在 DT-01 / DT-06 实现通过后移除,避免两套用例断言同一件事。

13 — 测试辅助 API 设计方案

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

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


1. 背景与动机

1.1 问题

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

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

1.2 设计理念

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

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

1.3 架构分层

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

1.4 设计目标

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

2. 模块结构

2.1 模块信息

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

2.2 Gameplay 查询特殊归属

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

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

路径:

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

2.3 文件组织

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

2.4 编译控制

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

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

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


3. 函数库 API 参考

3.1 ULyraTestInputLibrary — 输入模拟

蓝图分类: Lyra|Test|Input

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

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

注意事项:

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

与 ECF 配合示例:

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

3.2 ULyraTestWorldLibrary — 世界查询

蓝图分类: Lyra|Test|World

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

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


3.3 ULyraTestGameplayLibrary — Gameplay 系统查询

蓝图分类: Lyra|Test|Gameplay

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

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

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

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

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

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

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

注意事项:

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

3.4 ULyraTestUtilityLibrary — 工具函数

蓝图分类: Lyra|Test|Utility

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

4. 版本记录

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

文档结束


15 — Lua Test DSL 设计方案(基于 LuaMachine)

本文档记录 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**(v2025-06-04,活跃维护,支持 UE5,Luau 方言)。

1.2 动机

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

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

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

最终测试工作流比例:

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

1.3 设计目标

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

2. 架构分层

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

2.1 三层职责

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

2.2 与现有体系的关系

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

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

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

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



Phase 依赖关系

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

└──→ Phase 5 (CI/CD)

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

3. 实施阶段

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

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

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

工作项

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

验收标准

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

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

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

工作项

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

DSL 示例

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

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

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

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

suite:run()

验收标准

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

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

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

与 LuaMachine 方案的核心差异

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

工作项

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

验收标准

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

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

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

工作项

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

验收标准

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

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

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

工作项

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

验收标准

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

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

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

工作项

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

验收标准

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

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

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

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

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

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

GameFeature 懒加载处理策略:

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

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

4. 模块结构

4.1 文件组织

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

4.2 模块依赖

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

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

5. 关键设计决策

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

6. 风险与缓解

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

7. 与 UnrealMCP 的协同

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

Lua Test DSL 注册到 Automation Framework 后:

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


UnrealMCP (HTTP :2929/mcp)


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


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

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


8. 版本记录

日期 版本 变更内容
2026-07-22 v1.0 初始版本 — 6 阶段实施计划 + 架构设计 + 验收标准(当时基于 UnLua)
— 确立 LuaMachine + Lua DSL 方案
— 复用 LyraTestUtilities / Automation Framework / UnrealMCP
2026-07-22 v1.1 定位修正 — Lua DSL 从「并行选项」升级为「蓝图替代品」
— 明确目标比例:80% Lua DSL + 20% C++ CQTest,蓝图逐步淘汰
— Phase 6 扩展为「蓝图→Lua 迁移」含分批策略 + 迁移指南
— 新增「蓝图节点→Lua DSL 等价写法」迁移速查表要求
2026-07-22 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 Test DSL 重写后的完整 API 参考手册。
新 API 按引擎底层概念组织为 7 个显式子命名空间,而非扁平的 ctx 方法。
原扁平 API(ctx:GetAmmo / ctx:Click 等)已移除,迁移到 ctx.input / Helpers/*.lua
设计文档见 15-UnLuaTestDSL设计方案


1. 快速开始

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

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

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

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

suite:run()

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


2. DSL 语法规范

2.1 TestSuite 定义

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

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

2.2 Test 定义

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

2.3 生命周期钩子

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

执行顺序:

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

2.4 参数化测试

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

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

2.5 标签过滤

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

3. 命名空间 API 完整参考

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

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

3.2 ctx.ability — GAS 通用操作

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

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

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

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

3.4 ctx.asset — 资产操作

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

3.5 ctx.input — 输入模拟

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

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

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

3.7 ctx.config — 配置

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

4. 顶层方法(保留在 ctx

4.1 流程控制

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

4.2 断言

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

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

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

4.3 其他

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

5. Helpers 层 API

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

5.1 StateHelper

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

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

5.2 WeaponHelper

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

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

5.3 CombatHelper

require("Tests.Helpers.CombatHelper")

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

5.4 TeamHelper

require("Tests.Helpers.TeamHelper")

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

5.5 InventoryHelper

require("Tests.Helpers.InventoryHelper")

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

6. 调用约定速查

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

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

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

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

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

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

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

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

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

7. 类型转换说明

Lua ↔ UE 类型映射

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

示例

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

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

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

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

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

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

9. 已知限制与计划

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

10. 版本记录

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

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


17 — Lua 测试框架搭建复盘

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


0. 一句话结论

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

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

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


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

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

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

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

方案演进:

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

2. 实际发生的时间线

日期 事件 产物 / 证据
2026-07-19 测试辅助 API 方案 v1.0(蓝图优先) TestDocs/00-总纲/13-测试辅助API设计方案.md
2026-07-22 Lua DSL 设计方案 v1.0→v1.2(UnLua → LuaMachine);API 命名空间与混合桥接决策 15-*docs/adr/0001-*0002-*
2026-07-25 DSL Runtime 与学习记录:metatable 是命名空间基石 learning-records/0001-*
2026-07-26 API 参考 v2.0:7 个命名空间 + 5 个 Helper + 反射入口;7 个测试脚本迁移 16-LuaDSL-API参考.md
2026-07-27 PIE 集成 handoff:发现 Automation 中测试”跑通”但 PIE 从未启动 .reasonix/handoffs/handoff-lyra-lua-test-dsl-pie-plan-d.md
2026-08-02 决策:弃用蓝图功能测试;事件测试改用 CQTest 显式驱动;13 号方案冻结 00-测试总纲.mddocs/adr/0003-*、13 号方案头部冻结标记
2026-08-03 测试体系收敛为 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(7/26)标注了 6 个 stub(GetAttribute / GetEffectDuration / GetCVar / GetDeveloperSettings / Tick 回调等),次日 PIE 集成 handoff 又发现”测试跑通但 PIE 从未启动”。框架的地基问题是在投入大量工作量之后才被验证的。

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

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

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

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

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

后果:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7. 结论与当前状态

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

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

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

Lua 框架资产状态

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

8. 证据索引

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

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


Lyra 测试封板审查材料

目的:为封板审查环节提供一份可逐项核对的证据索引。审查人按第 7 节核对点逐项打勾;审查通过后,据此撰写《Lyra 测试总结报告》。

项目:LyraStarterGame(UE 5.8)|审查日期:2026-08-03|审查人:__________


1. 封板口径(审查基线)

  • 功能封板(2026-08-03 决定,决策 A):自动化 CQTest 全量通过(96/96)+ 无 🔴 阻塞 Bug 即为封板;性能全量、探索性测试、DT-04 网络、SP 剩余子状态等无法在封板日完成的项目,显式列入《已知缺口 / 封板后 backlog》(第 6 节),不影响封板声明。
  • 手工测试 Session 1~3 已完成(2026-08-03)。佐证:TestDocs/Bug报告/ 4 份 Bug 报告 + TestDocs/03-实施/14-蓝图功能测试实战-WF-01-01.md
  • 不推上线:本轮全部成果仅本地 commit(2026-08-03)。

2. 自动化证据索引(96/96 全绿,2026-08-03)

套件 测试文件(Source/LyraGame/Tests/ 用例数 结果 运行方式 说明
WeaponSystem(EQ/WS/WF 数值) CQTest/WeaponSystem_Equipment.spec.cpp + CQTest/WeaponSystem_Numerics.spec.cpp + CQTest/WeaponSystem_Firing.spec.cpp 21 21/21 常驻编辑器 MCP(AutomationTestToolset) 全量基线回归(02-full-suite-baseline,69/69 批次内)
ControlSystem(MV/AM/NV/FR) CQTest/ControlSystem.spec.cpp 30 30/30 同上 同上
EventSystem.Death(DT-01/03/05/06/07) CQTest/EventSystem_Death.spec.cpp(+ LyraEventDeathTestAbility 13 13/13(65.5s) 同上 冷启动复核 13/13 全绿(01-verify-event-death-cqtest
InitChain(IN-01~05) InitChain/ShooterMapsInit.spec.cpp 5 5/5 同上 冷启动复核 5/5 全绿
CameraSystem_Numeric(NV-01/02) CQTest/CameraSystem_Numeric.spec.cpp 9 9/9(0.3s,纯单元,无 PIE) 直接执行 默认 80 / meta [5,170] / Clamp 契约;俯仰 ±89 / ±89.9(03-camera-numeric-cqtest
AudioSystem_Numeric(NV-01/02 + UI 边界) CQTest/AudioSystem_Numeric.spec.cpp 8 8/8(0.13s,纯单元,无 PIE) 直接执行 5 通道默认 1.0 / setter clamp [0,1] / SourceRange / 通道独立(04-audio-numeric-cqtest
SpreadSystem(SP-01/02/05/06 单元 + SP-01-02/04 PIE) CQTest/SpreadSystem_Numeric.spec.cpp + CQTest/SpreadSystem_Functional.spec.cpp 10 10/10 单元直接执行 + MCP PIE(L_Expanse) 原型→正式化(08-sp-cqtest-prototype
合计 96 96/96

运行方式备注:

  • 批量回归顺序:Camera → Audio → Spread → Weapon → Control → Event → InitChain;每批前冷启编辑器,规避 L_Expanse socket / DT-06 偶发 flake。
  • 封板回归(2026-08-04 已完成):冷启动后全量复跑 96/96 全绿;DT-06/07 偶发失败已根因修复
    (双重生 churn 抢占 PlayerState ASC + 死亡能力异步授予,见 19 号审查 A 类处置提交 2f0b077),
    复跑 3 次隔离 + 套件内均绿;InitChain L_Expanse 5/5 通过。

3. 手工证据索引

Session 内容 状态 佐证
Session 1 武器动画 WA-0103 + 装备动作 EQ-0105 ✅ 已完成 Bug 报告:装备动画-武器先显示后播放拿出动作.mdWF-02-04-射击途中拒绝换弹指令(非阻塞).md
Session 2 控制动画 AN-01 + 视角 LC-01 + 相机 CM-01/02/05 ✅ 已完成 Bug 报告:AM-03-05-Dash中操作行为不一致.md
Session 3 HUD HD-0105 + 队伍 TM-0105 + 音频 AU-01~04 ✅ 已完成 无新增 Bug
蓝图功能测试 WF-01-0104、WF-02-0103(ShooterTests/Blueprint/weapon/ ✅ 已完成 TestDocs/03-实施/14-蓝图功能测试实战-WF-01-01.md(与宪章 CQTest 要求不符,按偏差记录)

4. 决策与偏差登记(审查重点)

# 决策 / 偏差 内容 出处 影响
1 WF-01/02 走蓝图功能测试 已用蓝图完成,与宪章”CQTest 实现”不符;只记录偏差,不补 CQTest 2026-08-03 决定 测试计划 §4.4、用例分级清单已标注
2 音频音量 clamp = 契约硬化,非 Bug C++ setter 原无 clamp;补 FMath::Clamp(InVolume, 0, 1) 使”负值→0”契约成立 2026-08-03(04-audio-numeric-cqtest 生产改动 Settings/LyraSettingsLocal.cpp(5 个 Set*Volume);不另开 Bug 报告
3 SP 散布系列:原型验证 → 正式化 原型可行即正式推进,10/10 全绿 2026-08-03 决定(08-sp-cqtest-prototype 剩余子状态进 backlog(§6)
4 用例删除 / 合并 状态系统整组(2026-08-02);NV-01 队伍数值、CM-03/04、HD-01-06、EQ-04 并入等(2026-08-03) TestDocs/00-总纲/用例分级清单.md + 各系统宪章 宪章同步;删除项均有理由
5 DT-04 网络同步暂缓 单机环境本轮不做,保留”暂缓但不可删” 用户(05-dt04-network-scope 已知缺口(§6)
6 性能 36 实例不可行 本机需 3~6 小时,封板日无法全量;建议骨架 + 冒烟验证管线 06-perf-test-feasibility + TestDocs/02-专项测试/13-性能测试可行性调研.md 已知缺口(§6)
7 DT-06 / L_Expanse 偶发 flake 会话负载下偶发失败(Until 超时 / socket_send_failure),冷启动后复跑全绿(13/13、5/5) 地图 Not yet specified 封板回归需冷启动后复核(§2)

5. Bug 登记(4 份,无 🔴 阻塞)

编号 标题 等级 来源 状态
NV-03-02 GravityScale=0 时 ABP_Mannequin_Base 除零 🟡 中(非阻塞 · LogScript Warning) CQTest(自动化发现的唯一真实产品 Bug) 已知问题,随封板发布
AM-03-05 Dash 中操作行为不一致(蹲下排队/切换打断/部分输入无效) 🟡 严重 手工 已知问题,随封板发布
WF-02-04 射击途中换弹被静默拒绝 🟡 严重(非阻塞) 手工(蓝图功能测试) 已知问题,随封板发布
装备动画 装备时武器先显示、后播放拿出动作(闪烁) 🟢 一般 手工 已知问题,随封板发布

等级说明:无 🔴 阻塞 Bug → 满足功能封板条件;🟡 严重项均为”行为不一致 / 交互时序”类,不影响崩溃与数据安全,作为已知问题发布。

6. 已知缺口 / 封板后 backlog

内容 依据
DT-04 网络同步 PIE 双开验证 DeathState 复制 / 回滚 暂缓决策
性能测试 PF-0112 × 3 地图 = 36 实例;本机 36h,建议先做骨架 + 冒烟管线 性能可行性调研
探索性测试 6 轮 × 90 分钟(8/1-8/2 计划) 执行计划阶段四
SP 剩余子状态 SP-02-02/03 弹孔实测(统计型)、SP-03-01~06 乘数动态与过渡、SP-04 首发精度(需确认资产 bAllowFirstShotAccuracy)、SP-05-01/02 冷却延迟时序 08-sp-cqtest-prototype
前端页面(FE) FE-02~06 Spec/Gauntlet 覆盖不完整(Elimination 菜单已实现) 测试计划 §4.4
环境 flake 防护 DT-06 移动输入解锁 Until 超时、L_Expanse LogHttpConnection socket_send_failure;冷启动可复跑通过,已记录复跑方法 地图 Not yet specified

CQTest 代码审查报告

审查日期:2026-08-03
审查范围:Source/LyraGame/Tests/CQTest/ 全部测试文件
审查类型:代码质量 + 测试有效性 + 稳定性 + 代码风格


1. 审查摘要

类别 发现数 严重程度
代码质量问题 6 🟡 中
测试有效性问题 3 🟡 中
潜在稳定性问题 3 🟡 中
代码风格问题 4 🟢 低

结论:测试代码整体质量良好,无🔴阻塞问题。发现的问题均为非阻塞,不影响测试执行结果。


2. 代码质量问题

2.1 弱断言(WeaponSystem_Equipment.spec.cpp)

问题描述:断言条件过于宽松,无法有效验证预期行为。

行号 当前断言 问题 建议修复
280 ASSERT_THAT(IsTrue(QuickBar->GetActiveSlotIndex() >= -1)); ActiveSlotIndex永远不会是-1,断言永远通过 改为验证槽位在有效范围内
330 ASSERT_THAT(IsTrue(QuickBar->GetActiveSlotIndex() >= -1)); 同上 同上

修复建议

1
2
3
// 改为验证槽位在有效范围内
ASSERT_THAT(IsTrue(QuickBar->GetActiveSlotIndex() >= 0));
ASSERT_THAT(IsTrue(QuickBar->GetActiveSlotIndex() < QuickBar->GetSlots().Num()));

2.2 条件永远为真(ControlSystem.spec.cpp)

问题描述:第744行条件逻辑错误。

1
2
3
4
5
6
// 当前代码(第744行)
return Character->GetVelocity().Size() > 0.0f || Character->GetVelocity().Size() < 0.1f;

// 问题:Size()总是>=0,所以第一个条件总是为真(除非Size()==0)
// 第二个条件Size() < 0.1f在Size() < 0.1时为真
// 整个条件永远为真

修复建议

1
2
// 改为验证速度在预期范围内
return Character->GetVelocity().Size() < 100.0f; // 负值MaxWalkSpeed不应产生高速移动

3. 测试有效性问题

3.1 EQ_02_04_RapidRepeatedSwitching测试

问题描述:测试未真正验证快速切换的稳定性。

当前测试逻辑

  1. 快速切换20次
  2. 断言GetActiveSlotIndex() >= -1(永远通过)

改进建议

1
2
3
4
5
6
7
8
9
10
11
12
// 记录切换前后的槽位状态
int32 SlotBefore = QuickBar->GetActiveSlotIndex();
// 执行快速切换
for (int32 i = 0; i < NumIterations; ++i)
{
QuickBar->SetActiveSlotIndex(i % 3);
QuickBar->CycleActiveSlotForward();
}
// 验证最终状态有效
int32 SlotAfter = QuickBar->GetActiveSlotIndex();
ASSERT_THAT(IsTrue(SlotAfter >= 0));
ASSERT_THAT(IsTrue(SlotAfter < QuickBar->GetSlots().Num()));

3.2 EQ_02_05_SwitchCooldownDelay测试

问题描述:测试未验证冷却时间,只验证了两次切换都能成功。

改进建议:如果需要验证冷却时间,应测量两次切换之间的时间间隔。

3.3 NV_05_01_MaxSpeedNegative测试

问题描述:条件逻辑错误导致测试永远通过。

修复建议:见第2.2节。


4. 潜在稳定性问题

4.1 使用Sleep等待异步操作

问题描述:WeaponSystem_Equipment.spec.cpp第394行使用FPlatformProcess::Sleep(1.0f)等待异步操作完成。

1
2
// 第394行
FPlatformProcess::Sleep(1.0f);

风险

  • 在不同硬件上表现不一致
  • 可能导致测试超时
  • 浪费测试执行时间

改进建议:使用CQTest的WaitUntil机制替代Sleep

4.2 时序依赖测试

问题描述:EventSystem_Death.spec.cpp中的测试依赖特定时序。

1
2
// 第501行
.Until(TEXT("等待移动输入解锁"), [this]() { return !PC->IsMoveInputIgnored(); })

风险

  • 在不同帧率下表现不一致
  • 可能导致偶发失败(Flake)

改进建议:添加超时机制,避免无限等待。

4.3 依赖特定环境

问题描述:所有测试都依赖特定的地图(L_Expanse、L_ShooterTest_DeviceProperties等)。

风险

  • 地图变更可能导致测试失败
  • 地图加载失败导致测试失败

改进建议:测试应具有更好的环境独立性。


5. 代码风格问题

5.1 命名规范

问题描述:ControlSystem.spec.cpp第37行成员变量命名不符合UE风格。

1
2
3
4
5
// 当前
int FrameCount = 0;

// UE风格建议
int32 FrameCount = 0; // 使用int32而非int

5.2 类型转换

问题描述:WeaponSystem_Numerics.spec.cpp第407行使用C风格类型转换。

1
2
3
4
5
// 当前
float* Value = (float*)MapHelper.GetValuePtr(i);

// 建议
float* Value = static_cast<float*>(MapHelper.GetValuePtr(i));

5.3 缩进不一致

问题描述:WeaponSystem_Numerics.spec.cpp第399行和442行缩进不一致。

1
2
3
4
5
// 当前(第399行)
const FMapProperty* Prop = CastField<FMapProperty>(...

// 应改为
const FMapProperty* Prop = CastField<FMapProperty>(...

5.4 注释风格

问题描述:部分测试缺少详细的测试目标说明。

建议:每个TEST_METHOD都应包含:

  • 目标(验证什么行为)
  • 参数(被测参数)
  • 有效区间(参数范围)
  • 预期(期望结果)

6. 文件审查详情

6.1 WeaponSystem_Equipment.spec.cpp

  • 测试用例数:8
  • 发现问题:3(弱断言2个,Sleep1个)

6.2 ControlSystem.spec.cpp

  • 测试用例数:20
  • 发现问题:2(弱断言1个,命名1个)

6.3 WeaponSystem_Firing.spec.cpp

  • 测试用例数:1
  • 发现问题:0(代码质量良好)

6.4 WeaponSystem_Numerics.spec.cpp

  • 测试用例数:7
  • 发现问题:2(类型转换1个,缩进1个)

6.5 EventSystem_Death.spec.cpp

  • 测试用例数:10
  • 发现问题:1(时序依赖)

6.6 CameraSystem_Numeric.spec.cpp

  • 测试用例数:9
  • 发现问题:0(代码质量良好)

6.7 AudioSystem_Numeric.spec.cpp

  • 测试用例数:8
  • 发现问题:0(代码质量良好)

6.8 SpreadSystem_Numeric.spec.cpp

  • 测试用例数:6
  • 发现问题:0(代码质量良好)

6.9 SpreadSystem_Functional.spec.cpp

  • 测试用例数:4
  • 发现问题:0(代码质量良好)

6.10 InitChain/ShooterMapsInit.spec.cpp

  • 测试用例数:5
  • 发现问题:0(代码质量良好)

7. 总结

优点

  1. 测试结构清晰,注释详细
  2. 使用CQTest框架,测试可维护性好
  3. 测试覆盖了关键功能点
  4. 有详细的测试策略说明

需改进

  1. 弱断言需要增强
  2. Sleep应替换为WaitUntil
  3. 代码风格需要统一
  4. 部分测试有效性需要提升

建议优先级

  1. 高优先:修复弱断言(第2.1节)
  2. 中优先:修复条件逻辑错误(第2.2节)
  3. 低优先:统一代码风格(第5节)

8. 附录

审查文件列表

  • Source/LyraGame/Tests/CQTest/WeaponSystem_Equipment.spec.cpp
  • Source/LyraGame/Tests/CQTest/WeaponSystem_Numerics.spec.cpp
  • Source/LyraGame/Tests/CQTest/WeaponSystem_Firing.spec.cpp
  • Source/LyraGame/Tests/CQTest/ControlSystem.spec.cpp
  • Source/LyraGame/Tests/CQTest/EventSystem_Death.spec.cpp
  • Source/LyraGame/Tests/CQTest/CameraSystem_Numeric.spec.cpp
  • Source/LyraGame/Tests/CQTest/AudioSystem_Numeric.spec.cpp
  • Source/LyraGame/Tests/CQTest/SpreadSystem_Numeric.spec.cpp
  • Source/LyraGame/Tests/CQTest/SpreadSystem_Functional.spec.cpp
  • Source/LyraGame/Tests/InitChain/ShooterMapsInit.spec.cpp

Lyra 测试总结报告

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


1. 结论

  • 自动化 CQTest 全量 96/96 全绿(7 套件;2026-08-04 冷启动回归复跑确认,含 DT-06/07 竞态修复后验证)
  • 无 🔴 阻塞 Bug;4 份已知问题(🟡×3、🟢×1)随封板发布,不阻塞
  • 手工测试 Session 1~3 已完成(2026-08-03)
  • 允许功能封板;不推上线,成果本地 commit(2026-08-03)

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

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

3. 手工与视觉验证

  • Session 1(武器动画 WA-0103、装备 EQ-0105):✅ 完成
  • Session 2(控制动画 AN-01、视角 LC-01、相机 CM-01/02/05):✅ 完成
  • Session 3(HUD HD-0105、队伍 TM-0105、音频 AU-01~04):✅ 完成
  • 蓝图功能测试 WF-01-0104 / WF-02-0103:✅ 完成(与宪章”CQTest 实现”不符 → 记录偏差,不补 CQTest)
  • Bug 报告 4 份:装备动画时序、WF-02-04 换弹互斥、AM-03-05 Dash 行为不一致、NV-03-02 动画蓝图除零(CQTest 发现)

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 重生 churn 竞态修复 封板回归中发现:双重生 Pawn 抢占 PlayerState ASC + 死亡能力异步授予,导致”血量归零但死亡状态不转换”;加 3 帧稳定/一致性/死亡能力门禁后确定性通过(DT_07 连跑 3 次 + 套件内均绿)

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

编号 标题 等级 来源
NV-03-02 GravityScale=0 时 ABP_Mannequin_Base 除零 🟡 中(非阻塞 Warning) CQTest
AM-03-05 Dash 中操作行为不一致 🟡 严重 手工
WF-02-04 射击途中换弹被静默拒绝 🟡 严重(非阻塞) 手工/蓝图
装备动画 装备时武器先显示、后播放拿出动作 🟢 一般 手工

6. 已知缺口 / 封板后 backlog

内容
DT-04 网络同步 PIE 双开 DeathState 复制(暂缓)
性能测试 PF-0112 × 3 地图 = 36 实例;本机 36h,建议骨架 + 冒烟管线
探索性测试 6 轮 × 90 分钟
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 竞态已修复,方法留档)

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 个用例(含 14 个子状态)
4 武器动画 装备/换弹/射击动画,共 3 个用例

测试计划

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

详细分配

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

测试细则

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

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

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

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

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

代码实现思路

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

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

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

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

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

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

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

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

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

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

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

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

📌 实际执行(2026-08-03):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 ⚠️ 射击途中换弹 正在执行射击动作 在换弹过程中按换弹键 射击被打断执行换弹 🔴 高 🖐️ 手工补验(2026-08-03 决策:蓝图未覆盖;互斥时序,与 -05 合一例)
WF-02-05 ⚠️ 换弹途中射击 正在执行换弹动作 换弹过程中被射击指令 换弹被打断,弹药不恢复,换弹动画停止 🟡 中 🚫 并入 WF-02-04(手工补验)

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

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

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

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

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

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

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


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

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

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

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

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

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

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

代码实现思路

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

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

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

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

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

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

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

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


控制系统 — 测试宪章

测试范围

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

测试计划

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

详细分配

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

代码实现思路

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

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

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

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

相机系统 — 测试宪章

测试范围

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

测试计划

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

详细分配

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

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

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

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

🚫 2026-08-03 删除说明: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 与变焦 — 🚫 已删(2026-08-03)

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

4. 模式切换与过渡 — 🚫 已删(2026-08-03)

🚫 删除说明:游戏不存在相机模式切换(能力相机/默认相机无接入,仅 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 混合时间参数验证 — 🚫 已删(2026-08-03)

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

代码实现思路

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

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

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

HUD — 测试宪章

测试范围

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

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


测试计划

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

详细分配

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

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

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

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

🚫 2026-08-03 删除说明:游戏无空手状态(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 加载动画 🚫 已删(2026-08-03:与 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. 加载动画 — 🚫 已删(2026-08-03)

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

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

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

代码实现思路

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

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

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

代码实现思路

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

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

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

代码实现思路

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

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

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

代码实现思路

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

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

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

代码实现思路

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

音频系统 — 测试宪章

测试范围

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

测试计划

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

详细分配

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

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

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

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

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

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

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

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

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

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

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

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

代码实现思路

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

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

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

队伍系统 — 测试宪章

测试范围

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

测试计划

测试方式 适用用例
🖐️ 手工测试 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) 🚫 已删(2026-08-03)

🎯 用例目的图例:🔵 冒烟(环境/链路基线,失败优先怀疑测试环境)|🟢 回归(防回归保护)|⚪ 回归·单元(数值/配置校验,降级为单元级断言,不走 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 换队后颜色更新 切换队伍后 切换到另一队 玩家颜色/标记更新为新队伍 🟡 中 🚫 删除(游戏无换队功能,2026-08-03)

🚫 2026-08-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. 数值测试 — 🚫 已删(2026-08-03)

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


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

测试范围

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

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


测试计划

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

详细分配

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

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

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

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

代码实现思路

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

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

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

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

代码实现思路

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

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

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

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

代码实现思路

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

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

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

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

代码实现思路

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

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

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

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

代码实现思路

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

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

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

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

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

代码实现思路

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

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

状态系统 — 测试宪章

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

测试范围

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

测试计划

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

详细分配

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

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

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

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

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

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

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

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

代码实现思路

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

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

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

事件测试 — 测试宪章

测试范围

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

📦 2026-08-02 交付范围: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 单机死亡/重生链路(2026-08 回归 CQTest,弃用蓝图功能测试)
🤖 C++ CQTest(后续) DT-04(网络同步)、SP-01 ~ SP-06 PIE Network + CQTest 实现

详细分配

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

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

1. 死亡

设计依据

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

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

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

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)

🤖 2026-08-03 修正:相机模式切换(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(相机切换需校验相机模式栈,手工无法判断,2026-08-03)
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 销毁(2026-08-02 按代码修正:原”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 风险:客户端预测死亡后回滚,如果移动冻结/碰撞禁用未正确恢复,角色变成幽灵

代码实现思路

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

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

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

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

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

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

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

2. 重生

设计依据

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

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

2026-08-02 决策: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 状态机未正确重置

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

3. 散布系统

📦 后续排期(2026-08-02):本次交付不含散布系统,用例保留。

设计依据

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

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

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

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

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

3.1 热量累积与冷却

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

3.2 散布与热量映射

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

3.3 散布修正乘数

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

3.4 首发精度

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

3.5 散布恢复冷却延迟

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

3.6 散布曲线边界值

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

代码实现思路

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

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

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

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

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

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

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

性能测试 — 测试宪章

测试范围

# 测试项 说明
1 帧率影响 30/60/120fps 及不稳定帧率下各系统的表现
2 网络延迟影响 50/100/200/500ms 延迟下操作响应的正确性
3 逐帧检测 Frame time、卡顿检测、内存帧级监控

全部自动化测试。通过模拟不同帧率和延迟环境,逐帧采集性能数据。
每个测试用例在 3 张不同地图上各执行一次,共 36 个测试实例。


测试计划

测试方式 适用用例
🤖 自动化测试 全部 12 个用例 × 3 地图 = 36 个实例

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

1. 帧率影响

编号 用例名 前置条件 测试步骤 预期结果 优先级 用例目的
PF-01 30fps 下功能正常 锁定帧率为 30fps 执行移动/射击/切换武器 功能正常,帧生成时间 ≤ 33ms 🟡 中 🟢 回归(性能基线)
PF-02 60fps 下功能正常 锁定帧率为 60fps 执行移动/射击/切换武器 功能正常,帧生成时间 ≤ 16ms 🔴 高 🟢 回归(性能基线)
PF-03 120fps 下功能正常 锁定帧率为 120fps 执行移动/射击/切换武器 功能正常,帧生成时间 ≤ 8ms 🟡 中 🟢 回归(性能基线)
PF-04 不稳定帧率下无卡顿 帧率在 30~120fps 间波动 持续操作 无单帧超过 100ms 的卡顿 🟡 中 🟢 回归(性能鲁棒性)
PF-05 帧率骤降后恢复 从 60fps 突降至 15fps 再恢复 持续操作 恢复后功能正常,无残留异常 🟡 中 🟢 回归(性能鲁棒性)

2. 网络延迟影响

编号 用例名 前置条件 测试步骤 预期结果 优先级 用例目的
PF-06 50ms 延迟下操作响应 模拟 50ms RTT 射击/移动 客户端操作即时响应,命中判定准确 🔴 高 🟢 回归(网络性能)
PF-07 100ms 延迟下操作响应 模拟 100ms RTT 射击/移动 客户端操作即时响应,命中判定可接受 🔴 高 🟢 回归(网络性能)
PF-08 200ms 延迟下操作响应 模拟 200ms RTT 射击/移动 客户端操作即时响应,延迟可感知但功能正常 🟡 中 🟢 回归(网络性能)
PF-09 500ms 延迟下操作响应 模拟 500ms RTT 射击/移动 功能正常,无断线或操作卡死 🟡 中 🟢 回归(网络性能)
PF-10 延迟抖动场景 延迟在 50~300ms 间波动 持续操作 不出现断线、瞬移或状态不一致 🟡 中 🟢 回归(网络抖动)

3. 逐帧检测

编号 用例名 前置条件 测试步骤 预期结果 优先级 用例目的
PF-11 逐帧内存检测 稳定 60fps 运行 5 分钟,每帧记录内存 内存无持续增长(无泄漏),帧分配稳定 🔴 高 🟢 回归(内存门禁)
PF-12 逐帧帧时间检测 稳定 60fps 运行 5 分钟,每帧记录帧时间 平均帧时间 ≤ 16ms,99% 帧 ≤ 30ms 🔴 高 🟢 回归(帧时间门禁)

探索性测试

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

测试范围

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

轮次 聚焦模块 高危可能
1 武器 + 控制 + 状态 换弹中切换武器 → Dash → 射击;死亡瞬间按射击/换弹
2 控制 + 相机 + HUD 快速移动+转向时相机是否卡墙;跑射/跳射/蹲射的 HUD 反馈
3 队伍 + 武器 + HUD 多人同时射击同目标;友军伤害判定;比分板实时更新
4 前端 + 初始化链路 快速进出菜单/设置后返回战斗;断线重连后状态恢复
5 状态 + 初始化链路 连续死亡重生多次后是否有状态残留;重生后能力是否完整
6 音频 + 武器 + 控制 密集音效叠加(多人同时开火+语音+脚步);操作时音频延迟

记录方式

发现 bug 时记录:

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

性能测试(Gauntlet)可行性调研

2026-08-03 产出,来源均为本地引擎/仓库一手资料(见文末”来源”)。供《测试计划》阶段四 PF-01~12 的排期与封板决策引用。

结论摘要

  • 本机可行:UE 5.8 自带 Gauntlet 运行时(实验性插件,本项目 LyraGame.Build.cs 已依赖并编译通过)与 Gauntlet 自动化壳(AutomationTool)。Ryzen 7 6800H / 16 线程 / 32GB 内存足以单机跑帧率与逐帧检测类用例;网络延迟类用例需双进程(Server + Client),本机亦可。
  • 最大障碍是时间:36 个实例(12 用例 × 3 地图),每实例含一次冷启动 + 采样(PF-11/12 要求 5 分钟逐帧),估算全量需要半天以上,8/3 封板当天全量不现实。
  • 建议:封板前只跑代表性子集(如 PF-02/PF-12 各 1 地图 + PF-09 1 组延迟),其余标记为”已知缺口/排期外”,在总结报告中明示;或先产出控制器骨架与 1 个冒烟实例,验证管线后按需扩展。

环境与前置条件

现状 来源
Gauntlet 运行时插件 Engine\Plugins\Experimental\Gauntlet\(实验性);Source\LyraGame\LyraGame.Build.cs:61 已加 "Gauntlet" 依赖 本地源码
既有控制器 ULyraTestControllerBootTest / ULyraTestControllerStartEliminationTest(继承引擎 UGauntletTestControllerBootTest),已编译 Source\LyraGame\Tests\LyraTestController*.h/.cpp
控制器基类 API UGauntletTestControllerOnInit / OnTick(float) / OnPostMapChange(UWorld*) / GetWorld() / GetFirstPlayerController() / GetCurrentMap() / EndTest(int32) / MarkHeartbeatActive(...);通过命令行 -gauntlet=<控制器类名> 创建 Engine\Plugins\Experimental\Gauntlet\Source\Gauntlet\Public\GauntletTestController.h
Gauntlet 自动化壳(C#) Engine\Source\Programs\AutomationTool\Gauntlet\(UnrealTestNode / UnrealSession / UnrealRoleConfiguration 等),经 RunUATAutomationTool)驱动多进程与产物采集 Engine\Source\Programs\AutomationTool\Gauntlet\Unreal\Base\*.cs
构建目标 已有 Development 构建(Binaries\Win64\UnrealEditor-LyraGame.dll);性能测试用 LyraGame(打包/Game 目标)或编辑器目标均可 仓库构建产物
逐帧采集备选 UE 5.8 AutomatedPerfTesting 插件(Engine\Plugins\Performance\AutomatedPerfTesting,含 GauntletSettings.xml 模板)+ -trace(Unreal Insights .utrace 本地引擎

PF-01~12 落地映射

用例 手段 判定
PF-01/02/03 帧率锁定 30/60/120 启动参数或 OnTick 内 t.MaxFPS 控制台命令;每帧用 FApp::GetDeltaTime / FPlatformTime 采样帧时间 平均帧时间 ≤ 33/16/8ms
PF-04 帧率波动 OnTick 按预设曲线改 t.MaxFPS(30↔120) 无单帧 >100ms
PF-05 骤降恢复 60→15→60 阶梯改 t.MaxFPS,恢复后继续采样 恢复后帧时间回落,无残留异常
PF-06~09 延迟 50/100/200/500ms Gauntlet 双角色(Server + Client)单机双进程;net.PacketSimulationSettingsPktLag)或 -PktLag=<ms> 命令行注入 无断线/卡死,操作可完成
PF-10 延迟抖动 PktLag + PktLagVariance(50~300ms 波动) 无断线/瞬移/状态不一致
PF-11 逐帧内存 OnTick 每帧记录 FPlatformMemory::GetStats(),持续 5 分钟写 CSV/JSON;或 -trace 用 Insights 分析 无持续增长(线性回归斜率≈0)
PF-12 逐帧帧时间 OnTick 每帧记录帧时间 5 分钟;或 AutomatedPerfTesting 插件 平均 ≤16ms,99% ≤30ms

本机可行性评估

  1. 帧率类(PF-01~05):单进程即可,改动小(1 个 ULyraTestControllerPerfTest + 参数化地图/上限/时长)。每实例约 24 分钟(冷启动 + 3060s 采样)。
  2. 网络类(PF-06~10):Gauntlet 原生支持多角色(UnrealRoleConfiguration / -NumClients),单机 Server+Client 双进程约 4~8 分钟/实例;延迟注入用引擎自带 PktLag,无外部依赖。
  3. 逐帧类(PF-11/12):5 分钟 × 3 地图 × 2 用例 = 30 分钟起步(不含启动),每实例 6~8 分钟。
  4. 总耗时估算:36 实例 ≈ 3~6 小时(取决于采样时长与地图加载),封板日(8/3)无法全量。
  5. 风险点:实验性插件 API 变动(5.8 将 Gauntlet 运行时移入 Experimental 插件);双进程对显存/内存要求(32GB 内存够,GPU 单卡双开可接受);PF-04/05 的”无 100ms 单帧”断言对非独占模式(后台窗口)敏感,建议 -unattended -nosplash -noPause + 固定窗口模式。

建议(供 07 封板决策)

  • 本轮(8/3):实现 ULyraTestControllerPerfTest 骨架 + 跑 1 个冒烟实例(如 PF-02 × L_Expanse)验证 -gauntlet=EndTest 管线;PF-11/12 用自研 OnTick 采样(比 Insights 集成成本低)。
  • 若排期不允许:将 PF-01~12 全部记为”已知缺口(排期外)”,门禁降级为”性能骨架可运行 + 手工性能抽样记录”,并在总结报告写明理由。
  • 后续完整执行建议放封板后的回归周期,一次跑一个子集(按地图分组)。

来源

  • TestDocs/02-专项测试/10-性能测试.md(PF-01~12 用例定义与门禁)
  • Source/LyraGame/Tests/LyraTestControllerBootTest.h/.cppLyraTestControllerStartEliminationTest.h/.cpp(既有 Gauntlet 控制器)
  • Source/LyraGame/LyraGame.Build.cs:61(Gauntlet 依赖)
  • UE_5.8\Engine\Plugins\Experimental\Gauntlet\Source\Gauntlet\Public\GauntletTestController.h(控制器 API)
  • UE_5.8\Engine\Source\Programs\AutomationTool\Gauntlet\Unreal\Base\Gauntlet.UnrealTestNode.cs 等(Gauntlet 壳)
  • UE_5.8\Engine\Plugins\Performance\AutomatedPerfTesting\(UE5.8 逐帧性能测试插件与 GauntletSettings 模板)

测试实施指南

本文档涵盖测试环境搭建、自动化工具选择、Bug 报告模板。属于实施层面的指导,不涉及具体测试用例。


1. 测试环境搭建

1.1 硬件环境

操作系统 Windows 11 UE 5.8 开发环境。

CPU 8 核 3.2GHz+

GPU AMD核显 16G显存。

内存 32GB UE 硬盘 500GB SSD

1.2 软件环境

组件 版本/工具 用途
Unreal Engine 5.8 引擎运行环境
Visual Studio 2022 编译调试
Git 最新版 版本管理
性能分析工具 Unreal Insights 逐帧性能分析
网络模拟 UE 内置 Network Profiler 延迟/丢包模拟

1.3 测试场景配置

1
2
3
4
5
6
7
8
9
10
11
客户端配置:
- 1 个 Editor 实例(开发/调试)
- 1 个 Standalone Game 实例(客户端)

网络模拟:
- 使用 UE 控制台命令: net.PacketSimulationSettings
- 模拟延迟: 50ms / 100ms / 200ms / 500ms
- 模拟丢包: 1% / 5% / 10%

帧率锁定:
- 控制台: t.MaxFPS 30 / 60 / 120

2. 自动化测试工具选择

2.1 CQTest

适用场景:武器数值、控制系统、初始化链路、前端页面、队伍系统。

| 参考 | 官方文档 |

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "CQTest.h"

TEST_CLASS(MyWeaponTest, "Lyra.WeaponSystem")
{
SpawnHelper spawn; // 自动创建 World 和 Actor

BEFORE_EACH()
{
// 生成测试角色和武器
}

TEST_METHOD(Shoot_Damage_IsCorrect)
{
// 射击并验证伤害
ASSERT_THAT(AreEqual(30, actualDamage));
}
};

2.2 Gauntlet(集成/性能测试)

项目 说明
适用场景 多进程测试(客户端+服务器)、性能测试、性能逐帧检测
框架 UGauntletTestController
优点 支持多进程、自动采集性能数据
参考 现有 LyraTestControllerBootTest

2.3 手工测试

项目 说明
适用场景 所有需要视觉/手感/听感判断的测试
覆盖 HUD、相机系统、动画测试、音频系统、探索性测试

2.4 工具选择总结

系统 推荐工具 原因
武器数值 CQTest SpawnHelper 轻松生成武器 + 目标,InputTestActions 模拟射击
武器功能 CQTest PIENetworkComponent 验证命中同步
控制系统(数值) CQTest 验证移动速度/跳跃高度/冷却时间
控制系统(功能) 手工 手感/动画需要人工判断
相机系统 手工 平滑度/碰撞感受需视觉判断
HUD 手工 所有元素需视觉判断
前端页面 CQTest + Gauntlet UI 交互流程可脚本化
音频系统 手工 听感判断
队伍系统 CQTest + Gauntlet PIENetworkComponent 模拟多玩家
初始化链路 CQTest + Gauntlet 资源加载 + 网络同步验证
状态系统 CQTest 血量和死亡状态可精确断言 🚫 已砍(2026-08-02 决策)
性能测试 Gauntlet + Unreal Insights 多进程 + 逐帧分析

3. Bug 报告模板

3.1 标准模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
## Bug 报告

**标题**: [系统名称] 简短描述

**严重等级**: 🔴 阻塞 / 🟡 严重 / 🟢 一般 / ⚪ 建议

**环境**:

- 构建版本: [Git Commit / Build Number]
- 测试模式: [单机 / 多人 / 服务器]
- 地图: [地图名称]
- 帧率: [锁定值 / 不锁定]
- 延迟: [模拟延迟值]

**复现步骤**:

1.
2.
3.

**实际结果**:
[实际观察到的行为]

**预期结果**:
[应该发生的行为]

**附加材料**:

- 日志: [日志文件路径]
- 截图: [截图文件]
- 视频: [录屏文件]

**关联测试用例**:

- [关联的测试用例编号,如 WS-03]

3.2 快速模板(适用于探索性测试)

1
2
3
4
[系统] 一句话描述
步骤: 1. 2. 3.
实际: xxx
预期: xxx

3.3 严重等级定义

等级 定义 响应
🔴 阻塞 游戏崩溃、无法继续、核心功能不可用 立即修复
🟡 严重 功能异常但可绕行、数值明显错误 当天修复
🟢 一般 边缘 case、UI 显示问题 迭代修复
⚪ 建议 体验优化建议 排期讨论

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

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

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


前置:打开参考资产

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

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

第一步:创建测试地图

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

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


第二步:创建测试蓝图

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

第三步:声明变量

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

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

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


第四步:编写 OnPrepareTest

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

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

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


第五步:编写 IsReady

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

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

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


第六步:编写 StartTest

搜索 Start Test 事件:

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

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

Input Key 节点详解

TestPawnGet ControllerCast To PlayerControllerInput Key

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

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

编译

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

放置到地图

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

运行测试

方法一:直接运行

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

方法二:通过 Session Frontend

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

常见问题排查

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

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

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

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

核心要点总结

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

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

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


测试工作流清单

2026-08-03 从《测试计划》§5.5 抽出独立成文:执行按分级、管理按模块的具体落地。
触发时机组织为可打勾的工作清单,每个事件附具体用例索引,可照着直接执行。
用例编号统一遵循各系统宪章(EQ/WF/WS/MV/AM/NV/CM/HD/FE/AU/TM/IN/DT/SP/PF)。

使用方式

遇到下列任一触发事件时,打开对应清单逐项执行;未打勾的项就是当前阻塞。
每个事件的”本事件用例”即该时机需要执行的具体用例,来源为各系统宪章。

事件 A:每次代码提交(开发)

  • 运行本次改动涉及的 ⚪ 回归·单元(数值 / 配置 / clamp 校验,可挂 pre-commit)
  • 判断改动影响哪些模块 → 对照对应系统宪章
  • 开发自测 / 一次性验证完成(个人工具,不进宪章)

本事件用例(⚪ 回归·单元)

  • 武器数值:WS-01(距离衰减曲线)、WS-02(射速配置)、WS-03(弹药量)、WS-04-01/02/04(部位伤害倍率校验)
  • 控制数值:NV-01-01/02(跳跃速度 clamp)、NV-03-01/02(重力 clamp)、NV-05-01/02(MaxWalkSpeed clamp)
  • 相机数值:NV-01(FOV clamp)、NV-02(俯仰限幅)
  • 音频数值:NV-01(音量边界)、NV-02(通道独立)
  • 事件数值:SP-01-03(HeatPerShot=0)、SP-02-01(曲线求值)、SP-05-03(冷却延迟负值)、SP-06-01(SpreadExponent=0)

事件 B:每次合并 / 构建(CI)

  • 🔵 冒烟全绿(环境 / 链路基线:地图加载、Pawn 生成、输入链路)
    • 失败 → 停止后续,先修环境或基础链路,回归结果不作数
  • ⚪ 回归·单元全量(随构建)
  • 🟢 回归全量(历史用例)
    • 失败 → 区分:功能被改坏(修代码)还是断言过时(更新用例)

本事件用例 — 冒烟(🔵)

  • 初始化链路:IN-01-01/02(进程启动 / Experience 加载)、IN-02-01~03(Pawn / AbilitySet / 默认武器)、IN-04-01/02(输入绑定)
  • 前端页面:FE-02-01/02(主菜单 → 进入对局)、FE-05-01/02(加载链路)
  • 武器:EQ-01(初始武器)、WF-01-01(单发射击)
  • 控制:MV-01(方向移动)、AM-01-01(平地跳跃)

本事件用例 — 回归(🟢,全量)

模块 用例
武器 EQ-02-0105、EQ-0305、WF-01-01/-03、WF-02-01/-02/-04、WS-04-03
控制 MV-01/-03、AM-01-01/-02/-04/-05、AM-02-01~04
前端 FE-02-03~05、FE-03、FE-04、FE-05-03、FE-06
初始化 IN-01-03、IN-02-04~06、IN-03、IN-05、NV-01
事件 DT-01、DT-03、DT-05-01/-03、DT-06、DT-07-01、SP-01-01/-02/-04、SP-02-02/-03、SP-03、SP-04、SP-05-01/-02、SP-06-02/-03

注:控制 NV 的中间值/最大值断言(NV-01-03/04 跳高、NV-03-03/04 下落、NV-05-03/04 速度)已并入 AM-01 / MV-01 实测,不单独执行。

事件 C:里程碑 / 版本候选

  • 逐条过《测试计划》§6 质量门禁
  • 🟠 验收 Session 1~3(按模块打包执行)
    • Session 1 核心视觉:武器动画 → 装备动作
    • Session 2 移动体验:控制动画 → 视角 → 相机
    • Session 3 UI 与信息:HUD → 队伍 → 音频
  • 手工发现 Bug 全部登记到 TestDocs/Bug报告/
  • 🔴 阻塞 Bug 清零
  • 视觉 / 手感问题决策:修复 or 记为已知问题

本事件用例 — 验收(🟠,手工)

  • Session 1 核心视觉:武器 WA-0103(装备 / 换弹 / 射击动画)、EQ-0105(装备动作手感)
  • Session 2 移动体验:控制 AN-01-0105(移动动画)、LC-01-0103/-05(视角)、AM-02-05(矮通道)、AM-03-01~06(Dash);相机 CM-01/02/05(跟随 / 碰撞 / 蹲下偏移)
  • Session 3 UI 与信息:HUD HD-0105;队伍 TM-0105;音频 AU-01~04;事件 DT-02-02(重生后摄像机恢复)、DT-05-04(Dash 中死亡)

事件 D:发布前(最后一关)

  • 功能冻结确认(探索期间不再改功能)
  • 🟣 探索性测试 6 轮 × 90 分钟(跨系统组合 / 边界操作)
    • 每轮结束做回流决策:可复现且值得保护 → 固化为宪章用例(标 🟢 / 🟠);一次性 → 留 Bug 报告
  • 探索期间若有改动 → 🟢 回归全量复跑
  • 性能门禁(PF-01~12)通过
  • 发布决策:无 🔴 阻塞 + 已知问题可接受

本事件用例 — 探索(🟣)

  • 6 轮聚焦模块见探索性测试宪章:武器+控制+状态 / 控制+相机+HUD / 队伍+武器+HUD / 前端+初始化链路 / 状态+初始化链路 / 音频+武器+控制
  • 每轮结束回流决策:可复现且值得保护 → 固化为宪章用例(标 🟢 / 🟠);一次性 → 留 Bug 报告

本事件用例 — 性能(🟢)

  • PF-0103(30 / 60 / 120fps 功能与帧时间)、PF-04/05(帧率波动 / 骤降恢复)、PF-0610(50~500ms 延迟与抖动)、PF-11/12(内存 / 帧时间门禁)——见性能测试宪章

分级执行矩阵(速查)

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

与其它文档的关系

文档 回答的问题
各系统宪章 测什么(用例归属:模块)
《测试计划》 按什么顺序实现、质量门禁是什么
《用例分级清单》 每个用例为什么存在(目的)
本清单 什么时机做什么动作(流程:打勾执行)

Bug 报告

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


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

步骤:

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

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

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

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

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

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


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

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

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


Bug 报告

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


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

步骤:

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

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

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

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


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


Bug 报告

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

等级: 🟡 严重


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

步骤:

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

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

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

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


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


Bug 报告

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

等级: 🟢 一般


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

步骤:

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

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

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

附加材料:

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

关联测试用例:

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

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


Lyra 测试项目执行计划

开始日期:2026-07-11

📌 封板收尾(2026-08-04):自动化 CQTest 96/96 全绿(7 套件)、手工 Session 1~3 完成(2026-08-03)、
无 🔴 阻塞 Bug → 按”功能封板”口径封板(2026-08-03)。性能 36 实例 / 探索性 6 轮 /
DT-04 网络 / SP 剩余子状态列入《已知缺口 / 封板后 backlog》,详见《Lyra 测试总结报告》。


阶段一:框架搭建(7/12 – 7/14,3天)

目标:基建跑通,验证测试工具链(CQTest / PowerShell)可用。

2026-08 决策:蓝图功能测试已弃用,原 Day 1 蓝图任务改为 CQTest 武器功能验证。

Day 1 — 7/12(周六)

顺序 任务 工具 预计
1.1 阅读 WeaponSystem_Equipment.spec.cpp,理解 CQTest + FMapTestSpawner 标准模式 VS 30min
1.2 新建 WeaponSystem_Fire.spec.cpp,实现 WF-01-01:单发射击 → 弹药 -1 CQTest 1.5h
1.3 编译 → Session Frontend 运行 CQTest VS + UE 15min
1.4 截图结果(通过/失败均可) 5min
1.5 如果失败:排查报错,修复 → 重新跑 → 记录到 Bug报告/ CQTest 1h
1.6 如果通过:扩展为 WF-01-02 连发射击测试(InputTestActions 按住) CQTest 30min

Day 1 完成标准:至少一个武器功能 CQTest 跑通并有截图。


Day 2 — 7/13(周日)

顺序 任务 工具 预计
2.1 打开 ShooterMapsInit.spec.cpp,逐行理解现有代码 VS 30min
2.2 补全 IN-01:游戏进程启动校验 CQTest 30min
2.3 补全 IN-02:Experience + GameMode 初始化校验 CQTest 1h
2.4 在 VS 中编译 → 通过 Session Frontend 运行 CQTest VS + UE 30min
2.5 截图全部通过的结果 5min

Day 2 完成标准:IN-01 ~ IN-02 CQTest 编译通过并执行成功。


Day 3 — 7/14(周一)

顺序 任务 工具 预计
3.1 新增 IN-03:网络同步就绪(GameState 复制校验) CQTest 45min
3.2 新增 IN-04:Pawn 资源加载(Class存在 + 生成无报错) CQTest 30min
3.3 新增 IN-05:AbilitySet 资源(GrantedAbilities 注册校验) CQTest 45min
3.4 编译 → 全部通过 VS + UE 20min
3.6 运行 RunInitChainTests.ps1 → 验证一键执行流程 PowerShell 30min

Day 3 完成标准:InitChain CQTest 全部通过 + PowerShell 脚本一键执行成功。


🔒 阶段一门禁(必须全部打勾才能进入阶段二)

  • 武器功能 CQTest(WF-01-01 单发射击)跑通(截图)
  • CQTest 初始化链路 5 个主用例全部通过(截图)
  • RunInitChainTests.ps1 一键执行无报错
  • 前端 Spec 至少完成 1 个用例

阶段二:核心自动化(7/15 – 7/20,6天)

目标:拿起武器 → 移动操作 → 射击 → 伤害 → 死亡,全链路可脚本化验证。

Day 4 — 7/15(周二)

顺序 任务 工具
4.1 新建 WeaponSystem_Fire.spec.cpp CQTest
4.2 实现 WF-01:单发武器射击验证(按一下 → 出一发子弹 → 弹药-1) CQTest
4.3 实现 WF-02:连发武器射击验证(按住 → 持续出弹 → 松开停止) CQTest

Day 5 — 7/16(周三)

顺序 任务 工具
5.1 实现 WS-01:散布值验证(固定位置 → 射击100发 → 计算散布半径) CQTest
5.2 实现 WS-02/03/04:距离衰减(0/负值/正常 → 不同距离伤害对比) CQTest

Day 6 — 7/17(周四)

顺序 任务 工具
6.1 实现 WS-05/06/07:射速边界值(0/负值/正常) CQTest
6.2 实现 WS-08/09/10:弹药量边界值(0/负值/正常) CQTest
6.3 实现 WS-11/12/13:部位伤害(头/身体/四肢) CQTest

Day 7 — 7/18(周五)

顺序 任务 工具
7.1 补全 ControlSystem.spec.cpp(已有文件):MV-01~03 对齐宪章编号重命名 CQTest
7.2 实现 AM-01(跳跃含连续跳跃 -04)、AM-02 蹲下(-01~-04) CQTest
7.3 AM-03 Dash、AM-02-05 矮通道 → 转手工(蓝图 GA,见宪章) 手工
7.4 编译 → 运行 Project.ControlSystem 验证 CQTest

Day 8 — 7/19(周六)

顺序 任务 工具
8.1 NV-01 跳跃速度:补 a-ε(-0.01) / mid(210)(0、420 已有) CQTest
8.2 NV-03 重力:补 a-ε(-0.01) / mid(5) / max(10)(0 已有) CQTest
8.3 NV-05 MaxWalkSpeed 边界值(-1/0/300/600)+ FR-01/02 死亡冻结回填 CQTest
8.4 NV-02 Dash 冷却、NV-04 灵敏度 → 转手工(蓝图 GA / 鼠标手感) 手工

Day 9 — 7/20(周日)🚫 已砍(2026-08-02 决策:状态系统不需要)

顺序 任务 工具
9.1 新建 StatusSystem.spec.cpp CQTest
9.2 实现 ST-01:血量初始值 / 受伤减少 / 治疗增加 / 不超上限 CQTest
9.3 实现 NV-01/02/03:初始血量边界值(0/负值/正常) CQTest
9.4 实现 NV-04/05/06:最大血量边界值(0/负值/正常) CQTest

🔒 阶段二门禁

  • 武器自动化全部通过(EQ/WS 走 CQTest;WF-01~02 已用蓝图功能测试完成,2026-08-03 决策:与宪章 CQTest 不符,先按蓝图执行)
  • 控制 CQTest 全部通过(MV-0103、AM-0102、NV-01/03/05、FR-01/02)
  • 状态 CQTest 全部通过(4主用例)(已砍,2026-08-02 决策)
  • 边界值(0/负/正常)全部覆盖
  • 无 🔴 阻塞 Bug

阶段二点五:死亡事件与网络(7/21 – 7/24,4天)

目标:死亡/复活完整链路 + PIE 多开验证状态复制。

Day 10 — 7/21(周一)

顺序 任务 工具
10.1 新建 EventSystem_Death.spec.cpp CQTest
10.2 实现 DT-01:血量归零触发死亡(DeathState 状态机验证) CQTest
10.3 实现 DT-02:死亡后移动/射击/操作冻结 CQTest
10.4 实现 DT-03:死亡后碰撞体变化(不再被射击命中) CQTest

Day 11 — 7/22(周二)

顺序 任务 工具
11.1 实现 DT-04:PIE 双开,验证 DeathState 在客户端—服务端复制一致 CQTest + PIENetwork
11.2 实现 DT-05:死亡瞬间输入排队(死亡时按射击 → 复活后不会自动开火) CQTest

Day 12 — 7/23(周三)

顺序 任务 工具
12.1 实现 DT-06:复活链路(血量/装备/技能重置完整性) CQTest
12.2 实现 DT-07:复活边界情况(死亡后立即重生 / 重生后立即死亡) CQTest

Day 13 — 7/24(周四)缓冲日

顺序 任务
13.1 阶段一~二点五 遗留 Bug 修复
13.2 更新执行计划文档,记录实际完成情况

🔒 阶段二点五门禁

  • 死亡单机(DT-01~05)全部通过
  • 死亡网络(DT-04)通过
  • 复活(DT-06~07)全部通过
  • 无 🔴 阻塞 Bug

阶段三:边界值 + 手工测试(7/25 – 7/28,4天)

目标:补完数值边界自动化 + 执行全部手工用例。

Day 14 — 7/25(周五)

顺序 任务 工具
14.1 新建 CameraSystem_Numeric.spec.cpp → NV-01/02(FOV 0/负/正常 + 俯仰限幅) CQTest ✅ 9/9
14.2 新建 AudioSystem_Numeric.spec.cpp → NV-01/02(音量边界) CQTest ✅ 8/8
14.3 新建 TeamSystem_Numeric.spec.cpp → NV-01(队伍数量边界) CQTest 🚫 已删(2026-08-03)
14.4 全部编译通过 VS + UE

Day 15 — 7/26(周六):手工 Session 1

顺序 任务 方式
15.1 武器动画 WA-01~03(装备/换弹/射击) 肉眼 + OBS录屏 ✅
15.2 装备动作 EQ-01~05(切换/空槽/满槽/冷却) 肉眼 + OBS录屏 ✅
15.3 状态机视觉 ST-02(死亡动画/特效) 肉眼 + OBS录屏
15.4 发现的 Bug 写入 Bug报告/ 模板 ✅(装备动画 / WF-02-04)

Day 16 — 7/27(周日):手工 Session 2

顺序 任务 方式
16.1 控制动画 AN-01(移动/跳跃/蹲下/Dash/切换) 肉眼 + OBS录屏 ✅
16.2 视角 LC-01(鼠标水平/垂直/反转/灵敏度) 肉眼 ✅
16.3 相机 CM-01/02/05(跟随/碰撞/蹲下偏移) 肉眼 + OBS录屏 ✅
16.4 发现的 Bug 写入 Bug报告/ 模板 ✅(AM-03-05)

Day 17 — 7/28(周一):手工 Session 3

顺序 任务 方式
17.1 HUD HD-01~05(准星/命中标记/伤害数字/布局/血条弹药) 肉眼 + 截图 ✅
17.2 队伍 TM-01~05(归属/识别/伤害/显示/比分板) 肉眼 ✅
17.3 音频 AU-01~04(音量/输出/HDR/加载) 耳朵 + 录屏 ✅
17.4 发现的 Bug 写入 Bug报告/ 模板 ✅(无新增)

🔒 阶段三门禁

  • 相机/音频 边界值 CQTest 全部通过(9/9、8/8;队伍 NV-01 已删,2026-08-03)
  • 手工 Session 1~3 全部执行完毕(2026-08-03)
  • Bug报告/ 有实际内容(4 份)
  • 无 🔴 阻塞 Bug

阶段四:压测与探索(7/29 – 8/3,6天)

目标:性能边界验证 + 跨系统组合 Bug 发现。

Day 18 — 7/29(周二)

顺序 任务 工具
18.1 实现 PF-01/02/03:30fps / 60fps / 120fps 帧率锁定测试 Gauntlet
18.2 实现 PF-04:帧率 30~120fps 波动 → 无单帧超过 100ms 卡顿 Gauntlet
18.3 实现 PF-05:帧率骤降恢复(60→15→60) Gauntlet

Day 19 — 7/30(周三)

顺序 任务 工具
19.1 实现 PF-06/07:50ms / 100ms 延迟射击验证 Gauntlet
19.2 实现 PF-08/09:200ms / 500ms 延迟射击验证 Gauntlet
19.3 实现 PF-10:延迟 50~300ms 抖动 → 无断线 Gauntlet

Day 20 — 7/31(周四)

顺序 任务 工具
20.1 实现 PF-11:逐帧内存检测(5分钟,每帧记录) Gauntlet + Insights
20.2 实现 PF-12:逐帧帧时间检测(5分钟,99%帧≤30ms) Gauntlet + Insights
20.3 所有性能数据导出 → 写入测试报告

Day 21 — 8/1(周五):探索性测试 前半

轮次 聚焦 方式 时长
第1轮 武器 + 控制 + 状态(换弹→Dash→射击;死亡瞬间操作) 自由探索 + OBS 90min
第2轮 控制 + 相机 + HUD(快速转向卡墙;跑射/跳射/蹲射HUD) 自由探索 + OBS 90min
第3轮 队伍 + 武器 + HUD(多人同目标射击;友伤;比分板) 自由探索 + OBS 90min

Day 22 — 8/2(周六):探索性测试 后半

轮次 聚焦 方式 时长
第4轮 前端 + 初始化链路(快速进出菜单;断线重连状态) 自由探索 + OBS 90min
第5轮 状态 + 初始化链路(连续死亡重生残留;重生能力完整) 自由探索 + OBS 90min
第6轮 音频 + 武器 + 控制(密集音效叠加;音频延迟) 自由探索 + OBS 90min

Day 23 — 8/3(周日):收尾

顺序 任务
23.1 全部 Bug 清零或标记为已知问题 ✅(4 份标记,无 🔴 阻塞)
23.2 更新测试总纲:标记全部用例最终状态 ✅(2026-08-04)
23.3 写一份《Lyra 测试总结报告》(1-2页,汇总发现) ✅(20-测试总结报告.md)
23.4 项目封板 ✅(2026-08-04,功能封板口径)

🔒 阶段四门禁

  • 性能测试 36 实例全部通过(➡️ 已知缺口:本机需 3~6h,见《性能测试可行性调研》)
  • 60fps 平均帧时间 ≤ 16ms,99% ≤ 30ms(➡️ 同上)
  • 延迟 50~500ms 无断线或卡死(➡️ 并入 DT-04 网络 backlog)
  • 5min 帧内存无持续增长(➡️ 同上)
  • 探索性 6 轮完成,Bug 清零(➡️ 已知缺口,封板后 backlog)

每周检查点

日期 里程碑
7/14(周一) ✅ 三种工具链全通,InitChain 自动化完成
7/20(周日) ✅ 核心玩法闭环自动化完成
7/24(周四) ✅ 死亡/复活/网络同步完成
7/28(周一) ✅ 手工测试全部完成
8/3(周日) ✅ 项目封板

每天工作流程

1
2
3
4
5
1. 打开本文件,看当天任务
2. 码代码 / 跑测试
3. 截图记录(通过/失败都要截图)
4. 如果失败 → 记录到 Bug报告/
5. 当天任务全部打勾后 → 休息

如果某天没完成

1
2
3
不追进度。不压缩后面的任务来补。
当天任务移到第二天第一项。
如果连续 2 天没完成 → 说明计划太紧 → 重新评估节奏,不硬撑。

项目封板后 → 投简历

项目完成日(8/3)开始投递,不提前。