Skip to content

AI 生成的游戏怎么保证能过小游戏这一关:预览里的微信严格模式沙箱

作者:Oleg Sidorkin,Cinevva CTO

玻璃罩里运行着一个低模 3D 游戏场景,罩外漂浮着几个被划掉的工具图标

我们刚上线了微信小游戏模式:一套生成规则,让 AI 写出来的游戏从第一行代码起就落在小游戏运行时的可移植子集内。不写 DOM UI、不用 Pointer Lock、音频只用 mp3、不碰动态代码。规则是有了,但做小游戏的同行都懂一个道理:约定靠不住,约束才靠得住。

麻烦在于,这类违规在浏览器里完全不可见。一个用 HTML 写血条的游戏在我们预览里跑得好好的,搬到小游戏运行时就是黑屏,因为那边压根没有 DOM。AI"大概率"遵守规则,剩下的小概率全在提审和真机上爆雷。所以我们把规则变成了物理:给预览加一个严格模式沙箱,让浏览器在微信做不到的地方直接说不。

核心取舍:模拟"没有",而不是仿真"有"

最直觉的方案是在浏览器里仿一套 wx.*:实现 wx.createCanvas、wx.onTouchStart、wx.setStorageSync,让游戏直接对着仿真层跑。我们走了反方向,原因在导出链路的形状上。

我们生成的游戏写的是标准浏览器 API,打包阶段再由适配层把浏览器 API 映射到 wx.,也就是社区里 weapp-adapter 那一套(官方最佳实践推荐的正是这个方向,Cocos 和 Laya 内部同理)。既然游戏永远不直接调 wx.,浏览器里仿一套 wx.* 测的就是不存在的代码路径。真正弄死移植的,不是浏览器里缺 wx.*,而是生成期悄悄用了微信没有的浏览器 API:一个 document.createElement('div'),一次 requestPointerLock(),一个 iOS 上解不出来的 .ogg。

所以沙箱模拟的是"没有":微信运行时里不存在的东西,在预览里也不存在,游戏从第一帧起就长在两个平台的交集里。

沙箱住在哪:一个本来就在的 Service Worker

