《游戏测试完全指南》一共 22 章,这里把前三章的笔记合在一起:第一章是基础理论,讲游戏测试和普通测试差在哪;第二章讲人工测试的思维工具;第三章以 UE5 为例,讲调试后门在今天以什么形态存在。


第一章 游戏测试和普通测试到底差在哪

教程一共 22 章,第一章是基础理论。游戏测试和普通测试的差别,不只是“测的东西是游戏”。

游戏手柄

一、交互复杂性:游戏的状态爆炸

普通软件的测试模型是清晰的:从输入到程序处理,再到输出 B。

但是,游戏的每一帧都在发生多系统的交叉作用。就比如说一个太空弹幕小游戏(**用 Go 做的小游戏**),一帧里同时跑的东西有:

1
2
3
帧 N: 玩家按空格 → 射击冷却检查 → 生成子弹 → 注册碰撞事件
帧 N+3: 子弹撞到陨石 → 伤害计算 → 分数累加 → Combo 检查 → 陨石分裂
帧 N+5: 小陨石生成 → 方向随机 → 检测出生位置是否与玩家重叠

复杂的网状交互让游戏的状态爆炸式增长。教程里给了个 MMO 战斗的复杂度公式:

C_combat = N_skills × N_buffs × N_targets × N_positions × N_timing

每一项都是一个维度,乘起来就是状态空间。计算出一个简化 RPG 角色的理论状态空间是一个天文数字,大到穷举完全没有可能。

二、实时性:细节到帧

游戏对响应时间要求很严格,必须考虑时间轴上的表现是否符合预期。

一个 60 FPS 的游戏意味着每帧不能超过 16.67ms。这 16.67ms 要完成输入处理 + 逻辑更新 + 物理计算 + 渲染 + 同步。不同的游戏类型对于游戏帧数的要求也不相同,教程给了不同帧目标的细分:

游戏类型 目标帧时间 FPS
竞技游戏 8.33ms 120
动作游戏 16.67ms 60
休闲游戏 33.33ms 30

电影行业里 24 帧就可以让人感到流畅,然而,24 帧在一些需要主动操作的游戏中却让人感到明显卡顿。一个原因是,游戏的逻辑都是逐帧处理的,并且每一帧都是一个独立的图像,与相邻帧之间没有转换关系,所以当玩家进行操作时,很容易观察到画面的跳变。所以对竞技游戏来说,120 帧已经成为标配。

帧率不稳定比低帧率更影响体验。根据 Weber-Fechner 定律,人眼对帧率变化非常敏感。所以在性能测试中要看 99% 帧率(也叫 1% low):99% 的帧率都要高于某个标准。比如 5090 显卡最高画质下,要求游戏 99% 的帧率大于 120 帧。

实时性另一个坑是竞态条件
这个虽然在软件里也会出现,但通常是在数据处理时才会特别关注,而游戏由于每一帧都要计算,所以这种问题非常普遍。逻辑线程和渲染线程分离,如果同步不当,客户端视觉上会出现”各种奇怪瞬移”,也会出现服务器端计算出错误的结果。
两个线程读到不同时刻的数据。这种 bug 很难复现,它依赖两个线程的精确执行时序,而时序每帧都略有不同。测 100 次可能只触发少数几次。
还有一种是多线程状态抢占:玩家同时拾取同一物品,交易过程中断线,多个服务同时修改玩家数据。

教程的常见陷阱列表里单独列了”异步系统的竞态测试”,概括下来就是:在开发环境里永远无法重现的生产环境 bug

三、随机性:复现困难

普通软件大部分场景是确定性的。登录失败,再试一次还是失败,确定性让 bug 可以复现。

游戏里充满随机:暴击、掉落、程序化地图、匹配对手。教程把游戏随机性分了四层:

1
2
3
4
L1: 简单随机(掉落、暴击)     → 二项检验
L2: 参数化数值(属性、数值) → 卡方检验
L3: 结构化构建(地图、关卡) → 连通性验证
L4: 智能生成(任务、剧情) → 规则约束

每一层需要不同的验证手段。L1 的暴击率不可能测无限次,只能统计足够多的样本,看是否符合概率。

验证 10% 的暴击率,至少需要 10000 次攻击才能让置信区间落到 [9.4%, 10.6%]。在足够的数据下才能让大数定律发挥作用。

三种统计检验的用途分工:

  • 二项检验:测一个概率 (25%暴击率)
  • 卡方检验:测多个类别概率(奖励掉落表 50/30/15/5 )
  • K-S 检验:测连续数值分布(伤害浮动是不是真的均匀分布)

这三个检验我在读之前完全没概念。普通软件测试不会用到它们,因为普通软件很少有”25% 概率触发某行为”这种设计。

四、边界条件组合爆炸

单个边界条件不难测。HP=0 和 HP=MAX。

但边界条件会叠加。教程举的例子:

  • 等级 1 + 最强装备(数值溢出)
  • HP=1 + 吸血效果(除零风险)
  • 坐标 Integer.MAX_VALUE + 移动(坐标回绕)

这些组合在正常测试流程里几乎不可能自然出现。但测试要找到这种现实中不太可能,但必须保证的极端叠加。

实例分析

等价类划分

练习1.1 120个英雄,4个技能如果要测试所有英雄的所有技能等级组合,需要多少个测试用例?如果采用等价类划分,将技能等级分为低(1-2)、中(3-4)、高(5)三类,测试用例可以减少到多少?

完整测试用例数:120 × 5^4 = 75,000 个。

采用等价类后:120 × 3^4 = 9,720 个。减少率 87%。

等价类的划分依据必须来自对游戏机制的深入理解。某些技能升级只改变数值不改变机制,才能将其归为一类。如果升级改变了技能形态(比如火球变火雨),就必须独立测试。

概率测试

练习1.2 游戏的暴击率标称为 25%。测试 1000 次攻击,暴击了 280 次。请计算 95% 置信区间,并判断是否符合预期。

计算过程省略,方法可以搜索二项检验。

计算结果为 [0.2522, 0.3078]

标称值 25%(0.25)落在置信区间下限 0.2522 之外。这个暴击率系统大概率有问题,实际暴击率偏高。

关键在于两点:一是样本量要够大,大数定律才能起作用;二是统计显著性和实际显著性要分开看。280 次比 250 次多了 30 次,在 1000 次测试里是 3% 的偏差,统计学上显著;但 3% 的差异在实际中很小,最好用更大样本量再测一次。

测试金字塔设计

