前置知识: 软件工程与测试

资源审计与截图回归

9 min入门

测试对象不是代码而是资源与画面:图集物理属性的静态断言、审计脚本家族、截图回归证据链与一次性手术脚本留痕,来自两个真实游戏项目的工程实践。

知识点地图

  • 知识类别:资源审计与截图回归(测试工程化家族;测的不是代码逻辑,而是资源数据与画面产出)。
  • 解决什么问题:游戏与富客户端项目里,一半以上的「bug」来自美术改图、数据手改、字体裁剪、序列化迁移——这类破坏编译器看不见、单元测试碰不到,需要把「资产约定」本身变成可执行断言。
  • 什么时候用到:图集/瓦片/关卡数据被非程序员频繁修改时;UI 画面需要防止悄悄劣化时;字体、配置等大宗资产做瘦身手术时;需要向团队证明「改了图但没改坏」时。
  • 同族文档分工:《CI/CD 测试门禁》讲这些脚本如何编排成门禁链;本篇讲每道门禁测什么、怎么写证据。

前置知识:知道测试断言与退出码的概念即可;例子用 Godot(GDScript/命令行)呈现,模式可平移到 Unity、Web 与任何有「资源管线」的项目。

一、心智模型:约定会腐烂,除非它有断言

一个真实场景链:美术说「我给地面图集换了两张图」,一周后玩家反馈「贴着 平台边缘走会掉下去」。排查半天发现:美术替换的瓦片在图集里挪了位置, 物理碰撞多边形没有跟着挪。代码零改动、测试全绿——坏的是数据。

这类问题的共同结构:代码与数据之间有一份「约定」(这格瓦片必须是整格实 心物理、那类是单向平台、装饰层零物理),而约定只活在某个人的记忆或设计 文档里。工程解法只有一个:把约定写成静态断言脚本,资源一动就跑。

三个场景例子:

  1. 真实工程场景(物理哨兵):speed-rouge(Godot 4 横版平台)在图集重构后引入 tools/check_tiledata.gd,逐瓦断言物理属性,CHANGELOG 明确记录它「改造为新图集物理哨兵」——从一次性的迁移工具升级为常驻门禁。
  2. 字体子集:构建产物里 17.77MB 的原版字体被重建为 0.63MB 子集(语料 1398 个字符),配套 font_coverage_check 断言「游戏文本用到的每个字符都在子集里」——缺一个字符就是渲染成豆腐块的生产事故。
  3. 关卡数据:手工摆放的 200 个关卡文件,门的坐标、出生点脚下是否有地这类约定靠人眼审不动,审计脚本逐文件扫出「门悬空」「出生点无地面」。

二、资源静态断言:给图集写物理哨兵

以瓦片图集为例。图集是按约定分源组织的:源 0 地面(47 变体整格物理)、 源 1 单向平台(可从下方跳穿)、源 2 装饰(零物理)、源 3 特殊件(坡、半 块、立柱各自独立形状)。哨兵脚本断言四条约定:

text
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 与画面的问题(布局错位、缺贴图、文字溢出)很难用数值断言捕捉,工程 落法是截图证据链:

text
截图回归通道的四个环节:

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、退出码必须干净、 已知噪声必须在文档里显式标注——否则门禁报告会慢慢被噪声淹没。

截图链与哨兵/审计的分工:哨兵断言「数据符合约定」,截图证明「画面如 预期」;前者自动判对错,后者把对错的最终裁决留给带上下文的人眼。

五、一次性手术脚本留痕:破坏性操作必须可审计

数据迁移、批量改图、清理冗余这类破坏性操作,两个选择:手工在编辑器 里逐个点(不可审计、不可复现),或写成一次性脚本(可评审、可重放、可 留痕)。真实项目里的形态:

text
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 节四条约定写,逐瓦报告违规坐标;
  • 截图至少覆盖「贴地行走」「平台下方上跳」「装饰物旁通过」三个物理敏感场景;
  • 全部产物汇总成一行可复现的命令序列。

参考实现(先自己写,再对照):

bash
# 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 与报告注释
手术不留痕迁移范围无人可答一次性脚本 + 统计数字进变更日志
哨兵抄当前值当断言数据合法变更也报错断言不变量,不断言快照

自检清单:

  • 项目里「代码与数据的约定」清单化,每条约定有对应断言或审计脚本
  • 截图证据带环境信息与完整性日志,可回溯到版本
  • 破坏性数据操作全部走一次性脚本且结果留痕
  • 门禁报告区分「失败」与「已知噪声」

练习

  1. 给你项目的某个资源目录(配置表、图集、图标皆可)列五条「代码依赖它」的约定,并为其中两条写静态断言脚本。
  2. 搭一条最小截图证据链:三个页面/场景、采集脚本、SHOT_SAVED 式日志、一次真实的改版对比。
  3. 假设你要把项目的图标目录从 PNG 迁移到 WebP(或任意格式迁移),按第 5 节模式写手术脚本方案:输入、变换、统计输出、哨兵验证四步各做什么。
  4. 找出你们仓库里「跑在某人机器上」的检查脚本,列出迁移到 CI 的顺序(先快、先常红、先挡事故的先迁)。

下一步

  • 这些脚本如何编排成提交门禁:见《CI/CD 测试门禁》。
  • 手工检查到自动化的边界判断:见《从手工到自动化》。
  • 测试计划里如何为「资源回归」排期与定退出准则:见《测试计划与风险》。