资源审计与截图回归
测试对象不是代码而是资源与画面:图集物理属性的静态断言、审计脚本家族、截图回归证据链与一次性手术脚本留痕,来自两个真实游戏项目的工程实践。
知识点地图
- 知识类别:资源审计与截图回归(测试工程化家族;测的不是代码逻辑,而是资源数据与画面产出)。
- 解决什么问题:游戏与富客户端项目里,一半以上的「bug」来自美术改图、数据手改、字体裁剪、序列化迁移——这类破坏编译器看不见、单元测试碰不到,需要把「资产约定」本身变成可执行断言。
- 什么时候用到:图集/瓦片/关卡数据被非程序员频繁修改时;UI 画面需要防止悄悄劣化时;字体、配置等大宗资产做瘦身手术时;需要向团队证明「改了图但没改坏」时。
- 同族文档分工:《CI/CD 测试门禁》讲这些脚本如何编排成门禁链;本篇讲每道门禁测什么、怎么写证据。
前置知识:知道测试断言与退出码的概念即可;例子用 Godot(GDScript/命令行)呈现,模式可平移到 Unity、Web 与任何有「资源管线」的项目。
一、心智模型:约定会腐烂,除非它有断言
一个真实场景链:美术说「我给地面图集换了两张图」,一周后玩家反馈「贴着 平台边缘走会掉下去」。排查半天发现:美术替换的瓦片在图集里挪了位置, 物理碰撞多边形没有跟着挪。代码零改动、测试全绿——坏的是数据。
这类问题的共同结构:代码与数据之间有一份「约定」(这格瓦片必须是整格实 心物理、那类是单向平台、装饰层零物理),而约定只活在某个人的记忆或设计 文档里。工程解法只有一个:把约定写成静态断言脚本,资源一动就跑。
三个场景例子:
- 真实工程场景(物理哨兵):speed-rouge(Godot 4 横版平台)在图集重构后引入
tools/check_tiledata.gd,逐瓦断言物理属性,CHANGELOG 明确记录它「改造为新图集物理哨兵」——从一次性的迁移工具升级为常驻门禁。 - 字体子集:构建产物里 17.77MB 的原版字体被重建为 0.63MB 子集(语料 1398 个字符),配套
font_coverage_check断言「游戏文本用到的每个字符都在子集里」——缺一个字符就是渲染成豆腐块的生产事故。 - 关卡数据:手工摆放的 200 个关卡文件,门的坐标、出生点脚下是否有地这类约定靠人眼审不动,审计脚本逐文件扫出「门悬空」「出生点无地面」。
二、资源静态断言:给图集写物理哨兵
以瓦片图集为例。图集是按约定分源组织的:源 0 地面(47 变体整格物理)、 源 1 单向平台(可从下方跳穿)、源 2 装饰(零物理)、源 3 特殊件(坡、半 块、立柱各自独立形状)。哨兵脚本断言四条约定:
tools/check_tiledata.gd 的断言清单(模式示意):
1. 源 0 的每一瓦:必须存在一个覆盖整格的物理多边形
—— 地面一旦出现「半格物理」,角色会陷进地里
2. 源 1 的每一瓦:必须是单向碰撞形状
—— 被误改成实心物理后,玩家无法从平台下方跳上来
3. 源 2 的每一瓦:物理多边形数量必须为 0
—— 装饰带物理后,玩家会被空气墙卡住
4. 源 3 逐瓦核对形状类型:坡是三角、半块是半高矩形
—— 形状与贴图不符时,视觉与碰撞错位断言失败时脚本 print 出「源编号 + 瓦片坐标 + 违反的约定」并以非零退出
码结束——打印即报告、退出码即门禁,CI 不需要理解 Godot 就能拦截合并。
写资源哨兵的三条经验:
- 断言「不变量」而不是「当前值」:写「源 2 零物理」而不是「源 2 第 3 瓦的 多边形是 [0,0, 16,0, 16,16]」——前者是约定,后者是把数据抄进代码。
- 哨兵要跑得快:静态断言只读数据、不开渲染,整个图集毫秒级扫完,才配 进每次提交的门禁。
- 数据格式变了哨兵会先红:这反而是它的另一个价值——格式迁移漏改某处 时,哨兵充当格式回归。
三、审计脚本家族:每类资产一个「安检口」
一个成熟的资源型项目会养出一族审计脚本,各盯一类资产:
| 审计脚本 | 盯什么 | 抓什么问题 |
|---|---|---|
leveldata_audit | 关卡数据文件 | 缺出生点、数据越界、引用了不存在的图集坐标 |
door_audit | 门与传送点 | 门悬空、两侧不互连、指向不存在的关卡 |
font_coverage_check | 字体子集覆盖 | 游戏文本字符超出子集语料(渲染豆腐块) |
hotpath_audit | 每帧热路径代码 | 在 _process 里做分配、查表等高频开销 |
keybind_audit | 键位绑定 | 动作没绑定、双绑定、与 InputMap 漂移 |
dump_tile_usage / clean_waste | 资源使用率 | 图集里从未被引用的「死瓦」 |
这一族的共性值得点破:审计脚本就是「把评审标准自动化」。人眼评审 关卡的检查单逐条写成了脚本,从此检查不依赖Reviewer的耐心与记忆。它们与 单元测试的区别是断言对象——单测断言代码行为,审计断言资产数据与约定。
四、截图回归证据链:把「画面没坏」变成可审查的产物
UI 与画面的问题(布局错位、缺贴图、文字溢出)很难用数值断言捕捉,工程 落法是截图证据链:
截图回归通道的四个环节:
1. 采集:tests/shot_all.gd 之类的脚本逐场景打开游戏、定位、截图落盘
(配套 win_shot 等按窗口/房间细分的变体,autoshot/setshot/roomshot/
tapshot/bootshot 系列覆盖不同触发方式)
2. 产物:每张截图带日志行,如 SHOT_SAVED <场景名> <文件名>,
汇入 run.log —— 产物可追溯、可核对完整性
3. 审查:人工目检或图像 diff 与基线对比;变更处即评审对象
4. 归档:截图与日志随版本留存,出问题回溯「哪个版本开始坏的」真实产物长什么样:一个截图工具项目的 run.log 里记录了引擎版本、渲染
驱动、GPU 型号与两行 SHOT_SAVED——环境信息与产物同行,半年后回看仍能
复现采集条件。这就是「证据链」与「随手截两张图」的区别。
配套的还有冒烟退出码:另一个真实项目(flower-card,Godot mono + C#)
的门禁是 godot --headless --quit-after 120 跑完冒烟后打印
[SMOKE] Main ready 并以退出码 0 结束;README 同时注明引擎侧 RID 清理
告警属强退噪声、不算失败。要点:冒烟输出必须可 grep、退出码必须干净、
已知噪声必须在文档里显式标注——否则门禁报告会慢慢被噪声淹没。
截图链与哨兵/审计的分工:哨兵断言「数据符合约定」,截图证明「画面如 预期」;前者自动判对错,后者把对错的最终裁决留给带上下文的人眼。
五、一次性手术脚本留痕:破坏性操作必须可审计
数据迁移、批量改图、清理冗余这类破坏性操作,两个选择:手工在编辑器 里逐个点(不可审计、不可复现),或写成一次性脚本(可评审、可重放、可 留痕)。真实项目里的形态:
tools/ 下的一次性脚本家族(模式示意):
migrate_level_tiles.py 一次完成 2229 + 20 瓦的重铺迁移
oneoff_clean_overrides.py 清理历史遗留的手工覆盖项
oneoff_levels1_surgery.py 对旧关卡数据做定点手术
共同特征:
- 命名带 oneoff / migrate 前缀,天然声明「我是一次性的」
- 输入输出明确:读旧数据 -> 变换 -> 写新数据 + 打印统计
- 执行结果写进 CHANGELOG(如「migrate_level_tiles 一次性完成 2229+20 瓦重铺」)
- 脚本本体留在仓库里作为手术记录,不删逐条解释为什么「留痕」是关键设计:脚本留在仓库里,半年后有人问「这批瓦 片为什么坐标全变了」,答案就是一个可执行的文件而不是一段口口相传;打印 的统计数字(2229 + 20)写进变更日志,评审者不用重跑就知道手术范围。换 成手工操作会发生什么:同样的事也能做完,但「哪些文件被动过、动了多少」 从此靠回忆——下一次数据迁移的事故排查成本直接翻倍。
与第 2 节哨兵的关系:手术脚本负责「把数据搬到新约定」,哨兵负责「验证 新约定成立」——一次成功的迁移 = 手术脚本跑完 + 哨兵转绿,两个工具是 一对,缺一个就有「搬完没验」或「验了没法搬」的窟窿。
动手实践
任务:给一个图集改图后,设计一套「自证物理未破坏」的证据包。
提示:
- 证据包 = 改图前的基线快照 + 改图后的断言输出 + 截图对比,三样缺一不可;
- 静态断言按第 2 节四条约定写,逐瓦报告违规坐标;
- 截图至少覆盖「贴地行走」「平台下方上跳」「装饰物旁通过」三个物理敏感场景;
- 全部产物汇总成一行可复现的命令序列。
参考实现(先自己写,再对照):
# 1. 改图前基线:断言全绿 + 截图归档
godot --headless --script tools/check_tiledata.gd # 期望 TILEDATA ALL PASS
godot --headless --script tests/shot_all.gd # 期望逐行 SHOT_SAVED
# 2. 执行改图(美术替换瓦片 / 数据迁移脚本)
# 若是批量手术,走一次性脚本并记录统计:
godot --headless --script tools/restyle_tiles.gd # 打印改动统计,写入 CHANGELOG
# 3. 改图后复核:同一套命令 + 三个物理敏感场景的对比截图
godot --headless --script tools/check_tiledata.gd # 必须再次 ALL PASS
godot --headless --script tests/shot_all.gd # 与基线截图逐张对比
# 4. 结论三行:断言结果 / 截图差异点 / 是否可合并对照要点:这套流程的本质是「把口头保证(我没改坏)换成产物证据(两份 PASS 日志 + 三张对比图)」。评审者不看你的态度,看产物——这也是它和 「我测过了」三个字的全部区别。
坑点与自检
| 坑 | 现象 | 对策 |
|---|---|---|
| 只测代码不测资产 | 美术改图破坏物理约定,测试全绿 | 约定写成静态断言哨兵 |
| 审计脚本跑在个人机器上 | 谁记得跑谁测,离职即失传 | 全部挂进门禁或定时任务 |
| 截图只截「好看的」 | 布局炸裂的页面没有记录 | 采集脚本逐场景全覆盖,不看美观 |
| 冒烟噪声不标注 | 真告警被噪声淹没,门禁疲劳 | 已知噪声写入 README 与报告注释 |
| 手术不留痕 | 迁移范围无人可答 | 一次性脚本 + 统计数字进变更日志 |
| 哨兵抄当前值当断言 | 数据合法变更也报错 | 断言不变量,不断言快照 |
自检清单:
- 项目里「代码与数据的约定」清单化,每条约定有对应断言或审计脚本
- 截图证据带环境信息与完整性日志,可回溯到版本
- 破坏性数据操作全部走一次性脚本且结果留痕
- 门禁报告区分「失败」与「已知噪声」
练习
- 给你项目的某个资源目录(配置表、图集、图标皆可)列五条「代码依赖它」的约定,并为其中两条写静态断言脚本。
- 搭一条最小截图证据链:三个页面/场景、采集脚本、
SHOT_SAVED式日志、一次真实的改版对比。 - 假设你要把项目的图标目录从 PNG 迁移到 WebP(或任意格式迁移),按第 5 节模式写手术脚本方案:输入、变换、统计输出、哨兵验证四步各做什么。
- 找出你们仓库里「跑在某人机器上」的检查脚本,列出迁移到 CI 的顺序(先快、先常红、先挡事故的先迁)。
下一步
- 这些脚本如何编排成提交门禁:见《CI/CD 测试门禁》。
- 手工检查到自动化的边界判断:见《从手工到自动化》。
- 测试计划里如何为「资源回归」排期与定退出准则:见《测试计划与风险》。