练习1.3 设计一个测试金字塔,针对一款包含PvE和PvP的卡牌游戏。请为每一层列出3个具体的测试项目。

1
2
3
4
5
6
7
8
9
        /\
/探索性\
/--------\
/ 集成测试 \
/------------\
/ API/逻辑 \
/----------------\
/ 单元测试 \
/--------------------\
  • 单元测试(底层):单卡效果解析、伤害计算公式、状态机转换
  • API/逻辑层:回合流程控制、卡牌抽换机制、战斗结算引擎
  • 集成测试:PvE 关卡AI行为交互、PvP匹配对战流程、卡组构建保存/读取
  • 探索性测试(顶层):新卡对 Meta 的冲击、极端卡组组合、UI/UX体验流畅度

越往上测试成本越高,发现的问题影响范围也越大。

技能树测试策略

练习1.4 某MMORPG的技能系统:30个主动技能,每个10级;存在前置关系(学B需要A达到5级);同时最多激活8个技能。请设计测试策略。

这个题的关键在于:**寻找高危”有效组合”**。

测试策略分两步:

  1. 等价类降维:10级技能分成低(1-3)/中(4-7)/高(8-10),每个区间内数值变化量很小。激活槽位 8 个,但玩家的真实选择集中在 3-5 种流派(爆发/持续/控制),按流派聚类。

  2. 高风险组合:教程列了 7 个常见陷阱,这里最相关的是边界叠加:

    • 全技能满级 + 同时激活 = 数值是否溢出
    • 洗点/重置 → 重新加点 = 前置关系是否残留

核心思路:用流派聚类覆盖真实玩家行为。教程参考答案给出的方案是约 130 个核心用例覆盖 80% 以上场景。
技能依赖链简单估算测试:30/2 = 15 用例
孤立测试:30 技能 × 3 类
组合测试:寻找 3 种流派(爆发/持续/控制)
集成测试:5种场景
冗余用例:10种
总测试用例数量:15 + 30 × 3 + 3 × 5 + 10 = 130

Roguelike地牢验证

练习1.5 验证程序化生成的Roguelike地牢:(a)所有地牢连通;(b)难度分布合理;(c)宝藏分布公平。

(a) 连通性——图论解决

把房间当节点,走廊当边,地牢就是一张无向图。判断连通性就是判断图是否全连通:

此外,与其生成后检测、不通就重试或修复,不如在生成算法里加连通性断言。生成每个走廊时检查是否连接了两个不同连通分量,保证生成完成时必然连通。这是”测试左移”的体现。

(b) 难度分布——统计检验

定义难度 = 敌人总战力 / 玩家可获得资源。生成 1000 个地牢,画难度分布直方图。期望是近似正态分布,不会出现极度简单(裸奔通关)或极度困难(必死局)。教程参考答案使用 Shapiro-Wilk 检验正态性,配合 3σ 原则检查极端值。

(c) 宝藏公平性——路径分析

公平 ≠ 均匀。公平是指:任何起点到任何终点的路径上,可获得的关键资源(钥匙、武器)数量差异不超过阈值。教程参考答案使用了三种空间统计方法:最近邻分析(判断是否随机分布)、象限卡方检验(判断是否均匀分布)、可达性分析(路径长度变异系数 < 0.3)。

这三个子问题分别用到了图论、统计学、路径分析,没有传统的”点点点测法”。

帧数据精确测量

练习1.6 格斗游戏运行在60FPS,每个招式有启动帧、活动帧、恢复帧。如何自动化测试所有角色的所有招式的帧数据准确性?

教程参考答案的核心思路:帧步进模式 + 状态机监控

  • 锁定 60FPS,关闭垂直同步,使用确定性输入系统(帧级输入队列)
  • 逐帧记录角色状态和碰撞盒:
    T0 输入指令 → T0+n 检测攻击判定激活(启动帧=n)→ T0+n+m 攻击消失(活动帧=m)→ T0+n+m+k 恢复待机(恢复帧=k)
  • 验证公式:总帧数 = 启动 + 活动 + 恢复,连招规则:A动作的恢复帧 + B连招的启动帧 < 对手硬直
  • 批量矩阵:20角色 × 30招式 × 4种情况(空击/命中/防御/反击)= 2400用例

不可能手工逐帧测量 2400 个用例,必须自动化批量验证

LOD系统:视觉与性能的双重验证

练习1.7 开放世界游戏的LOD系统,如何验证LOD切换不造成视觉跳变,同时确保性能优化有效?

解法是用两条独立的验证轨道:

轨道 指标 方法 通过标准
客观性能 三角形减少率、DrawCall减少率、FPS提升 自动化脚本在不同场景下采集 正常场景三角形减少60%+
客观视觉 SSIM结构相似性、像素差异率、轮廓偏移 逐帧截屏对比 SSIM > 0.95
主观视觉 A/B盲测评分 录制切换视频,随机播放给测试者 平均分 > 3.5/5

这揭示了游戏测试的一条原则:能自动化的用数据说话,自动化不了的用盲测说话。两条轨道互为补充,缺一不可。

网络延迟补偿:把不确定性矩阵化

练习1.8 实时PvP手游使用客户端预测+服务器回滚。如何测试在不同网络条件下游戏体验仍可接受?

教程参考答案展示了一个完整的系统化方法:先把不确定性变成确定性矩阵,再逐格验证。

1
2
3
4
网络测试矩阵:3 × 3 × 3 = 27 种环境
延迟: [20ms, 100ms, 200ms]
丢包: [0%, 1%, 5%]
抖动: [0ms, ±20ms, ±50ms]

然后对每种环境定义可量化的体验指标

  • 预测准确性:20ms延迟下预测准确率 > 95%,200ms下 > 75%
  • 响应性:输入到视觉反馈延迟(含预测)< 50ms
  • 公平性:”被击中但已躲开” < 每分钟1次
  • 压力场景:多人团战 + 每秒10+技能 + WiFi切4G + 高APM操作

核心思路:把网络测试变成确定性条件。用网络模拟器,在固定条件下对每种环境做统计验证。

六、八道练习题的方法论提炼

方法论一:降维,压缩指数级空间到可测范围

第一个问题:状态空间指数爆炸,穷举没有可能。

八道题里,练习1.1和1.4给出了两个降维手段:

  • 等价类划分(1.1):必须理解游戏机制(哪些升级只改数值、哪些改了行为)
  • 流派聚类(1.4):真实玩家只玩 3-5 种流派。用玩家行为数据聚类,用约束求解覆盖边界。

