25 档 opus 审查(对照代码/契约/git)→ 9C+23M+26m 真过时/错误 → 18 档 opus 修复(各自先按 commit/file:line 复核再改,1 项审查误判经复核拒绝)。主要簇:① gameDefinition 终态对齐——固定架构/SAA编排把'真结构化已成立'降级为'通往 src/ 的中间脚手架 + behavior.code 内嵌串=已知债',跨档对齐 README §3.2/设计裁决(06-20 创始人定调);② WG1 门面缺口按 06-14 scale-20 实测修订(预判几乎全推翻);③ 开闸接线改完成时(generic 桥接 7534bdf9 + 5 品类模板回填 046c061d 已落地);④ Flyway 数字 V17/V18→V25;⑤ 会员订阅(V21 ca53a0db)/community(U3 3f35acb4)/资产渲染(U1 73d33916)状态更新;⑥ prompt治理(classpath 加载)/引擎(Phaser gz)/前端(token 名·prefers-color-scheme)/运维(mini-infra=PostgreSQL·push=阿里云Gitea)细节订正。全 26 档过门(≤2000/有图/品牌绘境AI)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
引擎与运行时 · 架构设计文档
这是什么:绘境AI 游戏引擎层与运行时装载体系的架构设计主文档,回答「选了哪个引擎、为什么选、游戏怎么挂上去、包怎么交付」。 给谁看:后端 / 前端工程师、AI 生成链路开发者、引擎集成评审方。 怎么读:先读第 1–2 节建立全局认知(选型结论 + 一张架构图),再按模块深入第 3–5 节。
1. 一句话与一张图:引擎层做什么
绘境AI 的游戏以「信息流即刷即玩」的方式送达玩家,首屏冷启动时间直接决定用户是否留下来。引擎层的核心任务就是:让 AI 生成的游戏在千元机 + 4G 网络下,从点击到可玩不超过 3.5 秒。
为此,整个引擎层由三件事共同支撑:
- 选一个足够轻量的游戏引擎——这是体积与冷开速度的根本。
- 定义一套稳定的装载契约——让 AI 生成的游戏代码、引擎宿主、Runner(运行时载体)三者能通过同一套接口连接,不互相耦合。
- 明确能力边界——哪些能力由引擎提供、哪些需要补层,边界清晰才不会让游戏代码直接依赖引擎内部。
下图展示了从「AI 生成的 bundle(游戏代码包)」到「在玩家设备上渲染出可交互画面」的完整链路:
flowchart TB
subgraph gen["AI 生成侧(便宜模型 agent 按装载契约产出)"]
BD["engineBundle<br/>游戏代码包<br/>随 manifest JSON 内嵌"]
end
subgraph runtime["game-runtime · Runner v2(运行时)"]
LOAD["bootGameHost<br/>装载入口"]
HOST["集成段 host<br/>唯一 import 引擎处<br/>(Q4 铁律)"]
CAP["engine-caps.js<br/>能力包装层<br/>particles / audio / math"]
CTX["PluginContext<br/>受控面 6 项<br/>game-host.d.ts 第9类契约"]
end
subgraph engine["LittleJS 增强发行版(Tier1 引擎 · 55KB gz)"]
EI["engineInit<br/>五回调驱动主循环"]
MC["mainContext<br/>唯一绘制面(Canvas 2D)"]
end
subgraph game["游戏侧(装载契约·零引擎 import)"]
GM["game.init(ctx)<br/>game.update(dt)<br/>game.render(g)<br/>game.onInput(ev)"]
end
BD -->|window.__GameBundle 全局挂载| LOAD
LOAD --> HOST
HOST --> EI
EI -->|gameUpdate 回调| GM
EI -->|gameRender → mainContext| GM
HOST --> CAP
CAP --> CTX
CTX -->|ctx.getEngine() 受控面| GM
GM -. 禁止 .-> |直接 import littlejsengine| ERR["❌ Q4 违例"]
2. 引擎选型:为什么是 LittleJS
2.1 终裁结论
Tier1 游戏引擎 = LittleJS 增强发行版(55KB gz,2026-06-12 创始人终裁,对比候选引擎 Phaser,计分 85 vs 82)。
LittleJS(轻量级 JavaScript 游戏引擎,GitHub 开源项目)是针对极简、快速加载场景设计的 2D 引擎,体积约为 Phaser(功能更全的主流 2D 引擎)的 1/10。
绘境AI 选择它的核心理由只有一条:冷启动速度。
2.2 硬数据对比
「冷开」(Cold Open,即用户首次打开游戏、从零加载到可交互状态的时间)是信息流场景的决定性指标——这是玩家点击一款从未玩过的游戏时的真实体验。
| 指标 | LittleJS | Phaser |
|---|---|---|
| 裸冷开时间(千元机 + 4G) | 0.54 秒 | 2.86 秒 |
| gz 包体积 | 55KB | 326.7KB(raw 1.24MB) |
| 综合评分(S2 实测) | 85 分 | 82 分 |
首屏 P75(第 75 百分位用户的加载时间)目标是 < 3 秒,点卡可玩(从点击到可交互)目标是 S2 常态 ≤ 2 秒。Phaser 在千元机 + 4G 环境下的冷开时间已占去大部分预算,3G / 弱网长尾用户的出线风险显著上升,因此不适合绘境AI 的分发场景。
敏感性分析的结论是:即使把四组主观裁量维度全部拉平(双方同分),LittleJS 仍凭 S2 硬数据差(20 分 vs 12 分)领先——选型结论对裁量分不敏感。
2.3 体积约束的演进
早期曾有「13KB 极限」(js13k 游戏极限分支的约束)和「15KB 红线」(srcdoc 内联嵌入架构的衍生约束)两条旧规则。这两个前提架构已废除,对应约束一并失效。请勿再引用「13KB」或「15KB」口径。
现行约束是三层框架:
- SLO 地板(服务质量下限,必须满足):千元机 + 4G 首屏 P75 < 3 秒;点卡可玩 S2 常态 ≤ 2 秒。
- 预算入场券 B1(超出则体积风险预警):gz ≤ 350KB、raw ≤ 1.5MB。
- S2 实测为主考:以真机在 S2(标准 2G/4G 弱网)环境下的实测数据作为最终评判依据。
2.4 复议条件
终裁包保留复议权(终裁包 §4.5-3):若后续拔高样板游戏时暴露引擎级阻塞,可复议。切换成本仅限模板层重写,Runner(运行时载体)和装载契约层不受影响——这是两层架构设计的重要价值之一。
3. 模板哲学:模板是什么,不是什么
在绘境AI 的语境里,「模板」一词有严格的边界定义。厘清这一点,可以避免生成链路的实现方向走偏。
3.1 宪法级定义
模板 = LittleJS 能力插件 / 二次开发件。每个插件封装一项引擎能力(碰撞检测、粒子特效、物理模拟、手感反馈……),对外只暴露公开 API,可即插即用、可组合、版本化管理。
模板不包含:玩法设计、美术成品、关卡数据、UI 布局——这四项是 AI agent 在生成游戏时的生成域,由 agent 按游戏定义产出,不由模板预制。
3.2 「游戏模板」与「玩法模板」的区分
历史上曾有「游戏模板」概念,即填参式的整局代码、预先写好的 4 套游戏。这类模板已于 W-CLEAN(清场阶段,对应 Wave 清场任务)中删除,不再存在。
「玩法模板」(品类框架,用于引导 AI 生成特定品类的游戏,本身是框架性描述而非预建代码)则是有效功能,状态是待建、非最高优先级——最高优先级是 Tier 0 生成可靠性(即 AI 能稳定地生成可运行的游戏),玩法模板排其后。
这一区分由 HJ-DEMO-AUDIT-001(2026-06-17 创始人纠偏裁定,本档唯一权威口径)确立。历史 spec 中出现的「玩法模板永久废除」措辞属 2026-06-12 旧记录,已被本节取代。
3.3 可测性红线(硬门)
AI agent 生成的游戏必须导出取证清单:可交互几何(画布上的可点击区域)、锚点接线(事件绑定关系)、胜败可达断言(存在可以触发游戏结束的路径)。不可机器测试的游戏不许通过评估门。
好玩基线 v2 五要素(手感 / 美术统一 / 音乐 / 结构深度 / 角色壳)已从模板层迁出,挂在评估门上判结果值,不由模板预置。
4. 两层装载契约:引擎、宿主、游戏如何连接
这是引擎层最核心的架构设计,解决「AI 生成的游戏如何安全、稳定地挂载到引擎上」的问题。
4.1 为什么需要两层
历史上存在三个「断口」:
- 真引擎宿主:有掌帧(控制渲染循环),但没有游戏挂进去。
- 真游戏 ref:有游戏代码,但没接引擎。
- 生成产物:AI 生成的代码喂给一个掏空的旧版 runtime(运行时),实际跑不起来。
如果给三者各造一套接引擎的方式,就会产生三套漂移——这违反「同一职责不留两条并行路」的边界原则,并且迟早需要三次返工才能对齐。两层装载契约的设计,就是给三者提供一份共同遵守的约定,让它们通过同一套接口连接。
4.2 下层契约(已冻结)
下层解决「插件如何安全获取引擎能力」和「游戏包如何交付」。
PluginContext.getEngine():受控引擎能力面,共 6 个方向,定义在api.d.ts。插件只能通过这个接口获取引擎能力,不能直接访问引擎内部。- GamePackage
engineBundle:契约 #4(additive 追加字段),游戏代码包内嵌于 manifest JSON,随包交付。 - SDK storage 根契约:游戏存档与状态持久化的统一接口。
下层于 commit 28a57b8 冻结,不再修改。
4.3 上层契约(本弧 spike 实现)
上层解决「一款完整游戏(不只是插件)如何挂到引擎宿主」。
游戏宿主装载契约,定义在 game-runtime/src/core/game-host.d.ts,是第 9 类契约(additive 追加)。它落在 game-runtime 内部而非 contracts/ 顶层目录,原因是消费方只在 game-runtime 这一个代码库内,放到跨端契约目录会污染其他消费方。
游戏侧只需实现四个函数:
// game-host.d.ts · 第9类契约(精简示意)
interface GameModule {
init(ctx: PluginContext): void; // 初始化,接受受控引擎面
update(dt: number): void; // 每帧逻辑更新,dt=帧间隔秒
render(g: CanvasRenderingContext2D): void; // 渲染,g=引擎 mainContext
onInput(ev: InputEvent): void; // 输入事件
}
4.4 四条不可破约定
这四条约定是装载契约的护城墙,违反任何一条都会导致渲染错位、帧率失控或引擎版本耦合:
| 约定 | 内容 | 违例后果 |
|---|---|---|
| 掌帧唯一源 = 引擎 | 游戏不自起 RAF(RequestAnimationFrame,浏览器原生动画帧循环),update/render 由 engineInit 五回调驱动 |
双帧循环冲突,帧率翻倍或紊乱 |
绘制面唯一 = 引擎 mainContext |
所有美术与粒子渲到同一张 Canvas,setGLEnable(false) 纯 2D 模式 |
渲染层分裂,游戏画面与引擎效果错位 |
插件能力唯一面 = ctx.getEngine() |
引擎有的能力→薄包装提供;引擎缺的→插件补层,但都经同一接口 | 游戏代码直接依赖引擎内部,引擎升级即断 |
| 引擎 import 唯一活点 = 集成段 host | 游戏代码和插件代码零直接 import LittleJS(Q4 铁律,即第 4 季度确立的最终规则) | 多处 import 导致引擎多实例,状态不一致 |
4.5 装载时序
sequenceDiagram
participant Feed as 游戏信息流
participant Runner as Runner v2 (bootGameHost)
participant Host as 集成段 host
participant Engine as LittleJS engineInit
participant Game as 游戏模块 (game.js)
Feed->>Runner: 用户点击游戏卡片
Runner->>Runner: 解析 manifest JSON,取出 engineBundle
Runner->>Host: 挂载 window.__GameBundle
Host->>Engine: engineInit(五回调注册)
Engine-->>Host: 引擎主循环启动
Host->>Host: createHostDevContext → PluginContext 受控面
loop 每帧
Engine->>Game: gameUpdate 回调 → game.update(dt)
Engine->>Game: gameRender 回调 → game.render(mainContext)
Game->>Host: ctx.getEngine() 取能力(如需)
end
Game-->>Feed: phase==='gameover' 轮询 → 游戏结束事件
注意最后一步:游戏结束时,游戏模块通过将 phase 状态字段置为 'gameover' 来通知宿主,宿主轮询这个字段(game_end 靠 latch 终态非 emit)。游戏模块没有主动推送通道,不能也不应该直接 emit(发射)事件给宿主。这一设计经 commit 238ec2d(井字棋真玩入 feed)验证。
5. engineBundle 交付链与能力边界
5.1 包交付路径
AI 生成的游戏最终以 engineBundle 字段的形式随 manifest JSON 内嵌交付,而不是通过独立 URL 从 OSS(对象存储)加载。这个决策有两个好处:
- 省一条基建线:不需要部署和维护独立的 OSS 服务来托管游戏包。
- 整包单一校验面:manifest JSON 本身带 sha256 校验,游戏代码随包一起验证,不存在「manifest 版本与包版本不一致」的窗口。
装载时序:manifest JSON 下发 → 取出 engineBundle 字段 → window.__GameBundle 全局挂载 → bootGameHost({canvas, seed}) 装载启动。
packageUrl 外链字段(OSS 切换后用)和 immutable 强缓存头,是显式标注的 future-state 占位(未来可能启用的预留字段),当前未启用,不是孤儿设计,有明确的启用场景。
5.2 引擎能力边界三分
引擎能力分三类处理,边界由 engine-plugin-boundary-model(引擎插件边界模型)管理:
引擎原生有 → 薄包装(6 件):
| 能力 | LittleJS 提供 | 包装方式 |
|---|---|---|
| 粒子系统 | ParticleEmitter |
薄包装暴露给插件 |
| 音频合成核 | zzfxG / zzfxM |
薄包装暴露合成接口 |
| 数学工具 | lerp / smoothStep / easing(Ease) |
薄包装统一门面 |
引擎没有 → 自研补层(4 件):
| 能力 | 为何需要补层 |
|---|---|
| 碰撞检测(collision) | 引擎只返回 boolean(是否碰撞),补层返回 MTV Manifold(碰撞法向量)和 RayHit(射线命中点),给游戏更精细的物理信息 |
| 轻量物理(physics-lite) | 引擎是全刚体仿真(精确但重),补层提供半隐式欧拉抛体 / 弹簧 / 运动学约束(够用且轻量) |
| 后处理滤镜(palette-post) | 引擎后处理走 WebGL 语义,与纯 2D 模式不兼容;P5(第5优先级渲染层)红线规定只换色不给成品色板,补层提供 vignette / scanline / dither |
| 音频播放补层 | 引擎合成核不烤播放层增益,补层显式补 ×0.3 master 音量 |
铁律:A2(引擎接线阶段 2)禁留 sim(模拟器存根),A3 禁 vendored(供应商打包)转正——二者均有意替代,源码可证伪。自研只限「引擎之外的补层」,不替代引擎本体。
5.3 历史兼容
插件公开 API(如 EmitterConfig)一经冻结不变,老游戏照跑。包装层和补层是实现内部事,公开接口稳定性由下层契约保证。
6. Runner v2 三阶段收口记录
Runner(运行时,负责把游戏 bundle 装载进引擎并呈现给玩家)经过三个阶段(P1 → P2 → P3)完成实质收口,于 2026-06-15 T1b-β(T1b 第二个 beta 迭代,引擎接线与 Runner 第二轮验证阶段)阶段完成。
flowchart LR
P1["P1 · 装载契约 spike<br/>game-host.d.ts 建立<br/>实证 5 门"] --> P2["P2 · 通用宿主泛化<br/>bootGameHost 落地<br/>引擎游戏真渲入 feed<br/>真机 6 门"] --> P3["P3 · 派发面解封<br/>SUPPORTED_TEMPLATE_IDS<br/>接通生成主线"]
style P1 fill:#e8f4e8
style P2 fill:#e8f4e8
style P3 fill:#e8f4e8
P1 装载契约 spike:建立 game-host.d.ts(第 9 类契约),实证 5 个验收门,确认契约可行。spike(尖刺,指为验证可行性做的最小实现)阶段只验证接口,不做泛化。
P2 通用宿主泛化:bootGameHost 通用宿主落地,引擎游戏真实渲入信息流(不是「重构中」的占位,而是真渲染),真机验证 6 门。视觉终局:井字棋游戏空盘 → 落子 → 「X 胜!」全程在 feed 中真渲入。
P3 派发面解封:SUPPORTED_TEMPLATE_IDS = List.of("generic") 解封,接通 AI 生成主线(generic 是通用模板标识,意味着 Runner 不再只接特定 ID 的游戏,而是接受 AI 生成的任意游戏 bundle)。
Phase A 引擎真接线(与 Runner v2 并行):A0 引擎掌帧接管(门0 8/8)+ A1–A4 能力包装 + A6 真机门 6/6,动态 call-ID 取证验证真实调用链(particles.spawnEmitter / audio.synth.synthSfx / math.easing.quadIn 均验证为真调引擎叶方法)。
零灰度闭环缺口、零 split-brain(split-brain,即「脑裂」,指系统中两个部分对同一状态持有不一致的认知,是分布式系统和前后端协作的常见问题)——这是收口的核心验收标准。
7. 当前状态与待办
7.1 已完成(截至 2026-06-15 T1b-β)
- 引擎选型终裁落锤(LittleJS,55KB gz,85 分)
- Phase A 引擎真接线(A0–A6 全部通过真机门)
- Runner v2 全弧(P1 → P2 → P3 完成)
- 井字棋真玩入 feed 视觉终局验证(commit
238ec2d)
7.2 当前最高优先级(在飞)
W-G1 开闸验收门(Wave G1,即便宜模型游戏生成第一波验证):便宜模型(低成本 AI 模型)agent 按装载契约产出 bundle,九门 harness(九个验收门组成的自动化测试框架)兜底,目标是全 20 款游戏 bake-off(烤箱对比,逐款对比测试)+ 四模型横比 + Claude-free judge 门(使用非 Claude 模型作为评判者,避免自评偏差)。
7.3 Backlog(不在完成线关键路径)
以下属于技术债审计 / 样板拔高,不阻塞当前主线:
| 待办项 | 说明 |
|---|---|
render.js 切引擎 draw API |
现为 100% Canvas2D,引擎掌帧已达成「经引擎画游戏」的目标;切 API 是样板拔高,不是必须 |
| SIZES 单口径并表 | 现「插件增量主表 + 引擎产物附录」两口径并存,可合并 |
| 逐像素 WebGL readPixels 回归 | A0 有意降级(real 通道追帧 overshoot 不可控 → 逐像素由 stub 2D 通道独占兜),回归属优化 |
| `outcome:'win' | 'lose'` additive 字段 |
8. 关键指针
代码文件
| 文件 | 作用 |
|---|---|
game-runtime/src/core/game-host.d.ts |
第 9 类装载契约(上层,游戏-宿主接口) |
game-runtime/src/host/boot-game-host.js |
装载入口实现 |
game-runtime/src/host-dev/engine-caps.js |
引擎能力包装层(6 件薄包装 + 4 件补层) |
contracts/game-package.schema.json |
GamePackage 契约(含 engineBundle / immutable 字段) |
contracts/sdk-interface.d.ts |
SDK storage 根契约 |
关键 commit
| commit | 内容 |
|---|---|
204eaed |
装入引擎 |
11c8eaf + 7efe8a4 |
A0 引擎掌帧接管 |
e591e9b |
A1–A4 能力包装 |
3e5cab4 |
A6 真机 6 门 |
99b6c82 |
P1 装载契约 spike |
5d3f7a8 |
P2 通用宿主泛化 |
238ec2d |
井字棋真玩入 feed(视觉终局) |
延伸阅读
.agents/knowledge/tech-decisions.md §1.1:引擎选型权威蒸馏(已对齐现行真相).agents/skills/add-game-template.md:玩法模板 / 插件 onboarding,含能力插件库 v1 P1–P10 字节预算清单.agents/skills/game-e2e-cdp-harness.md §5/§6:引擎真接线门坑与 driver 六规则(CDP = Chrome DevTools Protocol,用于驱动浏览器的自动化协议).agents/skills/runtime-and-multichannel.md:打包 / 沙箱 / SDK / 多渠道导出手册docs/agent-specs/_archive/:本档取代的 4 份历史 spec(T1 引擎终裁包 / W-T1b-Runner 双层模板 / runner-v2-arc / T1b-β 收口报告)