小游戏平台
微信小游戏在 Estella 里不是特例,它只是平台家族的一个 profile。各家小游戏宿主
(微信的 wx、抖音的 tt 等)暴露的 API 高度相似:没有 DOM、打包式文件系统、分包、
触摸输入、键值存储。Estella 把这套模型只实现一次,每个平台厂商用数据来描述。
本指南面向「接入 Estella 未内置的平台」。如果你的目标是微信,请看 微信小游戏。
接入一个平台的成本
Section titled “接入一个平台的成本”一个 profile 就是三个事实,外加至多一个方法:
import { installMiniGamePlatform } from 'esengine/minigame';
declare const myHost: any; // 宿主全局对象,例如 `tt`
installMiniGamePlatform({ id: 'myvendor', hostLabel: 'MyVendor', get global() { return myHost; },});这就是一个完整的平台。文件系统、fetch、canvas、图片解码、触摸与按键输入、存储、
分包、内存告警、设备像素比、语言,全部来自归一化后的宿主全局对象。音频
(createInnerAudioContext)和 socket(connectSocket)也一样。视频走引擎自带的
wasm MPEG-1 解码器,对任何宿主都可移植。
global 要像上面那样用 getter 读取:宿主全局在运行时才存在,用 getter 才能保证在打包
工具或测试里 import 你的 profile 时不会碰到未定义的绑定。
唯一真正的分歧
Section titled “唯一真正的分歧”WASM 实例化。微信走 WXWebAssembly.instantiate(path) 而不是标准 WebAssembly,
所以它的 profile 覆盖了这一个方法:
{ id: 'wechat', hostLabel: 'WeChat', get global() { return wx as unknown as MiniGameGlobal; }, instantiateWasm(pathOrBuffer, imports) { /* WXWebAssembly */ },}如果你的宿主用标准 WebAssembly,就不用写它——家族会通过打包文件系统读出二进制并
实例化。
createAudioBackend、createVideoBackend、createSocket 同样是可选的。只在宿主
确实不同的地方覆盖;默认实现是 MiniGameAudioBackend、wasm 视频解码器和
MiniGameSocket,它们都从 esengine/minigame 导出,你可以包装而不必重写。
替换单个能力
Section titled “替换单个能力”这几个可选方法不只用于「宿主有差异」——它们就是替换你不满意的实现的接缝。 比如自己写一套视频软解码,只需要一个方法:
import type { MiniGameProfile, PlatformVideoBackend, VideoBackendContext } from 'esengine/minigame';
class SoftwareVideoBackend implements PlatformVideoBackend { readonly name = 'my-software-decoder'; constructor(private ctx: VideoBackendContext) {} createStream(url: string, options) { /* 你的解码 → VideoStreamHandle */ } dispose() {}}
export default { id: 'myvendor', hostLabel: 'MyVendor', get global() { return myHost; }, createVideoBackend: (ctx) => new SoftwareVideoBackend(ctx),} satisfies MiniGameProfile;VideoPlugin 在 build 时向平台要一次后端,你返回什么,游戏里所有 VideoPlayer
就驱动什么。要满足的契约是 PlatformVideoBackend:createStream(url, options)
返回一个 VideoStreamHandle,它的 pump(module) 每帧把当前帧上传到
textureHandle。音频(PlatformAudioBackend)和 socket(PlatformSocket)同理。
想保留某个宿主的大部分、只换其中一件,把它的 profile 展开:
import { wechatProfile } from 'esengine/wechat';
export default { ...wechatProfile, createVideoBackend: (ctx) => new SoftwareVideoBackend(ctx) };宿主需要提供什么
Section titled “宿主需要提供什么”你的 global 需要满足 MiniGameGlobal。必需成员:
| 成员 | 用途 |
|---|---|
createCanvas() |
上屏画布(第一次调用)+ 离屏 2D |
createImage() |
图片解码 |
getFileSystemManager() |
readFileSync / readFile / access / accessSync / writeFile |
request(opts) |
fetch polyfill |
createInnerAudioContext() |
音频播放 |
connectSocket(opts) |
GameSocket / 状态同步 |
getSystemInfoSync() |
视口尺寸、像素比、语言 |
onTouchStart/Move/End + off* |
指针输入 |
getStorageSync / setStorageSync / removeStorageSync / getStorageInfoSync |
存档、热更新记账 |
可选成员——适配器会探测,宿主缺失时优雅降级:onTouchCancel/offTouchCancel、
onKeyDown/onKeyUp(及 off*)、loadSubpackage、
onMemoryWarning/offMemoryWarning、onShow/onHide。
发布包的入口调用家族 runtime:
// game.js —— 宿主执行这个文件const engineFactory = require('./wasm/esengine.js');const bundle = require('./game-bundle.js');bundle.boot(engineFactory, { /* side module 工厂 */ });// 与游戏一起打包import { installMiniGamePlatform, initMiniGameRuntime } from 'esengine/minigame';import { myProfile } from './myProfile';
export function boot(engineFactory, sideModuleFactories) { installMiniGamePlatform(myProfile); return initMiniGameRuntime({ engineFactory, engineWasmPath: 'wasm/esengine.wasm', sceneNames: ['Main'], firstScene: 'Main', sideModuleFactories, });}initMiniGameRuntime 从适配器取上屏画布,把它的 GL 上下文注册到引擎模块,加载可寻址
manifest,然后跑起 app——和微信走的是同一条序列。initWeChatRuntime 就是这个函数
填上微信构建产物的 wasm 文件名,没有别的。
在打包项目对话框里,把鼠标移到平台列表的「项目自定义」分组上,点 +。 填一个标识和名称,编辑器就会把平台的两半都写好——并且已经连接好——然后选中这个 新平台;只要你描述完自己的宿主,就能直接打包。
它写出来的就是普通的项目文件,不会在别处注册什么。下面是打包那一半:
export default { id: 'acme-play', label: 'ACME Play', blurb: 'ACME 小游戏包。', defaultOut: 'dist-acme',
emitConfigFiles(ctx) { return [{ file: 'game.json', content: JSON.stringify({ orientation: ctx.orientation, appName: ctx.title, ...(ctx.subPackages.length > 0 ? { subPackages: ctx.subPackages } : {}), }, null, 2) + '\n', }]; },};整个文件就这些。三个事实加一个 emitter——其余全部落到「标准小游戏宿主」的默认值:
平台中立的 SDK 入口(index.minigame.js)、initMiniGameRuntime、标准
WebAssembly 引擎 glue,以及通用的 CommonJS game.js。剩下的一切——cook、
manifest、场景变换、side-module 扫描、打包、运行时拷贝——与微信走的是同一条管线。
一个平台有两半
Section titled “一个平台有两半”上面这个文件是打包那一半(点 + 会帮你生成)。运行时那一半就是本指南开头的 MiniGameProfile
——宿主全局对象,以及你替换掉的任何能力。用 runtimeProfile 指过去,生成的入口
就会在启动前把它装上:
export default { id: 'acme-play', label: 'ACME Play', runtimeProfile: 'src/acme-runtime.ts', // ← 另一半 emitConfigFiles(ctx) { /* … */ },};// src/acme-runtime.ts —— 默认导出运行时 MiniGameProfileexport default { id: 'acme-play', hostLabel: 'ACME Play', get global() { return acme; }, createVideoBackend: (ctx) => new SoftwareVideoBackend(ctx),};这个连接很关键:esengine/minigame 在游戏指明宿主之前不装任何平台。没有它,
包能正常构建,然后在设备上抛错。只有当你的游戏自己调用 installMiniGamePlatform
时才可以省略 runtimeProfile;编辑器会检查这个路径是否存在,不存在就不会把该平台
标为就绪。
id 只能是小写字母/数字/短横线,且不能与内置平台重名。它同时是 cook 的
导入设置键,所以逐纹理的压缩覆盖可以按名字指定你的平台。
完整的 profile 字段
Section titled “完整的 profile 字段”可覆盖的全部字段,以及管线拿它做什么:
| 字段 | 含义 |
|---|---|
id |
平台身份——同时是 cook 逐纹理读取的导入设置键 |
sdkEntryFile |
esengine 被 alias 到的 SDK dist 入口 |
runtimeInit |
生成的入口所 import 的启动函数 |
engineGlueCandidates |
按优先级查找的引擎 glue 文件名 |
esTarget |
游戏包与降级后 glue 的语法下限 |
wasmBuildHint / sideModuleBuildTargets |
错误提示里的 build -t … 名字 |
nativeSuffixes / binRestageExts |
打包器的后缀策略 |
subpackageDir |
懒加载分组落盘的位置 |
emitConfigFiles / emitEntry |
该平台的配置文件与 game.js |
wasmDir |
相对项目的引擎运行时目录(默认用编辑器自带的 web 运行时) |
runtimeProfile |
相对项目的模块路径,默认导出运行时 MiniGameProfile |
宿主需要单独的引擎构建吗?
Section titled “宿主需要单独的引擎构建吗?”只有当它加载不了 web 引擎的 wasm 时才需要。微信需要(-t wechat,ES_BUILD_WXGAME),
因为它有 WXWebAssembly 且没有 WebGPU。一个支持标准 WebAssembly 和 WebGL2 的宿主
可以直接用 web 构建的 glue——engineGlueCandidates 里已经有回退到 esengine.js 的
分支,正是为这种情况准备的。