两条原则:

  1. 降维的粒度取决于对游戏机制的理解深度
  2. 降维的方向要沿着真实玩家行为走,而非均匀抽样

方法论二:量化,用数据替代描述

第二个问题:必须把主观体验翻译成可测量的数字。玩家的”打击感””流畅感””概率抱怨””掉帧”这些模糊描述,都应该在测试里变成可量化语言。

练习 验证目标 量化手段
1.2 暴击率是否准确 置信区间
1.5b 地牢难度是否合理 Shapiro-Wilk
1.6 帧数据是否精确 帧级状态机 + 公式计算验证
1.7 LOD是否平滑 SSIM + 像素差异率 + A/B盲测
1.8 网络体验是否可接受 客户端预测准确率 + 回滚深度 + 延迟预算

这些工具分两类:客观量化(置信区间、SSIM、帧时间)和主观量化(A/B盲测评分、玩家投诉率)。两者都要有。

方法论三:分层,不同风险层级用不同武器

成本-收益的最优分配

1
2
测试金字塔越底层 → 越确定、便宜、适合自动化
测试金字塔越上层 → 越模糊、昂贵、需要人工判断
金字塔层级 对应练习 验证手段
单元/API 1.1, 1.4, 1.6 等价类、拓扑验证、帧步进自动化
集成 1.2, 1.5a, 1.5c, 1.8 统计检验、图论连通、路径分析、网络矩阵
系统/探索 1.3(设计层), 1.7(视觉), 1.8(体验) A/B盲测、玩家行为数据、体验评分

不要在应该自动化的层级做手工测试(如逐帧数帧数据),也不要在应该人工判断的层级试图写脚本(如判断”手感好不好”)。应该在正确的层级使用正确的武器

方法论四:左移——测试越早介入,成本越低

  • 地牢连通性验证,可以生成时就断言——把测试逻辑嵌入生成算法
  • 帧数据验证,可以CI 每次构建自动跑用例

左移就是让测试从开发阶段就开始介入,提前预防 bug 堆积。

结语

八道练习做下来,方法其实就三类:降维、量化、分层,外加一个左移。够用了。


第二章 游戏人工测试的思维工具

教程第二章讲人工测试

人工测试不只是”点点点”,有一整套测试思维工具

测试笔记

一、探索性测试:有限时间内的创造性测试

专业的探索性测试有严格的框架,叫会话式测试

每次测试都是 45-90 分钟的时间盒,围绕一个”测试宪章”展开。

第一章讲过,游戏状态爆炸,90% 用例覆盖不可能。单元测试和自动化测试之后,剩下的高危场景只能靠经验和直觉去探索——从玩家的视角出发,找出潜在 bug。

测试宪章是一种任务声明,比如,”在未来 60 分钟内,将探索商城系统的支付边界,重点关注货币换算和余额不足场景”。

宪章要满足 SMART 原则:具体、可衡量、可实现、相关、有时限。
宪章最重要的作用是量化测试工作,同时提醒自己主线任务。

时间盒也不要求将所有东西都测完,只要求在规定时间内找到足够多的问题。

为什么越测发现的新问题越少

一个覆盖率增长模型,用微分方程描述:

1
dC/dt = k · (S - C) · exp(-αt)

其中 C 是已覆盖的功能,S 是总功能,k 是学习速率,α 是疲劳系数。

这个公式翻译成人话就是:刚开始测的时候,对系统不熟,发现新问题的速度很快。随着测试进行,剩余未测功能变少了人也累了。连测两小时后效率断崖式下降。所以要写测试宪章,有预算时间的测试更有效

90 分钟分配方案:

阶段 时长 做什么
准备 5 分钟 阅读宪章、回顾上次测试的笔记
探索 70 分钟 执行测试,每 30 分钟停下来记录一次
整理 10 分钟 把发现的 bug 整理成可提交的报告
总结 5 分钟 记录覆盖率变化、标记下一步要测的区域

怎么判断测试是否高效

一个指标:信息密度 D

1
D = (新发现的问题数 + 新覆盖的功能数) / 测试时间(分钟)

优秀测试员的 D 值大于 0.5。也就是说,每两分钟至少要产出一个有价值的发现。
如果两分钟过去了你什么都没发现,说明要么测试策略有问题,要么该换一个宪章了。

两个启发式检查清单

人工测试没法遍历所有情况,但可以用启发式模型帮助系统化地扫描。两个维度:

SFDIPOT(面向通用软件):

维度 含义 游戏测试中的例子
Structure 结构 代码模块、资源文件、配置表
Function 功能 攻击、购买、存档、匹配
Data 数据 玩家属性、道具数据、排行榜分数
Interface 界面 UI 布局、HUD、菜单导航
Platform 平台 不同设备、操作系统、屏幕分辨率
Operations 操作 安装、更新、断线重连、后台切换
Time 时间 帧率、延迟、冷却时间、定时事件

GAMEPLAY(面向游戏专有):

维度 含义
Goals 目标:任务、成就、胜利条件
Actions 动作:攻击、移动、交互、使用道具
Mechanics 机制:伤害公式、碰撞检测、AI 行为
Economy 经济:金币、钻石、经验值、掉落率
Progression 进度:等级、技能树、章节推进
Loop 循环:核心循环、奖励周期、每日任务
Aesthetics 审美:画面、音效、UI 风格
Yield 产出:玩家获得什么、反馈是否及时

这两个清单的作用:不知道该测什么的时候扫一眼,看有没有维度被忽略。是很好用的防遗漏工具。

通用测试启发式

维度 含义
Frequent 频繁:高频操作路径、热点功能、常用组合
Error 错误:错误处理机制、异常恢复、错误提示
Worst-case 最坏情况:极限条件、资源耗尽、网络中断
Happy path 正常路径:期望流程、新手引导、主线任务
Interruption 中断:异常中断处理、断线重连、暂停恢复
Combination 组合:功能交互、状态叠加、并发操作
Configuration 配置:不同设置组合、分辨率适配、语言切换
Usability 可用性:用户体验、操作便利性、信息可读性
Performance 性能:响应时间、帧率稳定性、内存占用
Platform 平台:跨平台兼容、设备差异、系统版本
Scalability 可扩展性:负载能力、并发上限、数据规模

二、边界测试:找到那个”刚好会炸”的点

第一章讲了边界条件组合爆炸,这章进入了实战层面边界具体在哪

显式边界 与 隐式边界

