Mini-Game Platforms
WeChat MiniGame is not a special case in Estella — it is one profile of a
platform family. Mini-game hosts (WeChat’s wx, Douyin’s tt, and the rest)
expose a near-identical API: no DOM, a packaged filesystem, subpackages, touch
input, key-value storage. Estella implements that model once and describes
each vendor as data.
This guide is for bringing up a vendor Estella does not ship. If you are shipping to WeChat, you want WeChat MiniGame instead.
What a vendor costs
Section titled “What a vendor costs”A profile is three facts and, at most, one method:
import { installMiniGamePlatform } from 'esengine/minigame';
declare const myHost: any; // the host global, e.g. `tt`
installMiniGamePlatform({ id: 'myvendor', hostLabel: 'MyVendor', get global() { return myHost; },});That is a complete platform. Filesystem, fetch, canvas, image decode, touch and
key input, storage, subpackages, memory-pressure signals, device pixel ratio and
language all come from the normalized host global. So do audio
(createInnerAudioContext) and sockets (connectSocket). Video uses the
engine’s own wasm MPEG-1 decoder, which is portable to every host.
Read global through a getter, as above: the host global is ambient at runtime,
and a getter means importing your profile in a bundler or a test never touches an
undefined binding.
The one genuine divergence
Section titled “The one genuine divergence”WASM instantiation. WeChat routes it through WXWebAssembly.instantiate(path)
rather than the standard WebAssembly, so its profile overrides one method:
{ id: 'wechat', hostLabel: 'WeChat', get global() { return wx as unknown as MiniGameGlobal; }, instantiateWasm(pathOrBuffer, imports) { /* WXWebAssembly */ },}If your host uses standard WebAssembly, omit it — the family reads the binary
through the packaged filesystem and instantiates it.
createAudioBackend, createVideoBackend and createSocket are optional the
same way. Override one only where the host genuinely differs; the defaults are
MiniGameAudioBackend, the wasm video decoder, and MiniGameSocket, all
exported from esengine/minigame so you can wrap rather than rewrite.
Replacing one capability
Section titled “Replacing one capability”The optional methods are not only for host differences — they are the seam for replacing an implementation you don’t want. Writing your own video decoding, for example, is one method:
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) { /* your decode → VideoStreamHandle */ } dispose() {}}
export default { id: 'myvendor', hostLabel: 'MyVendor', get global() { return myHost; }, createVideoBackend: (ctx) => new SoftwareVideoBackend(ctx),} satisfies MiniGameProfile;VideoPlugin asks the platform for a backend once at build; whatever you return
is what every VideoPlayer in the game drives. The contract to satisfy is
PlatformVideoBackend — createStream(url, options) returning a
VideoStreamHandle whose pump(module) uploads the current frame to
textureHandle once per frame. Audio (PlatformAudioBackend) and sockets
(PlatformSocket) work the same way.
To keep most of a host and change one thing, spread its profile:
import { wechatProfile } from 'esengine/wechat';
export default { ...wechatProfile, createVideoBackend: (ctx) => new SoftwareVideoBackend(ctx) };What the host must provide
Section titled “What the host must provide”Your global has to satisfy MiniGameGlobal. Required:
| Member | Used for |
|---|---|
createCanvas() |
display surface (first call) + offscreen 2D |
createImage() |
image decode |
getFileSystemManager() |
readFileSync / readFile / access / accessSync / writeFile |
request(opts) |
the fetch polyfill |
createInnerAudioContext() |
audio playback |
connectSocket(opts) |
GameSocket / replication |
getSystemInfoSync() |
viewport size, pixel ratio, language |
onTouchStart/Move/End + off* |
pointer input |
getStorageSync / setStorageSync / removeStorageSync / getStorageInfoSync |
save data, hot-update bookkeeping |
Optional — the adapter probes for these and degrades gracefully when a host
lacks one: onTouchCancel/offTouchCancel, onKeyDown/onKeyUp (+ off*),
loadSubpackage, onMemoryWarning/offMemoryWarning, onShow/onHide.
Booting the game
Section titled “Booting the game”The shipped package’s entry calls the family runtime:
// game.js — the host runs thisconst engineFactory = require('./wasm/esengine.js');const bundle = require('./game-bundle.js');bundle.boot(engineFactory, { /* side module factories */ });// bundled with your gameimport { 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 takes the display canvas from the adapter, registers its
GL context with the engine module, loads the addressable manifest, and runs the
app — the same sequence WeChat boots through. initWeChatRuntime is this
function with WeChat’s staged wasm name filled in, and nothing else.
Packaging
Section titled “Packaging”In Package Project, hover the Project group in the platform list and hit +. Give it an id and a name, and the editor writes both halves of the vendor — already joined — then selects the new platform so you can package it as soon as you have described your host.
The files it writes are ordinary project files; nothing is registered anywhere else. This is the packaging half:
export default { id: 'acme-play', label: 'ACME Play', blurb: 'ACME mini-game package.', 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', }]; },};That is the whole file. Three facts and one emitter — the rest defaults to a
standard mini-game host: the vendor-neutral SDK entry (index.minigame.js),
initMiniGameRuntime, the standard WebAssembly engine glue, and the generic
CommonJS game.js. Everything else — cook, manifest, scene transform,
side-module scan, bundling, runtime copy — is the same pipeline WeChat uses.
A vendor has two halves
Section titled “A vendor has two halves”The file above is the packaging half (scaffolded for you by +). The runtime half is the
MiniGameProfile from the first part of this guide — the host global, and any
capability you replace. Point at it with runtimeProfile and the generated entry
installs it before booting:
export default { id: 'acme-play', label: 'ACME Play', runtimeProfile: 'src/acme-runtime.ts', // ← the other half emitConfigFiles(ctx) { /* … */ },};// src/acme-runtime.ts — default-exports the runtime MiniGameProfileexport default { id: 'acme-play', hostLabel: 'ACME Play', get global() { return acme; }, createVideoBackend: (ctx) => new SoftwareVideoBackend(ctx),};This link matters because esengine/minigame installs no platform until the game
names a host. Without it the package builds cleanly and then throws on the
device. Omit runtimeProfile only if your game calls installMiniGamePlatform
itself; the editor checks the path exists and refuses to call the platform ready
otherwise.
The id must be lowercase letters/digits/dashes and must not shadow a built-in.
It is also the cook’s Import Settings key, so per-texture compression
overrides can target your platform by name.
The full profile
Section titled “The full profile”Everything you can override, and what the pipeline uses it for:
| Field | What it says |
|---|---|
id |
vendor identity — also the Import Settings key the cook reads per texture |
sdkEntryFile |
SDK dist entry esengine is aliased to |
runtimeInit |
the boot function the generated entry imports |
engineGlueCandidates |
engine glue filenames to look for, in preference order |
esTarget |
syntax floor for the bundle and the down-leveled glue |
wasmBuildHint / sideModuleBuildTargets |
the build -t … names in error guidance |
nativeSuffixes / binRestageExts |
the packer’s suffix policy |
subpackageDir |
where lazy groups stage |
emitConfigFiles / emitEntry |
the vendor’s config files and game.js |
wasmDir |
project-relative engine runtime dir (default: the editor’s web runtime) |
runtimeProfile |
project-relative module default-exporting the runtime MiniGameProfile |
Does the host need its own engine build?
Section titled “Does the host need its own engine build?”Only if it cannot load the web engine’s wasm. WeChat needs one (-t wechat,
ES_BUILD_WXGAME) because of WXWebAssembly and because it has no WebGPU. A
host with standard WebAssembly and WebGL2 can ship the web build’s glue —
engineGlueCandidates already falls back to esengine.js for exactly this case.