← 工程笔记

ENGINEERING NOTE · 2026-10

13KB 一个 3D 游戏:把 Unifrost 拆开看

js13kGames 2026 的 3D 平台跳跃游戏。不看教程,直接解剖它的发布产物。

为什么拆它

js13kGames 的规则粗暴:整个 zip 不超过 13KB,在浏览器里跑。在这种约束下,虚胖的架构选择活不下来:没有 three.js(它光运行时就把预算吃穿),没有资源文件,也没有构建缓存可以藏。

Unifrost 是这届里少见的纯手写 WebGL 3D 平台跳跃(不是 raycaster,也不是 2D 假装 3D)。我把自己日常用的逆向分析工具链(REA,一个开源 MCP)对着它的发布 bundle 走了一遍,全程本机、静态、不执行游戏代码。这篇是现场记录。

第一层:它的全部家当

构建产物清单,一共两个文件:

dist/
├── index.html   24,706 B(19,784 B 内联 JS + 样式 + canvas)
└── b             5,629 B(二进制关卡数据)

构建日志的最后几行是这届比赛最迷人的画面:

Used: 93.134%
Remaining: 914 bytes (0KB 914B)
All good!

闸门是构建插件里硬编码的:产物过 advzip 压缩,超 13,333 字节直接让构建失败。预算不是文档里的倡议,是 CI 里的一行断言。(我的 3D 小镇项目用同样的思想管首屏 gzip,只是额度宽裕些。)

第二层:5,629 字节里装了什么

仓库源码是 TypeScript,构建的第一步是一个自定义生成器:node generate-bin-data.ts。它把全部关卡内容序列化成那个叫 b 的二进制文件,同时生成三个常量模块(对象索引、段偏移、对话表)。

二进制文件按段组织,读取端用段偏移游标扫:

palette → objectBank → levelObjects → npcs → balloons → shards
       → clouds → safeClouds → (end)

objectBank 是精髓:11 个"渲染对象"(独角兽、柱子、墙、屋顶、气球、NPC……)的顶点数据直接以二进制打包,共享一个调色板段。没有 glTF、没有 OBJ、没有解析器,顶点格式是作者自己定的,读取代码就是 DataView 逐字段抠。

11 个对象撑起整个 3D 世界,靠的是三件事:几何复用(柱子旋转 90° 就是横梁)、顶点级实例化、以及把"材质"退化成调色板索引。

顺带一个细节:生成器有鸡生蛋问题——*.gen.ts 在 .gitignore 里,但模块加载又需要它们。我在容器里复现构建时,得先按 import 清单写三个 stub 常量文件引导第一遍生成,之后才是真实索引。作者本地不会遇到这个问题(生成文件早就存在),这是开源"可复现构建"的一个典型暗坑。

第三层:逆向工具看到了什么

把 dist 目录喂给 REA 的 JavaScript 静态分析器(解析 AST,不执行代码),对 19.7KB 内联 JS 的读数:

读数值怎么读
AST 节点8,112单文件模块,无 import/export 关系——全部代码一个作用域树
网络调用0除加载自身外零联网:无遥测、无 CDN、无统计
存储关系1 个 keylocalStorage.getItem/setItem("unifrostBest")——最高分,就这一处
可疑面无无 eval 动态码、无 WebSocket、无跨域端点

工具同时诚实地标注了自己的边界:"静态导入、入口点、worker、端点与存储关系是从惰性语法恢复的,未执行 bundle 引导代码"。静态分析≠运行时观察,这份克制是工具输出该有的样子——它给你证据 ID 和覆盖率,而不是一个拍脑袋的"安全"。

一个实用发现:REA 的多文件 JS 图谱对"单 HTML 内联一切"的产物会退化成只认 HTML 资源壳——得先把内联 script 抽成 .js 文件再喂,AST 解析才真正跑起来(0 → 8,112 节点)。这是这类工具的通用注意点:它的输入假设和你的产物形态未必匹配,读数从 0 变非 0 的那一步才是真分析。

第四层:作者怎么说的

README 里的 AI 披露值得一抄(2026 年了,这就是新的职业礼仪):

游戏的任何设计、美术、音频、对话都没有用 AI。我常用聊天机器人回答关于 API、算法和语言细节的问题。有一个函数(createPill)主要由 LLM 生成。我没有使用任何 agentic AI。

边界划得很清楚:问答算工具,代写一个函数要声明,代理式不算数。比"全 AI 生成但我不说"和"用了 LLM 查文档心虚到不敢说"两个极端都诚实。

搬回自己项目的东西

  1. 预算写进构建,不写进 README。"超了会红"比"请控制体积"有效一万倍。
  2. 数据段化 + 自定义二进制是被低估的武器。当资源格式本身占预算大头时(glTF 的 JSON 壳、PNG 的元数据),自己定格式的收益是数量级的。
  3. 生成器要能冷启动。检查 npm run build && git clean -dxn 后还能不能一键复现——Unifrost 差一步(stub 引导),我的小镇项目也为此专门维护 verify.sh。
  4. 逆向是选型的一部分。想知道"别人的 App 这个功能怎么做的",对发布产物做静态分析比读评测文章可靠——它给你的是证据,不是观点。

游戏与源码:Unifrost 条目页;分析工具:morluto/rea(MIT)。