显式边界就是文档里写清楚的,整数溢出(2^31 - 1 是 32 位有符号整数的上限,2^63 - 1 对应 64 位)、配置表里标好的等级上限、业务逻辑里定义的 VIP 等级——这些都有明确的数字。

此外,还有一种棘手的隐式边界,游戏开发者自己都可能不知道,比如:

  • 渲染管线一次最多绘制多少个对象?超过之后的表现为闪烁还是崩溃?
  • 碰撞检测的精度阈值是多少?两个移动速度极快的物体会不会”穿模”?
  • 网络包大小有没有隐式上限?某些特殊操作组合产生的数据包超限后会怎么处理?
  • 寻路网格的节点数有没有上限?超大开放世界地图会不会让寻路算法超时?
    这些问题正常流程很难碰到,复现也困难,非常依赖知识积累和直觉。

边界测试点的通用集合:

1
B = { a-ε, a, a+ε, (a+b)/2, b-ε, b, b+ε }

其中 a 和 b 是边界值,ε 是一个非常小的偏移量。以等级系统 1-100 级为例,测试点就是 0 级、1 级、99 级、100 级、101 级,再加上中间一个正常值(比如 50 级)。这七个点覆盖了”边界外、刚好边界、边界内、中间值”四种情况。

浮点数的陷阱

游戏里浮点数无处不在:坐标、速度、伤害系数、时间。

一个经典例子:16777216.0f + 1.0f == 16777216.0f

32 位浮点数的尾数只有 23 位,小于 16777216 的时候步长是 1,一旦超过这个值,步长变成 2。也就是说,加 1,结果根本没变。这在伤害计算里意味着什么?攻击力够高的时候,加一点攻击力可能完全没有效果

还有 NaN(不是一个有效数字)和次正规数(极小值区间的精度异常)。这些边缘值在任何计算中出现都会像病毒一样传播,让所有后续结果都变成 NaN。

状态机的三级分层

游戏的状态机比普通软件复杂得多,分了三级:

1
2
3
4
5
宏观层:菜单 → 游戏中 → 暂停 → 结算

细节层:探索 → 战斗 → 对话 → 商店

原子层:攻击 → 防御 → 闪避 → 技能释放

每一层都是一个独立的状态机,而且它们是嵌套的。玩家可能在”战斗中”的细节层,同时处于”攻击”的原子状态,然后突然接了个电话,App 进入后台(宏观层切换),回来后所有状态需要正确恢复。

测试策略是分四步递进的:

  1. 状态覆盖:保证每个状态都被访问过
  2. 转换覆盖:用转移矩阵 T_ij 记录每两个状态间是否可能出现合法转换,逐一验证
  3. 路径覆盖:挑几条高频路径(比如”登录→匹配→战斗→结算→返回大厅”)做端到端验证
  4. 条件覆盖:同一个转换在不同条件下行为不同(比如”战斗→死亡”,HP=1 时扣 1 点和 HP=100 时扣 100 点结果一样,但中间状态不同)

大多数团队能做到转换覆盖就不错了。

并发状态机的问题更困难。如果有 n 个独立状态机并行运行(战斗状态机、背包状态机、网络状态机……),理论组合数是它们状态数的乘积。建议是用配对测试降维。不测所有组合,只保证每对状态被覆盖至少一次。

三、玩家行为建模

这一节讲如何从玩家视角测试

Bartle 四型

引用了经典的 Bartle 玩家分类:

类型 驱动力 典型行为 测试关注点
成就型 变强、通关 冲排行榜、刷成就 数值上限、排名系统
探索型 发现、理解 找彩蛋、研究机制 隐藏内容、边界交互
社交型 连接、影响 聊天、公会、交易 社交功能压力、消息系统
杀手型 竞争、支配 PvP、捣乱 平衡性、反作弊、恶意行为

每个类型的玩家会以完全不同的方式玩游戏。成就型玩家会压着数值边界走,探索型玩家会触发一些从没想过的操作组合,杀手型玩家会主动寻找能伤害其他玩家体验的手段。测试时要为每种类型设计不同的测试宪章。

用信息熵识别机器人

马尔可夫链建模玩家行为,然后计算信息熵来判断操作模式。

假设我们把玩家操作分类(攻击、移动、使用道具、等待),然后统计相邻操作之间的转移概率。正常的玩家操作序列有中等程度的熵:既不是完全随机,也不是完全可预测。

1
信息熵 H(X) = -Σ p(x_i) × log₂(p(x_i))

低熵意味着行为高度规律,转移概率集中,很可能是自动化脚本。高熵意味着每个操作的选择近乎等概率,不像真人——真人不会在”攻击”和”打开背包”之间以 50/50 随机选择。

练习 2.3 给了一个 12 步操作序列的例子:5 种操作分别出现 2/4/2/2/2 次,计算出熵值约 2.25 bits。等概率分布的最大熵是 log2 5 ≈ 2.32,2.25 已经很接近它——操作过于均匀,不像真人。

漏斗分析和断点检测

两种分析玩家群体的方法。

漏斗分析:追踪玩家在每个阶段的留存率。数据可能是:

1
2
3
4
新手教程 100%
→ 首次战斗 85%
→ 首次失败 59.5%
→ 首次付费 35.7%

每一层流失率突然变高的地方,就是测试需要重点关注的区域。如果”首次失败”到”首次付费”的转化率异常低,可能是因为失败后的引导不够,或者付费入口在失败界面没展示好。

断点检测:难度曲线的二阶导数。如果 d²D/dt²(难度变化的加速度)超过阈值 θ,说明难度在某个点突然陡增。这不是”难不难”的问题,而是”难度增长是否平滑”的问题。

四、Bug 复现

Bug 复现是整个测试流程里最考验思维能力的环节。

Delta 调试:二分法找最小复现集

核心问题是:**”触发这个 bug 的最小操作集合是什么?”**

假设你有一个 20 步的操作序列能稳定复现 bug,但不是每一步都必要。Delta 调试的做法是二分法递归:

  1. 把 20 步对半分,先测前 10 步——能复现吗?能的话问题在前半段,丢弃后 10 步
  2. 把剩下 10 步再对半分,测前 5 步……
  3. 直到找到”去掉任何一步 bug 就不出现”的最小集合

复杂度在 O(n log n) 到 O(n²) 之间,取决于 bug 的”分布特征”。教程练习 2.5 给出了一个 20 步的序列,经过四轮二分,最终锁定最小复现集合为 [H, I, K, L, M] 五步。

因果链追踪

理解bug为什么发生。可以使用因果链模型:

