构建与导出
一个 Estella 项目可发布到六个目标——Web、桌面、微信小游戏、单文件 Playable 广告, 以及原生 Android / iOS 应用——全部从编辑器完成。导出用的是你在编辑器里运行的同一套运行时, 所以你发布的就是你测过的(play == ship)。
从编辑器导出
Section titled “从编辑器导出”
打包项目对话框:从分组的列表里选一个目标、一个配置、输出目录,以及 cook 选项——然后 Package。
- 打开 文件 → Build… 调出 打包项目(Package Project) 对话框。
- 从列表里选一个目标——分为通用(Web / 桌面 / Playable)、小游戏 (微信,以及项目自己添加的)、移动(Android / iOS)。发往不止一个地方的目标会把这些 去处列在它下面:桌面分叉为独立发行与 Steam。
- 填这个目标自己的设置,就在它的页面上——应用叫什么、用什么 id 认它, 以及它要发往的那个商店需要的东西。
- 选一个构建配置——Development 或 Shipping(Shipping 会压缩)——一个输出目录、 source map,以及资产烘焙选项。
- 检查**构建场景(Scenes in build)**列表——取消勾选不想发布的场景,需要的话换一个 启动场景(见下文)。
- 点 Package,看实时构建日志。完成后输出目录会自动打开(有复选框可关掉), Web / Playable 构建还会提供 Preview over http。
你的选择会存进 project.esproject,所以对话框下次会记住它们。
项目场景目录下的每个场景都随导出发布,并成为 SceneManager.switchTo 的目标——游戏脚本
如何切换见场景。对话框的构建场景列表控制这个集合:
- 启动场景置顶并带播放徽标。它总是发布——点其他场景的播放按钮可把启动权移过去, 或在内容浏览器里右键任意场景选设为启动场景(Set as Startup Scene)。
- 取消勾选一个场景把它排除出构建(存为
project.esproject里的packaging.excludeScenes)。被排除的场景在编辑器里仍可正常编辑和运行;只被排除场景 引用的资产不会烘焙进构建。 - Playable 只带启动场景——它是一个有体积上限的单文件。
Web 或 Playable 构建完成后,点 Preview over http ——编辑器把输出目录架在
本机回环服务器上(http://127.0.0.1:<端口>/)并用浏览器打开。
| 目标 | 产出什么 | 输出 | 下一步 |
|---|---|---|---|
| Web | 一个静态、自包含的 Web 构建。 | dist-web/ |
Preview over http,或把文件夹上传到任意静态托管。 |
| 桌面 | 每装了一个桌面运行时模板,就出一个可直接运行的原生应用——macOS 上是 .app,Windows 与 Linux 上是 <Name>/ 目录(其中分别是 <Name>.exe 和 <Name>)。没有 Chromium,也没有 npm 工程。 |
dist-desktop/ |
双击即可。选了 Steam 渠道还会写出 depot 脚本和一份带本次构建确切取值的 STEAM.md——见 Steam。 |
| 微信 | 一个微信小游戏包。 | dist-wechat/ |
在微信开发者工具里打开该文件夹并设置你的 AppID。见 微信小游戏。 |
| Playable | 一个自包含的 index.html,一切内联——无外部请求(广告平台的要求)。 |
dist-playable/ |
Preview over http(它的真实宿主是广告平台的 iframe)。先选好广告平台,CTA 与体积检查才对得上投放目标。 |
| Android | 原生 arm64 应用(Vulkan)的内容——真 APK,不是 WebView。 | dist-android/ |
node build-tools/cli.js native --package --content dist-android 把它包成已签名的 APK。见 Android 与 iOS。 |
| iOS | 原生 arm64 应用(Metal)的内容;若本机已为 iOS 构建过引擎,还会附带 Xcode 工程。 | dist-ios/ |
打开生成的 Xcode 工程,选签名 Team,Run。见 Android 与 iOS。 |
桌面、Android 与 iOS 导出的是应用内容而非可直接运行的载荷:引擎、SDK 和游戏运行时都住在 应用二进制里。三者都由编辑器安装的运行时模板组装而成,所以打包本身不需要任何工具链——见下文的 前置条件。只有 iOS 仍然需要 Xcode,因为只有 Apple 能为真机签名。
试玩广告平台
Section titled “试玩广告平台”各家平台收的都是同一个单文件 HTML,但在三件事上不一致:体积上限、<head> 里必须放什么、
以及用哪个函数把玩家送去商店。在打包项目对话框里选平台(就在 Playable 目标自己的设置里),
导出会把这三件事都处理掉。同一个游戏投多家是常事,所以这是每次打包时的选择而不是项目属性;
每个平台还各有自己的输出目录(dist-playable/<平台>),打一家不会覆盖上一家。
游戏代码里不出现任何平台名字,只调一次 CTA:
import { playableCta } from 'esengine';
// 结束卡的按钮、通关之后——凡是"号召点击"的地方playableCta();导出会注入 playableCta() 要分发到的桥接层。未选平台时它是空操作,所以同一个场景在编辑器里
和网页上照样能跑。
| 平台 | 上限 | 点击跳转 | 说明 |
|---|---|---|---|
| 通用 | 2MB | — | 不接任何平台接口。上限取我们已知里最严的一档——毕竟未指名的目标可能是其中任何一家。 |
| Meta(Facebook / Instagram) | index.html 2MB |
FbPlayableAd.onCTAClick() |
Meta 的 5MB 是 ZIP 包的总量;单文件试玩受 index.html 那条约束。禁止任何 HTTP 请求。 |
| Google Ads(应用广告系列) | 5MB | ExitApi.exit() |
它的退出接口是 Google 托管的脚本,官方要求以字面 <script> 写在 <head> 里——导出会帮你写好。Google 上传 ZIP,所以要先把 index.html 压缩。 |
| MRAID 平台(通用) | 5MB | mraid.open() |
适用于任何 MRAID 宿主。mraid 由宿主 webview 注入,<head> 无需任何内容。 |
| Unity Ads | 5MB | mraid.open() |
要求试玩包等到 MRAID 的 viewableChange 之后再开始。 |
| AppLovin | 5MB | mraid.open() |
要求所有资源内嵌——单文件试玩本来就是。 |
上限会变,所以每条警告都指名它用的是哪一家的上限、以及这个数字出自哪里,而不是丢一个 让你盲信的数字。
我们没内置的平台
Section titled “我们没内置的平台”自己定义就行——内置的那几家写的就是这份契约,没有做不到的事。打包项目 → 新建平台 可以
生成脚手架(类型选试玩广告平台),也可以手写 .esengine/platforms/<id>.mjs:
export default { id: 'acme-ads', kind: 'playable', label: 'Acme Ads',
// 这家接受的体积,以及这个数字出自哪里——导出的警告会引用这条说明。 maxBytes: 5 * 1024 * 1024, limitNote: 'Acme 文档,创意规格 § playable',
// 可选:注入 <head>(方向 meta、平台自己的 SDK 脚本)。 emitHead: (ctx) => `<meta name="ad.orientation" content="${ctx.orientation}">`,
// 注入 playableCta() 要分发到的桥接层,在游戏代码之前执行。 emitBridge: () => `window.__ESTELLA_PLAYABLE__={cta:function(){AcmeSDK.openStore();}};`,};它随后就会和内置平台并列出现在对话框的“广告平台”下拉里,并走同一条导出管线。
运行时前置条件
Section titled “运行时前置条件”Web、桌面与 Playable 都复用随编辑器附带的引擎运行时,所以导出无需准备——试玩包内联的 就是同一份 web 运行时(glue 作为 blob 模块、wasm 以 base64 传入),不需要单独构建。在这些基于 网页的目标里 微信是例外:它的 WebAssembly 加载器不同,运行时需先构建一次:
# 仅在首次微信导出前node build-tools/cli.js build -t wechat当某个目标的运行时缺失时,打包项目对话框会提醒你,而微信导出会直接失败并给出确切的
构建命令——而不是产出一个运行时才崩的包。微信还需要为游戏用到的每个引擎特性构建对应的
侧模块(physics-wechat、spine-wechat,压缩纹理用 basis-wechat)——见
微信小游戏。
对话框区分两类缺失的前置条件,因为它们挡住的程度并不一样:
- 缺引擎运行时(Web / Playable / 微信)意味着根本产不出包。
- 缺原生工具链(Android / iOS)则不然。导出照样把应用内容写出来;只有最后组装成 可安装应用那一步需要工具链,而那一步可以在另一台机器上跑——编辑器在 Windows 上时, iOS 构建本来就得如此。
目标渲染不了的内容
Section titled “目标渲染不了的内容”一个目标未必编译了编辑器能创作的每个子系统。当它确实缺某个子系统时,导出会点名这个缺口 以及踩到它的场景,而不是写出一个悄悄少了半个场景的包——警告会列出子系统(瓦片地图、粒子、 后处理、物理、Spine、视频、文本)和创作它的文件。
目前没有任何目标存在缺口:原生应用编译整份引擎源码清单,网页端作为独立侧模块的那三个子系统 在这里是编进二进制的。这项检查保留着,因为情况可能变。
可选的引擎特性以独立的 WebAssembly 侧模块发布——每个模块是一对 emscripten
产物:<file>.js 胶水加 <file>.wasm——按需加载,让基础引擎保持小体积:
| 模块 | 产物 | 何时加载 |
|---|---|---|
| 物理(Box2D) | physics.js / physics.wasm |
项目声明了物理,或场景使用了物理组件。 |
| Spine | spine21 / spine38 / spine41 / spine42 / spine43 的 .js/.wasm |
场景使用了对应格式版本的 Spine 骨骼。 |
| DragonBones | dragonbones.js / dragonbones.wasm |
场景使用了 DragonBones 骨架。一个模块读所有版本。 |
| Basis 转码器 | basis.js / basis.wasm |
构建里发布了 KTX2 压缩纹理。 |
| 视频解码器 | videodec.js / videodec.wasm |
wasm 视频路径解码片段时。 |
- Web / 桌面 —— 产物作为零散文件放在
esengine.wasm旁;运行时经 fetch 侧模块 host 从wasmBaseUrl(引擎自身加载自的目录)拉取。 - Playable —— 不允许任何外部请求,导出器把模块以 base64 内联进单个 HTML
文件;启动代码(
initPlayableRuntime)拿到的是内嵌侧模块 host 而非 URL。 - 微信 —— 侧模块打包为微信预编译的模块工厂(这也是每个特性都需要上文
*-wechat运行时构建的原因)。
编辑器自己的播放模式也经由同一个发行运行时启动(initPlayRealmRuntime),从
编辑器 origin 拉取侧模块——所以“玩到的即发布的”同样覆盖这些模块。运行时 API 见
应用设置与生命周期。
构建配置与 source map
Section titled “构建配置与 source map”- Development —— 不压缩的构建;配上 source map 得到可调试的输出。
- Shipping —— 为体积和速度压缩。这是你发布用的。
- source map 把运行的 bundle 映射回你的 TypeScript。Web 和桌面可用;发布构建请关掉。
资产烘焙选项
Section titled “资产烘焙选项”烘焙如何处理某一个资产,是那个资产自己的事——在检视面板的逐资产导入设置里, 每个平台一个页签,所以一张纹理可以为手机压得比网页端更狠。构建这一层只决定是否遵循这些设置:
- 资源压缩 → 按导入设置 —— 每张纹理与每段音频按各自的导入设置压缩
(PNG → KTX2 Basis Universal、Max Size、WAV → MP3),同属一张图集的纹理打包成图集页,
于是许多小精灵作为一张纹理发布、在一次绘制里合批。图集成员来自
<name>.atlas/文件夹, 或.esengine/asset-groups.json里的atlases条目。 - 资源压缩 → 全部跳过 —— 一律原样输出,便于快速迭代。
KTX2 纹理由运行时转码为设备支持的最佳格式(ASTC → ETC2 → S3TC,最后回退 RGBA8),并带上 mip 链——下载更小、显存更省。
内容寻址 对 Web/桌面默认开启(每个文件按字节哈希命名——URL 不可变、自动去重)。 高级里放着热更新的 CDN 根地址、source map, 以及构建结束后是否打开输出目录。
某一个目标叫什么、发往哪里、商店用什么认它,在 文件 → Build… 里那个目标自己的 页面上编辑——就在它所改变的那次构建旁边:
- 微信 —— AppID。
- 桌面 —— 应用 id 覆盖、产品名(用于命名应用),以及发行渠道 (独立发行或 Steam)。
- Android —— 版本号(Google Play 用来排序构建的整数)、构建产出安装包还是 Gradle 工程, 以及是否在 APK 之外再写一份 App Bundle。
无论发往哪里都成立的那些,留在 项目设置 → 打包(Packaging),并随项目保存:
- 应用 ID —— 已安装应用的反向域名 id:Android 的 manifest package、iOS 的 bundle id、 桌面安装包的 id。留空则按项目名推导;正式发布的应用应当自己指定,因为应用商店会永久沿用它。
- 应用图标 —— 一张方形 PNG,供所有可安装目标使用。
- 成就 —— 你的游戏会解锁的 id。平台中立:每个商店都有这份同样的清单,只是名字不同。
方向是一个项目级设置,而非逐平台:项目设置 → 显示(Display) → 屏幕方向(竖屏或横屏),就在设计分辨率旁边。所有目标都遵循它——微信 game.json 的 deviceOrientation、Web 构建的“请旋转设备”提示(以及尽力而为的 screen.orientation.lock)、桌面窗口的宽高比,以及原生应用的 app.config.json(引擎能 letterbox,却转不动手机)。留空则从设计分辨率的宽高比推导(更宽或方形 ⇒ 横屏,更高 ⇒ 竖屏),因此横屏设计零配置即在各处横屏发布。
试玩包是有意的例外:它从不锁定页面方向。在广告 SDK 里,容器尺寸由 SDK 决定,因此“请旋转设备”这类遮罩可能把游戏永久挡住——玩家转了手机却毫无变化,因为那条 media query 一直在读容器而不是设备。试玩包保持自适应(各平台都要求如此),方向只在平台需要时声明出去,例如 Google 的 ad.orientation meta 标签。请为两种比例都设计,或用相机适配做 letterbox,让构图在任一比例下都成立。
设置界面见 编辑器。
导出完成后包体会被称重,结果显示在构建日志下方:玩家要为它付出多少、它由什么组成、哪些文件最大。
三个数字,因为字节待在哪里比总共多少更重要:
- 首包 —— 玩家在能玩之前必须下载的部分:宿主页面、引擎运行时、你的脚本,以及
local组里的资产。 - 按需分包 —— 分包,在游戏请求那个组时才拉取。
- CDN(不进包) ——
remote组里的资产。它们是真实存在的字节,但住在你的 CDN 上,对包体零成本。见热更新。
这个划分读自运行时加载的同一份清单,所以报告说的和游戏做的不可能对不上。把一个关卡挪进分包是真的把它移出了主包,报告在你操作后立刻反映出来。
凡是平台公布了上限的目标,构建都会被对照判定,并且会说明是哪一条、数字从哪来——原文引用,好让你去核对该平台当前的文档,而不是相信我们:
| 目标 | 上限 |
|---|---|
| 微信小游戏 | 主包 4MB,所有分包合计 20MB |
| 试玩广告 | 取决于所选平台 —— Meta 单个 index.html 2MB、Google ZIP 5MB 等 |
| 网页 / 桌面 / Android / iOS | 无公开上限;可用下面的方式自定 |
超出上限只报告、不阻断——它构建成功也能跑,是否卡发布由你决定(见下面的用体积卡构建)。超过上限的 90% 后进度条转为琥珀色,因为再来一轮美术就该破线了。
打包项目 → 高级 → 体积预算 为当前所选目标设置一个 MB 上限,按平台保存在 packaging.sizeBudget 里。两种用途:你的包远低于微信 4MB,想在它悄悄涨上去之前就收到提醒;或者你的目标平台上限编辑器并不知道。
它替换该目标的主上限 —— 微信那条 4MB 主包数字会换成你的 —— 并且归因为“本项目自定的体积预算”,而不是伪装成一条平台规则。它不是豁免:微信的 20MB 总量依然生效。清空该字段即恢复平台自身的上限。
用体积卡构建
Section titled “用体积卡构建”无头导出器的结果里带着同一份报告,并且可以据此失败:
node pipeline/bin/estella.mjs export ./my-game \ --platform wechat --json report.json --enforce-budget加上 --enforce-budget,任何一条上限被突破都会以非零码退出,并在 stderr 上点名是哪一条;不加则同样报告事实但构建通过。报告就是 JSON 里的 result.size——与对话框绘制的是同一批判定,所以 CI 和编辑器永远不会对“这个包放不放得下”给出两种答案。
构建 CLI(进阶)
Section titled “构建 CLI(进阶)”编辑器打包你的游戏。而 build-tools/cli.js 命令构建的是引擎和 SDK 本身——你只在要产出上面
的微信运行时、或开发引擎时才需要它。常用命令:
node build-tools/cli.js build -t web # 构建引擎(web 目标)node build-tools/cli.js build -t wechat # …微信运行时node build-tools/cli.js sdk # 只构建 SDKnode build-tools/cli.js watch # 改动即重建(引擎开发)node build-tools/cli.js native --target ios # …原生移动宿主- Android 与 iOS —— 原生应用:前置条件、如何组装,以及只在真机上才咬人的那些事。
- 微信小游戏 —— 分包布局与平台差异。
- 资源 —— cook 步骤如何打包与内容寻址资产。
- 性能剖析与诊断 —— 发布前检查帧时间与 draw call。