我们编辑器的预览不走网络。一个 Service Worker 拦截 /game/* 请求,直接从 IndexedDB 出文件,这是预览秒级热更的底层。这个 SW 本来就往每个游戏页面里注入两个虚拟文件:一个错误与控制台捕获脚本,一个场景检查器。沙箱是第三个虚拟文件 _wechat-strict.js,注入位置在捕获脚本之后、游戏代码之前。顺序两头都压着担子:在捕获之后,它的 console.error 才会被转发给编辑器;在游戏模块之前,毒化才来得及生效。

激活是一行代码的事。创作器里的微信模式开关存在 localStorage,预览 iframe 和编辑器同源,沙箱直接读:

js
try {
  if (localStorage.getItem('cinevva-gc-profile') !== 'wechat-minigame') return;
} catch (e) { return; }

非微信模式的游戏只付一次字符串比较的成本。

毒化清单:抛错和标记两档

不是所有违规都该同一种死法,所以分两档。微信运行时里不存在的 API 直接抛错,因为导出之后它就是会崩,生成期崩一次是提审期崩溃的诚实预演。存在但日后会出问题的行为只大声标记不改变行为,不然预览里的游戏状态就失真了。

抛错档:创建任何 HTML UI 元素(createElement 和 createElementNS 两条路都拦,因为 Three.js 建 canvas 走的是 NS 那条)、eval、new Function、requestPointerLock、注册 Service Worker。每条报错都带着修法:

js
throw violate("document.createElement('div') — WeChat mini games have no DOM. " +
  "Draw UI on canvas (offscreen 2D canvas -> THREE.CanvasTexture on a screen-space quad) " +
  "and hit-test taps yourself.");

标记档:读 indexedDB(返回 undefined,跟微信行为一致,同时报错指向 localStorage)、任何 iOS 解不动的音频格式(Audio 构造器、media 元素的 src setter、fetch 的 URL 三处都查)、请求没备案没白名单的外部域名、以及 Tone.js 出现在页面里。页面加载后还有一次 body 审计,把静态 HTML 写进去的 UI 元素点名列出来,因为静态标签绕得过 createElement 的闸门。

所有报告按消息去重、每局封顶五十条。一个每帧建一个 div 的死循环产出一条报错,不是一场把信号淹掉的刷屏。

四个例外,每个都是被自己的工具教育出来的

做沙箱最花心思的是决定不毒什么,四个例外全是开发过程中我们自己的工具先摔了跟头。

我们的调试器跑在 eval 上。 创作器的 execute_js 工具(AI 用它探查运行中的游戏)底层就是 eval。直接毒掉等于把 AI 自己的眼睛蒙上。解法是毒化之前先把真 eval 藏进一个不可枚举属性,捕获脚本回退用它:

js
Object.defineProperty(window, '__cinevvaRealEval', { value: window.eval, enumerable: false });
window.eval = function () { throw violate('eval() is banned in WeChat mini games.'); };
// 捕获脚本在消息处理时:
returnValue = (window.__cinevvaRealEval || eval)(e.data.code);

游戏代码碰 eval 照死,调试器照活。

录屏工具也要建元素。 我们的 reel 录制器是用 iframe 自己的 createElement('script') 注入的,导出视频靠一个合成的 a 标签点击。所以 script 放行,a 只警告不抛。游戏真写了个链接照样被点名,工具照常干活。

键盘事件留着。 AI 验证操作手感的工具会合成按键、回读游戏状态的位移变化,这是抓"反向摄像机"这类 bug 的关键闭环。触屏优先靠生成规则和操控方案保证,不靠沙箱掐键盘。

WebGL2 留着,这一条牵出的账值得单独算。

WebGL2 和高性能+模式的账

草稿版毒化清单封了 getContext('webgl2'),想强制"WebGL1 安全"。结果 Three.js 在 r163 就删光了 WebGL1 支持,我们锁的是 r181:封 WebGL2 等于把这个模式下所有 3D 游戏一起拉黑屏。规则本身写错了,沙箱审计出了沙箱自己的规格书。

顺着这条线把平台的渲染地板查清楚了。安卓侧,小游戏运行时的 WebGL2 基本随处可用。iOS 才是重点:按官方 WebGL2 支持说明高性能+模式文档,iPhone 上真正可用的 WebGL2 只在高性能+运行时里,要求微信客户端 8.0.45 起(iOS 14 上要求更高)、系统差不多 iOS 15.5 起。普通高性能模式跑 WebGL2 官方自己说"问题较多",普通模式干脆没有。最阴的是失败形态:不支持的环境里 getContext('webgl2') 可能返回一个真值但坏掉的上下文而不是 null,社区早就在催官方改。信了返回值的游戏不会干净地失败,只会渲染出垃圾或者一块黑。

我们的答案分三层。沙箱不碰 WebGL2 的上下文创建,跟自家引擎打架没有意义。生成规则里现在强制每个游戏带启动检测:渲染器创建包在 try/catch 里,再做一次功能探测(typeof gl.createVertexArray === 'function',WebGL2 独有能力,假上下文装不出来),失败就用 2D canvas 画一句"请更新微信"的友好提示,绝不留黑屏。这也是 Unity 转换的小游戏在线上普遍的做法。导出器落地时会在 game.json 里锁高性能+模式,让能跑的路径成为默认路径。

代价呢?iOS 15.5 以下或微信客户端太老的用户,2026 年占比是低个位数,但集中在旧机型上,下沉盘子重的品类要自己掂量。真有数据说这条尾巴值得追,我们手里留着一张便宜的牌:把微信模式的 three 锁回 r162,最后一个支持 WebGL1 的版本。这个模式下的游戏只碰稳定核心 API,降级就是 import map 改一行。牌先攥着,数据说话再打。

和 AI 的闭环

这套东西对 AI 优先的产品才真正值回票价。每次构建完,创作器的 AI 都会调一个读游戏控制台的工具,等启动完成才返回。捕获脚本把 console.error 全量转发进这条流。所以一条 [wechat-strict] 违规不是人类可能划走的警告,它落在模型每次收工前必查的那条通道里,同一轮就能修,报错文本里就写着怎么修。

生成规则再补一句话堵住最后的缝:所有 [wechat-strict] 消息按构建失败处理,修根因,永远不要试图检测或绕过沙箱。只有关掉沙箱才能跑的游戏,就是导出后必死的游戏。

效果上,沙箱把一个模糊的合规问题("模型这十几条规则都守了吗")换成了这个系统本来就擅长的调试问题("控制台有报错,修")。

浏览器装不出来的东西

说实话的部分。这个沙箱抓的是 API 面上的违规,按我们的判断这占了移植死因的大头。它装不出来的:真机性能、iOS 上没有 JIT 的 JavaScriptCore、微信 WebGL 实现的各种脾气、解码器和文件扩展名之间的差异。这些只有真运行时给得了。

所以沙箱是三环里的第一环。第二环,等导出器能吐出微信开发者工具工程之后,用开发者工具的 CLI 和 miniprogram-automator 无头跑官方模拟器:启动不报错、首帧渲染、点一下有反应。第三环是官方的设备通道,预览二维码和真机调试,只有那里能看到别处看不到的真相。一环比一环慢、一环比一环真,每一环的任务是让下一环少跑几趟。

想试的话,游戏创作器里打开"微信小游戏模式"就行。市场和资质那笔账在这篇里算过了。

参考