1
触发条件 → 状态变化 → 错误传播 → 可见症状

看到的 bug 是”可见症状”,比如角色突然飞天。
但真正的根因可能在几层之前:某次状态切换时一个标志位没重置,然后这个错误状态被正常逻辑处理了,最后才在渲染层表现为异常坐标。

分层日志在这里是关键工具:

级别 查什么
FATAL 系统崩溃的直接原因
ERROR 异常被捕获的位置
WARNING 被降级处理的边缘情况
INFO 正常流程的节点记录
DEBUG 变量的中间值
TRACE 每一帧的精确状态

追踪 bug 时,从 ERROR 层入手,向下钻到 DEBUG/TRACE 去重建因果链。

确定性重放

“刚才还能复现,现在又不出现了”。
应对这种问题引入一个新概念:游戏状态是初始状态和所有输入的函数

1
S_n = f(S_0, I_1, I_2, ..., I_n)

如果你能记录 S_0(初始状态:关卡、装备、属性值)和所有输入序列 I,理论上就能在任何时候完美重放。很多游戏引擎已经内置了重放系统(回放录像文件用的就是这个原理),但测试环境往往要求更精细(包括随机种子的记录、网络包的时序等)。

教程列举了 8 个常见的复现陷阱:

  1. 只在特定帧率下触发(帧率变化后消失)
  2. 依赖内存中未初始化的值(重启后消失)
  3. 竞态条件(只在特定线程调度下触发)
  4. 缓存污染(清缓存后消失)
  5. 浮点精度(只在特定硬件上触发)
  6. 时间相关(只在服务器特定时间触发)
  7. 账号状态残留(新建账号不可复现)
  8. 多设备差异(只在特定配置上出现)

每个陷阱对应的调试技巧也不一样:二分法缩小范围、时光机调试(用保存的快照回退到关键帧)、差异对比(能复现和不能复现的环境到底差在哪)、最小化重现、假设验证(先猜一个根因再设计实验验证)。

五、实例分析

练习 2.1:探索性测试宪章设计

练习 2.1 探索性测试宪章设计:为一个 MMORPG 的交易系统设计三个不同焦点的测试宪章,每个宪章时长 45 分钟。

任务:验证装备系统基本功能流程
目标:

  • 验证装备穿戴/卸下的基本功能
  • 确认装备堆叠的上下限处理
  • 检查多装备buff叠加的数值计算
    范围:
    • 穿戴、卸下、替换装备
    • 装备堆叠(0/1/上限/上限+1)
    • 2 件以上装备特效同时生效
      约束:45 分钟
      预期风险:
  • 快速连续替换可能导致数值残留
  • 堆叠数 = 0 时装备可能不消失
  • 装备buff产出冲突或者链式反应

任务:验证装备系统安全性流程
目标:

  • 异常断线后装备状态回滚
  • 背包占满时装备拾取
  • 两个客户端同时操作同一件装备
    范围:网络重连,竞态条件操作装备
    约束:45 分钟
    预期风险:回滚导致装备异常变化,背包溢出装备消失,客户端同时抢占装备导致装备复制,

任务:验证装备系统性能
目标:

  • 大量特效叠加对性能影响
  • 高频装备掉落/拾取
    范围:帧率 ≥ 60fps,LOD0 三角面 ≥ 10 万时不掉帧,特效表现正常,一帧拾取 10 件装备延迟 ≤ 50ms
    约束:45 分钟
    预期风险:跳变,帧率掉60,丢包,事件被回滚。

练习 2.2:等级系统的边界值分析

练习 2.2 某 RPG 游戏等级系统 1-100 级,经验公式 E(n) = 100n²。请设计边界测试,包括等级边界和经验值边界。

等级边界可以用七点法:

1
2
3
等级测试点:0(除0错误), 1, 50, 99, 100, 101
经验测试点:399,400,401(1升2),980099、980100、980101(99升100),999999、1000000、1000001(满级),2^31-1,2^31(溢出临界点)
特殊测试点:转职(10级,20级),经验惩罚开始点,并发(多个经验)

练习 2.3:从操作序列中找出机器人

练习 2.3 一段 12 步操作序列中,5 种操作分别出现 2/4/2/2/2 次。计算信息熵,判断是否是机器人在操作。

计算过程:

1
2
3
各操作概率:p1 = 1/6, p2 = 1/3, p3 = 1/6, p4 = 1/6,p5 = 1/6
信息熵公式
H=−∑pi*log2pi =2.25

5 种操作等概率分布时,最大的信息熵是 log2 5 = 2.32。熵率 = 2.25/2.32 ≈ 0.97,接近 1。这意味着操作过于均匀,人类的操作更像长尾分布。疑似自动化脚本

正常玩家的操作频率分布通常接近幂律(少数操作高频,多数操作低频),出现极端均匀分布,就要怀疑是 bot。

练习 2.4:战斗状态机的全覆盖

练习 2.4 某格斗游戏有 6 种战斗状态:待机、攻击、受击、闪避、技能、死亡。设计状态转换测试,需要的测试用例数。

6 个状态,两两之间可能存在转换(包括自身转换,比如”技能→技能”表示连招)。理论转移总数是 6×6 = 36 条,但不是每条都有意义(比如”死亡→攻击”不存在)。

粗略统计有效转换约 25-30 条。算上条件分支(攻击衔接窗口、技能冷却中、体力不足等),建议设计 25-30 个核心用例,覆盖:全部状态至少一次 + 全部有效转换 + 3-5 条高频路径的端到端验证。

练习 2.5:Delta 调试的实战演练

练习 2.5 以下 20 步操作能复现一个 bug,用 Delta 调试找出最小复现子集。

1
2
3

A B C D E F G H I J K L M N O P Q R S T

Delta 调试流程:

第 1 轮:测试前 10 步 [A-J]。假设不能复现,那么问题在后 10 步。

第 2 轮:把后 10 步 [K-T] 再二分,加前 5 步组成 [A-E, K-O]。假设不能复现,那么问题在 [P-T]。

第 3 轮:测试 [A-E, K-O, P-R] vs [A-E, K-O, S-T]。锁定到 [A-E, K-O, P-R]。

第 4 轮:在有问题的子集里继续收缩。最终锁定最小复现集合 [H, I, K, L, M]。

不需要理解 bug 的原理就能找到复现集。它纯粹是算法化的排除法,但前提是已经有一个能稳定复现的”大集合”。
如果连稳定复现都做不到,那就得先回到因果链追踪,找到触发条件。

