03技术文
不写教程搬运,只写我踩过的:每个结论背后都有一次被证伪的旧方案。
ALGORITHM
让求解器替我玩自己的游戏
决策段 12 帧、beam 320、启发式排序、0.5 量化去重——以及为什么「有一关没解出来就拒绝写文件」。
QUALITY
1056 关的换皮,9 条断言说了算
海狮还是海豹?外耳叶像素数回答。第一版眼睛判据被模糊负控证伪(真图 176 / 负控 91),阈值形同虚设——后来怎么改的。
AIGC
别再重掷提示词了:角色一致性产线
free 模型每轮细节必漂,提示词永远锁不住脸。正确分工:模型只供一张母版,一致性交给本地确定性合成。
让求解器替我玩自己的游戏:物理谜题的自动可解性验证
H5 · Beam Search · 2026-11
关卡设计有个尴尬问题:「我觉得这关能过」不算证明,人工试玩又覆盖不了全部走法。我的做法是让程序替我玩——而且玩的必须是同一套物理。
前提:物理内核与渲染分离
游戏是单文件 HTML,但核心模拟层包在 __CORE_BEGIN__/__CORE_END__ 标记里,不碰 DOM。测试加载器用 Node 的 vm 沙箱把这段源码原样抽出来跑——浏览器和游戏逻辑之间没有第二套实现,也就没有「仿真过了真机翻车」的缝隙。
搜索设计
- 决策段:每 12 帧(0.2 秒)做一次输入决策,把连续操作离散成关键帧;
- 动作空间:6 个基础动作(推进 × 转向)+ 能量满且冷却结束时的 2 个爆发动作;
- beam=320:连续状态 + 长时程(关卡时限数百秒),BFS 爆内存、A* 没有可采纳的启发式,beam search 用最优性换覆盖率;
- 去重:位置/速度量化到 0.5、加上存活水母数、珍珠数、能量条,拼成状态指纹,同段撞指纹直接丢。
const EPOCH = 12; // 每个决策段 = 12 帧(0.2s)
const BEAM = 320; // 每段保留的候选数
// 排序:离终点越近越好;顶开的水母/吃到的珍珠有加成;帧数微小惩罚
next.sort((a, b) => (a.s.best - b.s.best) * 1
- (b.s.popped + b.s.pearlsGot - a.s.popped - a.s.pearlsGot) * 12
+ (a.s.f - b.s.f) * 0.02);
写回关卡数据:全有或全无
搜到的解会压缩成关键帧序列,写回每关的 solve 字段——它同时是玩家可回放的「标准答案」。写入是 fail-closed 的:找不到关卡锚点就 throw 拒绝改写,有一关没搜出解就整批不写,先改关卡设计或加大搜索预算。
if (!done) throw new Error('一个解都没搜到,不写文件');
// 主流程:有关卡没搜到解 —— 不写入,先改设计或加大搜索预算
结果
7 关全部可解(7/7),每关输出用时、关键帧数、顶开数、气泡收集率、爆发次数。求解脚本和关卡数据同仓库,任何人 node tests/search_solve.js 复跑一遍就能核对这页面上的数字。
1056 关的换皮工程:9 条精灵断言,每条配负控
Godot 4.7 · 内容管线 · 2026-11
《小海狮爱洗澡》从早期鳄鱼皮换成海狮皮,1056 关、美术音频全自产。换皮最大的风险不是画错,而是「512 源图看着挺好,游戏里 60px 一坨糊」——肉眼验收在这个量级上不可靠,得让脚本说了算。
在真实绘制尺寸上量
游戏里精灵只被 GPU 缩小一次(512→约 60px,无 mipmap 的 GL_LINEAR)。审计脚本先用 PIL 逐像素复现这条采样链,然后在「60px 结果图」上做可读性断言,而不是在源图上自欺。
9 条断言
| 断言 | 在测什么 | 阈值 |
| whisker_outside_body | 胡须必须探出身体轮廓(第一识别特征) | >400 px |
| ear_flaps_present | 外耳叶——「海狮」而非「海豹」的唯一硬证据 | >150 px |
| nose_darkest_block | 鼻镜是脸上最暗成块区(额头区做对照) | 比值 ≥4.0 |
| flipper_span | 前鳍张开到画幅两侧,体型读起来是鳍脚类 | 跨度 ≥0.86 |
| coverage_vs_croc | 替换后体量不变(鳄鱼基线 80.1%) | 78–88% |
| portal_no_croc_green | 传送门贴图里不得残留旧角色 | =0 |
| portal_no_baked_seal | 也不得预烘焙新角色(运行时只画一次) | <50 且正控>1000 |
| disp_eye_structure | 60px 下眼里必须同时有暗块和亮块 | ≥12 且负控不过 |
| disp_pupil_dark | 60px 下瞳孔占得住(不是白饼) | ≥0.20 且负控不过 |
判据自己也要被审计
每条判据配「模糊负控」:把小于 50px 的结构全抹掉后必须不及格——如果糊图也能过阈值,说明判据在空转。第一版眼睛判据用「眼内明暗极差」,实测真图 176、糊负控 91,阈值形同虚设,被负控当场证伪;换成「暗块/亮块成块计数取 min」后真图 22、负控 0,才敢上线。残留检测同理配「正控」:合成一块角色脚印大的色块,同一判据必须看得见,否则「0 像素」只是假绿。
结果
9/9 PASS 才算换皮完成。关卡本体是数据驱动(levels.gd + 23,745 行的 levels_extra.gd 数据),内容与代码分离,加关卡不动逻辑。审计脚本在仓库里:python tools/sprite_audit.py。
别再重掷提示词了:一条角色一致性 AIGC 产线
ComfyUI · 图像处理 · 2026-11
用 AI 出游戏素材最先撞的墙:同一个角色,每轮细节必漂——腮红位置、鼻形、描边粗细全在变。反复重掷提示词永远锁不住脸,这是概率采样的本质,不是你 prompt 写得不够好。
把「生成」和「一致性」拆开
- 模型只干一件事:出一张绿幕背景的角色母版(姿态、表情、配色定稿);
- 抠图前先验背景:取样四角像素确认是纯绿——模型经常给「看起来是绿」的底色,不验就抠,整张作废;
- 确定性合成:本地 sharp 做 chroma-key 抠成透明精灵,图标、头像、游戏内贴图全部用同一张母版缩放合成。
一致性从「祈祷采样」变成「文件复制」,100% 可复现。云端(ComfyUI / 图像编辑 API)负责产能,本地负责确定性,各干各擅长的。
踩坑清单
- 图像模型直出的 RGBA alpha 常常不是真透明(极值全 255 或残留底色),抠图要用像素级校验,别信「看着是透明」;
- 本机手写 PNG 编解码是坑,交给 sharp 这类库;
- 出图验收要量化的:alpha 极值分布、透明像素占比,不是肉眼扫一眼。
结果
这条产线支撑了合集里多个游戏的图标与角色贴图:一个角色,全场景同一张脸。它也是我对「AIGC 落地」的理解——产线不是会写 prompt,是把不确定的模型框进确定的流程里。