练习 2.8:风险评估矩阵

练习 2.8 对一个包含商城系统的 MMORPG,进行风险评估。要求按”概率 × 影响度”矩阵打分,并提出资源分配建议。

风险评估矩阵:

风险场景 概率 影响度 风险值
支付流程金额错误 极高
道具复制 bug 极高 中高
排行榜数据异常
UI 显示错误(不影响功能) 中低
字体渲染问题 极低
极端并发下服务器崩溃

建议是:支付系统(高概率×高影响)应该吃掉 40% 的测试资源
道具复制 bug 虽然概率低,一旦出现影响无法挽回的,也应该分配约 20% 的资源做专项测试。

风险应该决定测试资源的分配优先级。

六、四个方法论提炼

方法论一:时间盒思维

Bug无穷无尽。时间盒思维要求在有限时间内产出最具决策价值的信息,放弃追求完美覆盖

具体做法:

  • 每次测试前明确”宪章”,知道 45 分钟要回答什么问题
  • 复盘总结时可以用信息密度 D 判断效率,低于 0.5 就调整策略

方法论二:培养对临界点的直觉

可以按下面的问题自查:

  1. 比最小值小 1 会怎样?
  2. 比最大值大 1 会怎样?
  3. 中间有没有隐式边界(浮点精度、数据结构限制、网络包大小)?

方法论三:bug链溯源

出现Bug,回溯它的因果链

1
2
3
4
5
6
7
8
9

看到的现象:角色在空中卡住

直接原因:角色坐标.z 异常为 NaN

中间状态:某次跳跃落地检测返回了 NaN

触发条件:跳跃高度 = 0(在地面跳跃),且地形高度数据缺失

每一步都需要”假设→验证”的循环。

Delta 调试、分层日志、确定性重放,都是这个思维过程的工具。

方法论四:玩家视角

成就型玩家、探索型玩家、杀手型玩家会用完全不同的方式拆解游戏。

具体做法:为每种 Bartle 类型设计独立的测试宪章。测经济系统时,想象你是一个成就型玩家,你会最大化收益、找漏洞、研究最优路线。同一套系统,再想象你是一个杀手型玩家,你会找不给钱的途径、恶意交易、利用经济系统攻击其他玩家。

结语

这一章的所有工具都在回答同一个问题:有限时间里,把注意力放在哪里。


第三章 从作弊码到UE5调试后门

教程第三章讲作弊码与调试后门。这一章有时代局限:现在游戏引擎已经内置了调试后门,不需要自己手搓作弊码。

所以这一章不照搬原文。以 UE5 为参照,只挑今天仍然有用的概念,看它们在 UE5 里以什么形态存在、怎么用。

复古游戏主机

一、CheatManager:开发者控制面板

传统作弊码消失了

原文花了很大篇幅讲 KONAMI CODE 的有限状态机实现、输入序列哈希化、动态生成作弊码。这些东西在 2026 年几乎没人在用了,除非你要手搓游戏引擎。

Game Genie:作弊码时代的实体遗产

UE5 里的 CheatManager

UE5 作弊系统的架构:APlayerController 持有一个 UCheatManager 实例,任何标记为作弊的命令都通过它路由:

1
2
3
4
5
6
7
8
// 在 APlayerController 中:
virtual void Cheat(const FString& Msg) override
{
if (CheatManager)
{
CheatManager->Cheat(Msg);
}
}

内置作弊函数包括:

命令 对应函数 做什么
god CheatManager::God() 无敌
fly / walk / ghost CheatManager::Fly() etc. 飞行/穿墙/幽灵模式
teleport CheatManager::Teleport() 瞬间传送
slomo N CheatManager::Slomo(N) 时间缩放,slomo 0.5 即半速
playersonly CheatManager::PlayersOnly() 冻结所有非玩家 Actor
freezeframe N CheatManager::FreezeFrame(N) 冻结渲染 N 秒
viewself / viewbot 切换视角 观察 AI 行为
changeSize N 改变碰撞体积 测试碰撞边界

如果项目需要自定义作弊能力,继承 UCheatManager 并写 UFUNCTION(Exec) 函数即可:

1
2
3
4
5
6
7
8
9
10
11
UCLASS()
class UMyCheatManager : public UCheatManager
{
GENERATED_BODY()

UFUNCTION(Exec)
void GiveAllItems()
{
// 一键灌满背包,用于测试道具 UI 和数值
}
};

然后在 APlayerController 中指定 CheatClass = UMyCheatManager::StaticClass()

这意味着可以合理利用 Cheat 命令:写一个 CheatManager 函数,在控制台敲几行命令,就能进入想要的测试状态。

二、控制台系统

UE5 的控制台架构

UE5 的控制台底层是 IConsoleManager 管理的全局命令注册表。任何 CPP 模块都可以在静态初始化阶段注册命令和变量,运行时通过字符串查找。

1
2
3
4
5
6
7
8
9
10
键盘输入

FConsoleManager::ProcessUserInput()

词法拆分 + 查找已注册的 IConsoleObject

├── FConsoleCommand → Exec(无参数/有参数两种重载)
└── IConsoleVariable → Set/Get with type validation

执行 + 输出反馈(可重定向到日志文件)

命令在编译期注册,运行时只做字符串查找。所以写了新的命令记得重新编译。

UFUNCTION(Exec):把任意函数变成控制台命令

除了通过 IConsoleManager 注册,UE5 还有一种更简单的途径。在任何 UObject 派生类(特别是 APlayerControllerAHUDUCheatManagerAGameMode)上标记 UFUNCTION(Exec)

1
2
UFUNCTION(Exec)
void ResetAllCooldowns();

引擎会自动把这个函数注册为控制台命令 ResetAllCooldowns,参数自动解析。不需要手动调用 IConsoleManager::Get().RegisterConsoleCommand()

你可以在不修改测试框架的情况下,通过控制台直接调用任何标记了 Exec 的函数。
这样一个 Cheat 命令就构建完成了,可以用控制台命令来驱动测试流程。

管道和脚本化

原文练习 3.6 设计了一个支持 |>$var 的控制台解析器。UE5 的控制台不支持管道,但你可以通过 -ExecCmds 启动参数批量执行命令:

1
MyGame.exe -game -log -ExecCmds="open TestLevel, god, spawn_enemy 10, stat fps"

-ExecCmds 里的逗号分隔的命令会依次执行。

三、CVar

如果你想临时打开某个调试显示(比如碰撞框),看完再关掉。
那适合用 CVar,这是一个在控制台可以随时使用的开关。

TAutoConsoleVariable:一行注册

1
2
3
4
5
6
7
8
9
10
TAutoConsoleVariable<bool> CVarShowCollision(
TEXT("p.ShowCollision"),
false,
TEXT("Draw collision wireframes for all physics bodies."),
ECVF_Cheat
);

// 运行时:
// 控制台输入 p.ShowCollision 1 → 开启碰撞可视化
// 控制台输入 p.ShowCollision 0 → 关闭

任何地方都可以读它的值:

1
bool bShowCollision = CVarShowCollision.GetValueOnGameThread();

ECVF 标志位

UE5 的 ECVF_ 类似于标记作用:

Flag 含义 行为
ECVF_Cheat 作弊命令 Shipping 构建中不可设置,不可读取
ECVF_ReadOnly 只读 运行时无法通过控制台修改
ECVF_Scalability 画质可拓展 随 Scalability 组一起变化
ECVF_Default 默认 无特殊限制
ECVF_SetByConsole 来源追踪 标记此值是通过控制台设置的(非代码或 Scalability)

ECVF_Cheat 特别值得关注:在 Shipping 配置下,引擎不仅拒绝修改——它连读取都会返回默认值。带这个标记的变量,在正式发布的游戏里完全不存在。

OnChangedDelegate:值变化即事件

每个 IConsoleVariable 都支持注册回调:

1
2
3
4
5
CVarShowCollision->OnChangedDelegate().AddLambda([](IConsoleVariable* Var)
{
UE_LOG(LogTemp, Log, TEXT("ShowCollision changed to %d"), Var->GetInt());
// 可以触发:重新绘制、断言检查、上报统计
});

测试中的实际价值:可以监听关键 CVar 的变化,作为自动化测试的”断言点”。比如监听 g.GodMode,从 0 变 1 时自动打日志标记”测试环境开始”,从 1 变 0 时做状态一致性检查。

Scalability Groups:预设系统

UE5 的 Scalability 系统其实就是原文说的”CVar 预设”。把一批 CVar 按档位(Low/Medium/High/Epic)打包,通过 sg. 前缀控制:

1
2
sg.ViewDistanceQuality 0  // 最低
sg.ViewDistanceQuality 3 // 最高(Epic)

每个 sg. 命令背后是几十个渲染 CVar 同时变化。测试中的用法:一键切到最低画质跑一轮,切到最高画质再跑一轮,验证渲染不会因为某个 CVar 组合而崩掉。

四、时间操控:被低估的调试工具

UE5 的时间系统

UE5 的时间控制比原文的分层模型更灵活,因为有多个独立的时间膨胀因子

控制层 API 作用范围
全局时间膨胀 AWorldSettings::SetTimeDilation(Value) 整个 World
Actor 自定义时间膨胀 AActor::CustomTimeDilation 单个 Actor 的逻辑时间
Slomo 作弊 CheatManager::Slomo(N) 相当于调 WorldSettings + 全局 TimeDilation
物理 Sub-stepping UPhysicsSettings::MaxSubstepDeltaTime 物理步长的最大上限

时间膨胀公式很简单:ΔT_game = ΔT_real × TimeDilation

关键在于这几层可以独立控制。UI 的时间膨胀因子永远为 1.0;物理系统即使全局 TimeDilation 为 0.1,每帧仍然会按 FixedFrameRate(默认 60Hz 即 0.016s)跑多个 substep。

实战场景

慢镜头(slomo 0.1:排查碰撞 bug。遇到”有时候走不过去”类似问题,放慢 10 倍,就能看到是什么问题。

快进(slomo 10:验证长线系统。比如,装备耐久度 100 次战斗后损耗,slomo 10 后打 10 场就够了。

**playersonly**:冻结所有 AI 和物理,只剩玩家可移动。验证关卡布局时不必被敌人干扰,也用来排查”是不是物理 tick 造成了性能问题”,如果 playersonly 后帧率恢复正常,问题在 AI/物理。

极端缩放下的物理陷阱

原文提到了高速穿墙的问题。UE5(Chaos 物理引擎)的解决方案是通过 sub-stepping 控制:

1
2
3
// UPhysicsSettings 中的关键参数
MaxSubstepDeltaTime = 0.016667f; // 单步最多 16ms
MaxSubsteps = 8; // 单帧最多 8 步

slomo 10 时,一帧的逻辑时间增量是 0.16s。如果物理固定步长是 0.016s,引擎会在这一帧内跑 10 个 substep。但如果 MaxSubsteps = 8,实际只会跑 8 步,剩余的 0.032s 被”吃掉”——物体速度低于预期,甚至可能穿透碰撞。

测试时要注意:极端 slomo 下观察到的行为,必须在 MaxSubsteps 限制的上下文里理解,不能直接等同于”玩家在正常速度下会看到什么”。

五、DemoNetDriver:UE5 内置的确定性回放

这是原文第三章最核心的话题,UE5 对此有一套完整的答案:UDemoNetDriver

视频录像 ≠ 状态回放

很多人以为”回放”是录屏。录屏只是一串 RGBA 像素。Bug 复现需要的是每帧的游戏状态:Actor 的位置、旋转、属性值、RPC 调用序列。

UE5 的回放系统(UDemoNetDriver)做的是状态回放:它记录网络上复制的所有数据,回放时按帧重建游戏状态。

1
2
3
4
控制台命令:
!demorec MyBugReport // 开始录制
!demostop // 停止
!demoplay MyBugReport // 回放

回放过程中可以用:

  • DemoPause / DemoPlay — 暂停/继续
  • DemoScrubPercent N — 跳到 N% 位置
  • DemoGoTo — 跳到指定时间点

工作原理

UDemoNetDriver 继承自 UNetDriver。录制时,它把自己作为一个”幽灵客户端”加入网络会话,拦截所有复制数据和 RPC 调用,序列化写入磁盘。回放时,它扮演服务器的角色,把录制的数据按帧喂给”幽灵客户端”——即你现在看到的回放画面。

这意味着 DemoNetDriver 记录的不是像素,是网络复制流。文件大小取决于游戏中复制了多少数据——一个简单的 FPS 回放文件可能是几 MB/分钟,一个 MMO 的复杂场景可能是几十 MB/分钟。

回放的确定性前提

回放能”完美复现”的前提是游戏逻辑是确定性的。现实是 UE5 的很多系统天然不是确定性的:

  • 物理模拟(Chaos)默认使用非确定性浮点运算
  • 动画蓝图中的随机节点
  • 粒子系统的 GPU 模拟
  • 外部数据(服务器时间、玩家输入时机等)

原文列出的破坏源在 UE5 中依然存在:

破坏源 UE5 中的体现 对策
浮点误差 FMath 在不同平台有不同精度 固定运算顺序,-deterministic 启动标志
多线程 Task Graph 调度不可预测 逻辑线程与渲染线程分离,关掉并行物理
随机数 FMath::RandRange 基于全局种子 FRandomStream 独立种子,录制时保存种子
时间依赖 DeltaTime 随帧率浮动 固定时间步长(FApp::UseFixedTimeStep

Network Prediction 插件

UE5 新引入的 NetworkPrediction 插件是一个更高级的确定性方案。它基于完全确定性的模拟 + 服务器校验模型:

  1. 客户端以确定性方式运行模拟,预测未来 N 帧的状态
  2. 服务器同样以确定性方式运行,只做校验
  3. 一旦客户端和服务器的状态校验和不一致,服务器强制回滚

这原本是为格斗游戏/射击游戏的低延迟设计的,但它天然适合测试:如果你能让游戏逻辑跑在 Network Prediction 的确定性通道上,你就拥有了完美的回放能力——因为每一帧的输入序列和随机种子都被记录,状态完全可重建。

测试实战用法

  1. Bug 复现:给 QA 每人发一个带 !demorec 快捷键的构建。报告 bug 时附上 .replay 文件。开发者拿到手就能回放,跳过”QA 说的步骤我复现不了”的困境
  2. 二分定位:回放时 DemoScrubPercent 跳到 bug 发生前十秒,逐帧推进,精确找到触发帧
  3. 性能分析:回放时挂 Unreal Insights,用完全相同的游戏场景对比不同优化方案的效果——这是性能测试的黄金标准

六、构建配置:让调试后门在正式版里物理消失

原文花了很大篇幅讲作弊码的加密、混淆、服务器验证。在 UE5 的实践中,安全性不靠”把作弊码藏起来”,而是靠构建配置 + 预处理器的物理隔离

UE5 的四种构建配置

配置 编译宏 Console 行为 CheatManager 用途
Debug UE_BUILD_DEBUG 全部可用 完整功能 开发
DebugGame 全部可用 完整功能 调试优化后的引擎代码
Development 全部可用 完整功能 日常开发、QA 测试
Shipping UE_BUILD_SHIPPING ECVF_Cheat 变量不可设置、不可读取 CheatManager 不会被实例化 正式发布

关键点:Shipping 构建中 UCheatManager 根本不会被创建。不是运行时检查”当前是不是正式环境”然后拒绝——而是代码路径在物理上不存在。任何依赖 CheatManager 的逻辑在 Shipping 中都是空操作。

1
2
3
4
5
6
7
8
// Engine/Source/Runtime/Engine/Private/PlayerController.cpp (简化)
void APlayerController::SpawnPlayerCameraManager()
{
// ...
#if !UE_BUILD_SHIPPING
CheatManager = NewObject<UCheatManager>(this, CheatClass);
#endif
}

ECVF_Cheat 的编译器级保护

同理,标记了 ECVF_Cheat 的 CVar 在 Shipping 中也是双重保护的:

1
2
3
4
5
6
7
// ConsoleManager.cpp 中的 Set 路径 (简化)
#if UE_BUILD_SHIPPING
if (CVar->TestFlags(ECVF_Cheat)) // 总是拒绝
{
return; // 不允许设置
}
#endif

这意味着你在 QA 构建中可以放心添加任何调试 CVar,只要标记 ECVF_Cheat,Shipping 就自动安全。

命令注入

UE5 控制台的参数解析没有管道/重定向,所以类 Unix 的 teleport "$(rm -rf /)" 这种注入在 UE5 里不适用。但另有风险:控制台命令可以调用任何标记了 Exec 的 C++ 函数。如果一个 UFUNCTION(Exec) 内部没有对参数做校验,传入了意外的字符串,底层 C++ 代码会出现未定义行为。

实践

  • APlayerController::ConsoleCommand() 接受 FString 参数,任何可以调用它的人都能执行任意控制台命令
  • 多人游戏中,千万不要把服务器端的 ConsoleCommand() 暴露给客户端 RPC
  • QA 构建保留完整的控制台能力即可,不需要额外的”安全层”

七、UE5 特有的调试工具

这部分是 UE5 提供的、在”传统作弊码”时代完全不存在的调试后门。

Gameplay Debugger

'(撇号键)激活。在关卡中覆盖显示所有 AI 决策信息:

  • AI 行为树的当前节点
  • 感知系统(AIPerception)检测到的敌对目标/声音/伤害源
  • EQS(Environment Query System)查询的评分网格
  • NavMesh 路径

测试价值:AI 行为异常的排查神器。”这个 Boss 为什么站着不动”——打开 Gameplay Debugger,看它的行为树卡在哪个节点、它的 Perception 是否”看到”了玩家、EQS 评分是否全为零。

Unreal Insights(Trace System)

Unreal Insights 记录的是引擎级别的完整时间线:每一帧、每一个 Task Graph 任务、每一个函数调用、内存分配、GPU pass。Insights 中的回放窗口支持类似”视频播放器”的操作:播放、暂停、逐帧前进、跳到指定位置。

1
2
启动录制:-trace=cpu,gpu,frame,bookmark
回放分析:打开 UnrealInsights.exe 加载 .utrace 文件

测试价值:当你有一个”偶尔卡一帧”的 bug 且 DemoNetDriver 无法复现时(因为它是网络层的问题),Insights 录制 + 逐帧回放可以精确定位是哪个 Tick 函数的耗时超标。

自动化功能测试(Automation Tests)

UE5 的 GAutomator 支持 Functional Tests,可以直接调用控制台命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FMyGameplayTest,
"MyProject.Gameplay.BossFight_Slomo_DoesNotCrash",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)

bool FMyGameplayTest::RunTest(const FString& Parameters)
{
// 通过自动化测试驱动控制台命令
APlayerController* PC = GetWorld()->GetFirstPlayerController();
PC->ConsoleCommand("open TestBossLevel");
PC->ConsoleCommand("slomo 10"); // 10 倍速跑 boss 战
PC->ConsoleCommand("god"); // 不被打死
// ... 等待、断言、验证
return true;
}

这意味着你可以把控制台命令写进 CI 的自动化测试用例,让它们不再是一次性手敲的调试工具。