Builda SDK
面向把游戏发布到 Builda 平台的开发者(Godot Web / HTML5 / Unity WebGL):Builda SDK 提供哪些能力、
如何接入使用,以及必须遵守的注意事项。打包与上传规范见 h5_bundle_spec.md,本文只讲 SDK 本身。
复制给 AI
Install Builda game tools from /agent/setup.md
Builda SDK 是什么
Builda SDK 是游戏与 Builda 平台(Builda App 宿主)之间的唯一接口层,以全局对象
window.Builda 暴露给游戏。三个引擎共用同一套 JS API;Godot 与 Unity 各有一层官方包装
(GDScript addon / C# 静态类),语义与 JS 完全一致。
| 模块 | 能力 |
|---|---|
runtime | 就绪通知、退出、安全区/胶囊按钮避让、当前语言 |
whoami / requestToken | 当前玩家身份、game-scoped 短期服务 token |
getRoleInfo / getServerTime | 其他玩家的公开昵称与头像、Builda 服务端权威时间 |
privateKV | 玩家私域云存档(当前游戏 × 当前玩家) |
assets | 平台资源包(assets.zip)URL 解析 |
audio | BGM / 音效 / 静音 / 震动 / 麦克风权限 |
mic | 麦克风 PCM 流原语(声控玩法) |
pay | 内购支付面板 |
ad | 激励视频广告 |
input | 平台原生文本输入面板 |
rank | 排行榜提交与查询 |
events | 订阅式回调:App 切后台/回前台、暂停面板弹出/关闭 |
双版本机制:mock 开发版与 runtime 正式版
SDK 有两个实现,API 表面完全一致,游戏代码不需要区分环境:
- mock 开发版(
builda sdk install装进项目、builda dev本地伺服):只跑在浏览器里。身份/存档本地伪造(localStorage)、音频/震动走浏览器原生能力、支付/广告弹 mock 模态框由你手工点「成功/失败」。用于本地开发调试。 - runtime 正式版(发布在平台 CDN):只跑在 Builda App 宿主内,所有能力走宿主原生实现。无宿主时一律返回失败态(
HOST_UNAVAILABLE)或available: false,绝不伪造成功。
关键契约:游戏 zip 不打包 SDK 文件本身。
index.html的<head>用根相对路径引用<script src="builda-sdk.js"></script>。本地builda dev自动兜底伺服 mock 版;正式运行时由 Builda App 按 release manifest 的sdk契约从 CDN 下载正式版注入到同一路径。上传校验会硬查这条引用; 构建工具(vite/webpack)不要把它当模块打包,必须保持<script>外链形态。
统一返回格式
所有异步 API 返回 Promise,结果是统一的 Result 结构,不抛异常:
{ "ok": true, "data": { } }
{ "ok": false, "error": { "code": "BAD_KEY", "message": "..." } }
少数纯本地数据接口是同步裸返回(不包 Result、不需要 await):runtime.safeArea()、
runtime.capsuleMenuRect()、runtime.language()、assets.url() / assets.baseUrl,
以及 mic.read()(实时数据要当帧拿到)。多数异步 API 支持 opts.timeoutMs(默认 120 秒),
超时返回 code: "PENDING"。
除请求式 API 外还有一组订阅式回调 Builda.events.on/off(宿主主动推给游戏的事件:App
切后台/回前台、暂停面板弹出/关闭),见下方「events:订阅式回调」。
接入方式(按引擎)
三个引擎共同的前提:项目已按首页 / upload 页的安装任务装好项目级工具链(./.builda-agent/builda)。
没装的话,点右上角「复制给 AI」把安装任务发给你项目里的 AI 即可。
HTML5(pixi / phaser / Cocos / 原生 canvas)
- 项目根运行
./.builda-agent/builda sdk install—— mock SDK、builda-sdk.d.ts、测试外壳装到.builda-agent/sdk/web/(可再生,不入库)。 - 游戏 HTML
<head>加<script src="builda-sdk.js"></script>(根相对路径,构建产物必须保留)。 - 直接调用
window.Builda.*。游戏最开始必须调用Builda.runtime.ready()(入口脚本第一件事)。 - TypeScript 项目可把
builda-sdk.d.ts加进 tsconfig include 获得类型提示。 - 本地验证:
./.builda-agent/builda dev --web dist,在输出的 dev-url 测试外壳里验收。
Builda.runtime.ready(); // 游戏最开始必调(30 秒内没收到会被强制关闭)
const who = await Builda.whoami();
if (who.ok) console.log(who.data.name);
await Builda.privateKV.set("save_slot1", bytes); // bytes: Uint8Array
const r = await Builda.privateKV.get("save_slot1"); // r.data === null 表示 key 不存在
Godot(Web 导出)
- 在 Godot 项目根(含
project.godot)运行./.builda-agent/builda sdk install与./.builda-agent/builda sdk check。会安装addons/builda/(SDK 包装builda.gd、mock JS、示例), 把推荐封装BuildaClient.gd拷到scripts/并注册 Autoload,同时把 SDK 引用写进 Web 导出 preset 的html/head_include。 - 结果模型是异步 request/signal:每次调用返回
request_id,结果经sdk_result(request_id, result)信号回来(result即统一 Result 的 Dictionary 形态):
字节类值(privateKV、mic 样本)过桥统一 base64,用var builda = BuildaClient.builda var rid := builda.whoami() # 在 sdk_result 回调里按 request_id 匹配结果 builda.private_kv_set("save", var_to_bytes(save_dict)) # 读取侧:bytes_to_var(builda.private_kv_bytes(result.data))private_kv_bytes()/mic_samples()还原; 存档格式统一用var_to_bytes()/bytes_to_var()。 - 游戏最开始必须调用
builda.runtime_ready()(自动装好的BuildaClientAutoload 已在_ready()第一件事调用)。 - 编辑器 / 非 Web 导出下所有调用返回
UNSUPPORTED(不崩溃);行为验证必须导出 Web 后用builda dev --web build/web在浏览器里做。 - 不要手动混拷不同版本的 addon / JS / d.ts;版本不一致时重跑
sdk install。scripts/BuildaClient.gd归你的项目所有,可按需修改,升级不覆盖。
Unity(WebGL 导出)
- Unity 项目根运行
./.builda-agent/builda sdk install—— C# 包装层与 jslib 桥装到Assets/Builda/,Builda WebGL 模板装到Assets/WebGLTemplates/Builda/。 Player Settings → Resolution and Presentation → WebGL Template选 Builda (模板已含 SDK 引用;自定义模板需自己在<head>加)。- 调用
Builda命名空间下的静态类BuildaSDK,异步 API 传回调:using Builda; BuildaSDK.RuntimeReady(_ => {}); // 游戏最开始必调(30 秒内没收到会被强制关闭) BuildaSDK.Whoami(result => { if (result.Ok) Debug.Log(result.DataMap["name"]); }); BuildaSDK.KvSet("save_slot1", bytes); // byte[],SDK 内部 base64 过桥 BuildaSDK.KvGet("save_slot1", r => { var data = BuildaSDK.KvBytes(r.Data); // null = key 不存在 }); var lang = BuildaSDK.RuntimeLanguage(); // 同步 API 直接返回 - 编辑器 / 非 WebGL 平台所有调用返回
UNSUPPORTED失败态(不抛异常)。验证同样走 WebGL 导出 +builda dev。
能力详解与使用规范
runtime:就绪、退出、避让、语言
runtime.ready()必须在游戏最开始调用:放在入口脚本/首个场景的第一件事,不要等资源加载完再调,并确保任何启动路径都能调到它。宿主靠它确认游戏活着:页面加载 30 秒内没收到 ready,游戏会被强制关闭。mock 版页面加载 15 秒未收到 ready 会在 console 警告提醒。runtime.quit()通常不用调:平台胶囊菜单自带退出入口。调用它不会直接关游戏,而是调起平台通用暂停/退出页,去留由玩家决定。runtime.safeArea()(同步裸返回):安全区四边 inset{top, right, bottom, left},单位 CSS 像素,无刘海设备全 0。背景可全屏,关键 HUD / 按钮必须避开。游戏视口分辨率与窗口不一致时先按比例换算(inset × 视口宽 ÷ 窗口 CSS 宽)再应用。宿主会在方向变化时更新,可在 resize 回调里每次现取。runtime.capsuleMenuRect()(同步裸返回):平台右上角悬浮胶囊按钮矩形{top, right, width, height}(top/right是距视口上/右边缘的 inset)。可点击 UI 不要摆进这个矩形;自己的顶部按钮可取其top/height对齐排在左侧。width/height为 0 表示无悬浮入口、无需避让。runtime.language()(同步裸返回 String,绝不返回空串,兜底"en"):平台只给裸语言码(zh、en,不带区域变体,不会出现zh-CN),唯一例外西语为es-419。不要做简繁分支或按区域细分文案。匹配语言包按三级回退:精确匹配 → 语言前缀降级(es-419→es)→ 游戏默认语言。启动时读一次即可;宿主支持运行中切语言,在切场景等节点重读即可跟上,无需轮询。
whoami 与 requestToken
whoami()返回{id, name, avatar?}。真机由宿主提供真实玩家,游戏不感知登录、不处理任何 token;本地 mock 返回 URL 参数指定的假玩家。requestToken()返回{token, expiresAt}(Unix 秒):game-scoped 短期 token,绑定当前游戏 × 当前玩家 × 有限时效,供游戏逻辑层直连需要鉴权的 Builda 服务(如 Builda Online),不是完整登录态。临近expiresAt自行重新请求。本地 mock token 是伪造形态,连不上真实服务,只用于跑通过期刷新逻辑。
红线:token 只在内存持有,严禁写入 privateKV、本地文件、URL、日志或存档。
getRoleInfo 与 getServerTime:他人公开资料、权威时间
getRoleInfo(roleIds)批量查询其他玩家的公开昵称与头像,返回{roleId: {userName, userImg}}映射:有 key 表示本次查到,没有 key 表示本次没查到,昵称或头像可以是空字符串。roleId是正整数十进制字符串,单次最多 32 个;排行榜、联机房间里的对手信息都用它展示(whoami()只给当前玩家自己)。Godot 侧builda.get_role_info(role_ids),Unity 侧BuildaSDK.GetRoleInfo(roleIds, callback)。- SDK 会把 50ms 内的多次查询合并去重,成功资料在当前游戏页面内缓存 30 分钟、最多 512 项,因此每个玩家只查一次即可,不要高频轮询,也不要把缺 key 当成网络错误反复重试。本地 mock 返回确定性假资料,真机才访问 Builda。
getServerTime()返回{serverTimeMs}:Builda 服务端的权威时间(Unix 毫秒)。每日任务刷新、限时活动、赛季倒计时、防止改机器时间刷奖励,都应以它为准,不要用设备本地时间。Godot 侧builda.get_server_time(),Unity 侧BuildaSDK.GetServerTime(callback)。- SDK 首次调用时向 Builda 对一次时钟(最多等 1 秒),之后用本地单调时钟推算并定期重新同步,所以连续调用不会产生网络请求;对时失败且没有可用时钟时返回
NETWORK失败态,游戏应稍后重试或先按“时间未知”处理。它面向秒级业务判定,不是毫秒级对时,也不能替代服务端对奖励、排行等结果的校验。本地 mock 用本机时间推算。
const profiles = await Builda.getRoleInfo(["11299825", "11299826"]);
if (profiles.ok) {
const p = profiles.data["11299825"]; // 可能为 undefined:本次没查到
showOpponent(p ? p.userName : "玩家", p ? p.userImg : "");
}
const now = await Builda.getServerTime();
if (now.ok && now.data.serverTimeMs >= dailyResetAtMs) refreshDailyTasks();
规范:公开资料只用于当前会话内展示,不要写入存档或拼成自己的玩家库;时间判定以
serverTimeMs为准,但涉及奖励、排名、支付的最终结果仍由平台服务端校验。
playerData:本人存储观察
Builda.playerData.getMany(["coins"]) 要求非空 keys;listKeys() 返回目录 data.meta.keys,values={}。两者 data 都是 {meta,values,source}。已发布游戏联网登录时不进房也可读取本人数据,草稿试玩不支持。Godot:player_data_get_many / player_data_list_keys;Unity:PlayerDataGetMany / PlayerDataListKeys。
这是可能滞后的非权威观察,revision/source 不保证最新。发奖、扣减、偷菜和支付由 Online 游戏服务器动作裁决;不能从观察整档覆盖存档,后续旧读不能推翻正式动作结果或触发重放。失败不是空档或零余额;保留旧展示时标明“可能滞后/本次刷新失败”。
缺 key 返回 null,tombstone 是 value=null 的非 null 项。value 保留 {type,data} 类型树:int64/revision 是十进制字符串,bytes 是 Base64,object 递归包含 typed 字段,schemaVersion 为 uint32 数值或 null;不把 int64 转 Number。它与 privateKV 独立,服务端秘密不能放在可读字段。本地 mock 为独立 coins=0 fixture,不发送真实 HTTP。
privateKV:玩家云存档
当前游戏 × 当前玩家的私域存档,value 是不透明字节流(JS 侧 Uint8Array,Godot 侧
PackedByteArray + var_to_bytes(),Unity 侧 byte[])。接口:
get / set / remove / getMany / setMany / removeMany。
| 项 | 规则 |
|---|---|
| key 格式 | ^[A-Za-z0-9_:-]{1,64}$(新代码建议只用字母、数字、_、-) |
| 单 value | ≤ 32KB(按解码后字节数) |
| 批量 | ≤ 32 key / 次(SDK 不自动拆批,超过直接返回 BATCH_TOO_LARGE) |
| 总配额 | 每玩家每游戏总活 key ≤ 100(无聚合字节配额) |
| 写频率 | 服务端管控约 60 次/分钟,batch 算 1 次(mock 不模拟) |
| 并发 | last write wins,无版本号/CAS |
get 返回 null | key 不存在(空字节是合法值,删除必须显式 remove) |
setMany | 整批校验,任一违规整批拒绝;写入尽力、不承诺事务 |
removeMany | 对不存在的 key 幂等 |
使用规范
- 游戏自行维护的私域字节存档走 privateKV——不要自建 localStorage、IndexedDB、Cookie 或引擎本地文件存档。只有 privateKV 的数据能被平台统一管理(玩家在 App 里重置存档);绕开自存的数据平台管不到,重置后会出现数据残留。
- 按事件节点写入(过关、结算、购买完成),多 key 一起变时用
setMany合并成一次写。不要每帧/定时写,不要当实时状态同步用——高频写会被真机拒绝。 - 启动时读多个 key 用
getMany一次取回,不要逐 key 串行get;超过 32 key 需自行显式分批并逐批检查结果。 - 校验分层:key 格式、单值 32KB、批量 32 key 由 SDK 前置校验;总活 key 100 与写频率由服务端权威执行。超限返回明确错误码(
VALUE_TOO_LARGE/QUOTA_EXCEEDED等),不会静默截断;mock 版配额与真机一致,本地就能撞到并修复。
assets:平台资源包
大体积资源(音频、3D 模型等)不打进游戏主包,单独打成 assets.zip 经
builda assets upload 上传,内部两个顶层子树:
audio/**:宿主音频通道播放(.mp3/.ogg/.wav),配合Builda.audio.*使用。res/**:游戏代码自己 fetch 的大资源(3D 模型、纹理、关卡数据、AssetBundle、data-only pck 等)。
限额:单文件 ≤ 32MB、解压总量 ≤ 320MB、文件数 ≤ 10000;禁止脚本/可执行内容
(.js / .wasm / .html)与字体。
- 禁止写死 CDN URL——
assetsBaseUrl每次重传都会变。永远用相对路径现取:Builda.assets.url("res/models/car.glb")(Godotasset_url(...),UnityBuildaSDK.AssetsUrl(...)),返回值总是绝对 URL。 - 已走
res/**的资源不要再打进主包 zip(体积白付两遍);主包仍须自包含到「可进入并开始核心玩法」,res/**只做延迟加载内容。 - 加载失败必须可感知、可重试(进度条/占位/重试按钮),不允许白屏或卡死。
- 本地开发把
audio/、res/拷进构建目录即可闭环:mock 版在无 assetsBaseUrl 时按页面所在目录解析成绝对 URL,GodotHTTPRequest/ UnityUnityWebRequest可直接用;正式包里删掉这些本地拷贝。
audio:BGM / 音效 / 震动
await Builda.audio.playBGM("audio/bgm/main.mp3", { loop: true, volume: 0.8 });
await Builda.audio.stopBGM();
await Builda.audio.playSFX("audio/sfx/hit.ogg", { sessionId: "hit", volume: 1 });
await Builda.audio.setMuted(true, "bgm"); // group: "bgm" | "effect"
await Builda.audio.vibrate(2); // 1 轻 / 2 中 / 3 重
- 路径传 assets.zip 内
audio/**的相对路径;真机由宿主原生(FMOD)播放。 - 音频不需要「首次点击解锁」:真机是宿主原生实现,不受浏览器 autoplay 策略限制,进游戏就能直接播 BGM。不要照搬 Web 游戏惯例做「点击任意处开启声音」的引导层。本地 mock 走浏览器
Audio,可能被 autoplay 拦到首次交互后才出声——这是开发环境现象,不要为它改游戏逻辑。 - 不要自己做音乐/音效开关和退出按钮:平台通用设置/暂停退出页统一提供(含静音开关),游戏设置页里也不要放。
mic:麦克风声控
声控玩法(音量/吹气/音高)优先用官方内置件 BuildaMic(随 SDK 分发,Godot / Unity 同构),
不要直连底层原语:
- Godot:注册 autoload
BuildaMic="*res://addons/builda/mic/builda_mic.gd";点击回调里BuildaMic.start();每帧读BuildaMic.volume(RMS)/BuildaMic.pitch(Hz)驱动玩法,监听state_changed更新引导 UI。
打包注意:导出后必须把addons/builda/mic/web/原样拷进导出目录再压 zip——分析 worker 必须随包分发,本地 dev 有兜底会骗过你,正式包缺文件真机报WORKER_LOAD_FAILED。 - Unity:把
BuildaMic组件挂常驻对象,StartMic()/Volume/Pitch/StateChanged;分析 worker 在Assets/StreamingAssets/builda-mic/,WebGL 构建自动带上。 - 换算法/加特征只改各自的
builda-mic-worker.js(重 DSP 就该在这个 Worker 里跑);该目录归项目所有,SDK 升级不覆盖。
直连底层(audio.requestMicrophone + mic.start/read/stop,HTML5 无内置件时用)的核心约束是两次手势:
- 第 1 次点击(「开启声控」按钮)回调里调
audio.requestMicrophone()申请权限。available: false= 用户拒绝或无麦:给出明确的阻断提示并允许重试(不能黑屏/卡死/静默);无麦替代玩法可选、不是必须。 - 权限结果回来后不要直接调
mic.start()——权限回调不在手势上下文(Godot 信号经逐帧轮询派发必然脱离手势),AudioContext 会被自动播放策略拦下且不可恢复。停在「再点一次开始」的引导,第 2 次点击回调里调mic.start()。 - 之后每帧
mic.read()同步拉单声道 float32 PCM([-1,1]),不用时mic.stop()。
start()幂等可重入;并发/重复调用在途去重合并到同一结果,不会返回瞬时BUSY。read()的dropped > 0表示拉取过慢被环形缓冲覆盖(实时控制通常无害,别当连续流做录音);state: "interrupted"表示采集中断(来电/切后台),SDK 回前台会自动重建,持续中断时引导玩家点击后重调start()。- 实际采样率以
start()返回的sampleRate为准,期望值不保证生效。 - SDK 只给原始 PCM,不做任何特征分析;音高/音量提取放你自己的 Web Worker,不要在 GDScript 或主线程跑重 DSP(会和引擎抢帧预算)。
- 开着 BGM 实测声控灵敏度——SDK 显式关闭了回声消除/降噪(否则 BGM 时人声被对消成静音),BGM 串音必然存在于 PCM,阈值要在真实混音下调。
红线:PCM 只许本地实时分析,禁止录音留存/上传,违反视为审核事故。
iap:内购支付
// grantBy 声明预期发货路径;不覆盖订单冻结配置,实际发货按回包 grantBy 分流
const r = await Builda.iap.showPayPanel("coin_pack_1", { grantBy: "client" });
// r.data = { success, orderId, grantBy } success 只表示付钱成功,订单停在 Paid
// r.data.grantBy === "client":按 orderId 幂等发货,然后
await Builda.iap.claim(r.data.orderId);
// r.data.grantBy === "room":把 orderId 交 Room doOrder,客户端不发货不 claim
// { orderId, status: "delivered" }(幂等)
const pending = await Builda.iap.getPendingOrderIds();
// pending.data = { orderIds: string[] }
saleId即 manifest(builda.publish.json)payPoints里声明的payId(格式^[A-Za-z0-9_-]{1,64}$)。商品配置、订单创建/查询全部由宿主直连平台完成,游戏不要自己拼订单接口。success == true只表示付钱成功,订单停在 Paid,不是发货完成;false表示取消或失败,不要发货。回包grantBy是随这一单冻结的发货方,以它为准:client由客户端按orderId幂等发放,发完必须claim(orderId)(Paid→Delivered,幂等;不 claim 的单永远留在未交付列表);room把orderId交给 RoomdoOrder,客户端不得发货、不得 claim。Builda.pay.showPayPanel是 legacy:宿主在回包前自动 claim,订单直接 Delivered、无法补发、不能用于grantBy=room。只为线上老游戏保留,新代码不要接;pay下没有claim/getPendingOrderIds。getPendingOrderIds()只读返回未交付orderId列表,没有payId/grantBy。发货责任以下单当时冻结的为准,不能用当前价目表反推,也不能从裸orderId猜。未知时不要猜成客户端发货。确认是 room 单才把orderId交 Room(doOrder的allowed才带可信payId);确认是 client 单才客户端幂等发。payPoints声明规则:price是整数,单位平台虚拟币 G 币(1 元人民币 ≈ 10000 G 币),最小 100、建议 100 的整数倍。发货方grantBy只允许client/room;省略或""等于client。Room 发货必须显式"grantBy":"room"。同一payId只能一个发货方。无内购省略整个payPoints(省略 = 不改动平台已有价目表),不要写空数组;当前没有「清空全部付费点」的自助手段,需下架联系平台。- 修改已有
payId会改变商品映射,改前务必确认。
ad:激励视频
const r = await Builda.ad.showRewardAd("revive");
// r.data = { success, transId }
posId是游戏自定义的广告位统计标识(如revive、double_reward),任意字符串,只用于区分游戏内触发点做统计。不是平台分配的广告位 ID,无需申请、无需在 manifest 声明。success == true= 有效看完,按游戏自己的设计发放游戏内奖励;false= 失败/中途关闭/宿主未接广告,不发放。平台只负责播广告并回结果,不发放任何奖励;transId是平台广告流水号,可用于游戏自己的展示或服务端校验。游戏不要调用任何广告核销接口。- 浏览器或无宿主时支付/广告返回
HOST_UNAVAILABLE——不能伪造成功。
input:文本输入
手机 WebView 里游戏自绘输入框拿不到系统 IME(拼音候选、光标、选区全没有)。起名、输房间号等文本输入一律调:
const r = await Builda.input.showInputPanel({ placeholder: "输入昵称", defaultValue: "", maxLength: 16 });
// 确认: { confirmed: true, text } 取消: { confirmed: false, text: "" }
- 返回的
text已由平台完成敏感词审核与长度截断,拿到直接用,不要再自行送审。 - 改名等回填场景把当前值传
defaultValue。maxLength默认 64、上限 256。 - 同刻单例:面板未关时重复调用返回
BUSY。 - 不要自绘文本框监听键盘事件。
rank:排行榜
榜单必须先在 builda.publish.json 的 rankBoards 声明并随发布审核生效
(每榜 rankId/displayName/sortType/cycleType/minScore/maxScore,最多 100 个榜;
sortType: asc|desc,cycleType: day|week|month|forever)。
await Builda.rank.submitScore("weekly_score", 1234);
const r = await Builda.rank.getRankList("weekly_score", { limit: 50 });
// r.data = { rankId, list: [{rank, score, player}], self: {rank, score} | null }
- 分数是整数,平台总范围 ±9999999999;每榜自己的 min/max 由宿主按已发布配置权威校验(未发布
RANK_NOT_FOUND、越界SCORE_OUT_OF_RANGE)。 - 宿主按
sortType保留每玩家每周期最优成绩——结算时直接提交即可,不用自己比大小。 getRankList默认 50、上限 100;self为null表示本周期未上榜。- 改
rankId等价于换新榜(历史分丢失);改排序/周期/分数范围或删榜会在审核发布后清分。 - mock 版不校验榜单配置(拿不到 manifest),只回固定假对手供 UI 调试;真实语义要在 Builda App 草稿试玩里验证。
events:订阅式回调(App 前后台、暂停面板)
请求式 API 是「游戏问、平台答」;events 是「平台推、游戏收」。当前有两个事件。
const off = Builda.events.on("appVisibility", ({ visible, source }) => {
if (!visible) pauseGame(); // App 最小化 / 切后台 / 锁屏
else resumeGame(); // 回到前台
});
// off(); // 需要时退订;Builda.events.off("appVisibility") 清空全部订阅
Builda.events.on("pausePanel", ({ visible }) => {
if (visible) pauseGameplay(); // 宿主「已暂停」面板弹出(胶囊 ✕ 或 runtime.quit() 弹的同一面板)
else resumeGameplay(); // 面板关闭且没有退出
});
appVisibility:visible=false= App 最小化/切后台/锁屏(WebView 仍存活),true= 回前台。false时暂停玩法、计时器、BGM,停掉不必要的网络写入;true时恢复。source为"host"(宿主推送,精确)或"browser"(WebViewvisibilitychange兜底),SDK 已归一并按状态去重,游戏不必区分。pausePanel:面板弹出时宿主会自己暂停 BGM、停音效,但游戏逻辑在 WebView 里照跑,这个事件让游戏同步暂停玩法与计时。真退出不触发visible:false。只来自宿主推送,无浏览器兜底。- 两组事件正交:面板开着时切后台两个都会触发。按「任一暂停即暂停、全部恢复才恢复」处理,SDK 不做合并。
- 都只在状态变化时触发,订阅时不补发当前状态(初始视为前台、面板未显示)。
on返回退订函数;同一 handler 重复on只登记一次;某个 handler 抛错不影响其他订阅者。 - 引擎侧:Godot 用
builda.gd的app_visibility_changed(visible, source)/pause_panel_changed(visible)信号(通用形态sdk_event(name, payload));Unity 用BuildaSDK.AppVisibilityChanged/PausePanelChanged(通用形态BuildaSDK.EventsOn(name, handler))。都在引擎主线程回调。 - 本地
builda dev测试外壳:「切后台」按钮模拟宿主前后台推送,点胶囊按钮弹出的暂停面板与「继续游戏」模拟pausePanel;浏览器切标签页走source:"browser"。
online:联机(Builda Online)
需要多人对战、房间或权威服务器时,联机能力由 Builda Online 提供,与本 SDK 同版本发布:
Builda Online 用户文档。在完成本页的 Builda 接入之后,让项目 AI 运行
./.builda-agent/builda sdk install --with-online(CLI 0.4.56 起):联机 Skill、客户端 SDK、Room SDK 与本地 Mock
都跟随本 SDK 版本安装,之后由 sdk upgrade 一起升级。AI 可阅读的安装说明:https://ai.builda.game/online/releases/0.12.3/setup.md。
本地开发与调试
builda dev 测试外壳
./.builda-agent/builda dev --web <构建目录> # 如 dist、build/web
./.builda-agent/builda dev --web build/web --safearea 44,0,34,0 # 模拟刘海+底部横条
打开输出的 dev-url,测试外壳提供:横竖屏切换、刘海/安全区模拟、胶囊按钮模拟、支付/广告 mock 模态框
(模拟宿主面板盖在 WebView 上)、「拒绝麦克风」/「麦克风中断」开关、「切后台」按钮(模拟宿主前后台推送);
胶囊按钮弹出的暂停面板同时模拟 events.pausePanel。
mock 环境 URL 参数
| 参数 | 作用 |
|---|---|
builda_mock_player / builda_mock_name | 切换 mock 玩家身份(KV 按玩家隔离) |
builda_mock_lang=es-419 | 覆盖 runtime.language() 返回值,测语言回退 |
builda_mock_safearea=44,0,34,0 | 即 --safearea,注入安全区(上,右,下,左) |
builda_mock_mic=deny | 模拟用户拒绝麦克风 |
mock 与真机的差异
本地测过 ≠ 真机没问题。mock 验证接口形态与 UI 流程;真实身份、KV、支付、广告、排行榜语义只能走草稿流程 在 Builda App 待发布区试玩验证。
| 能力 | mock(builda dev) | 真机(Builda App) |
|---|---|---|
| 身份 | URL 参数假玩家 | 真实登录玩家 |
| privateKV | localStorage(配额校验一致;写频率不管控) | 宿主落服务端/本地,写频率 60/min |
| 支付/广告 | 模态框手工点成功/失败 | 宿主真实面板/广告 |
| 音频 | 浏览器 Audio(受 autoplay 限制) | 宿主原生,无 autoplay 限制 |
| 排行榜 | 不校验榜单配置,固定假对手 | 按已发布 rankBoards 权威校验 |
| requestToken | 伪造 token,连不上真实服务 | 宿主向平台换发真 token |
| events.appVisibility | 外壳「切后台」按钮模拟宿主推送;切标签页走浏览器兜底 | 宿主 onAppEvent 推送 + WebView 兜底 |
| events.pausePanel | 外壳胶囊按钮弹出的暂停面板 / 「继续游戏」 | 宿主「已暂停」面板弹出/关闭推送 |
| SDK 文件 | dev 兜底伺服 mock 版 | 宿主按 manifest 从 CDN 注入正式版 |
发布前自查清单
- 游戏最开始就调用了
runtime.ready(),且任何启动路径都能调到(dev 时看 console 有无 15 秒警告;真机 30 秒内没收到会被强制关闭)。 index.html<head>保留<script src="builda-sdk.js">外链,zip 未打包 SDK 文件。- 私域字节存档走 privateKV;Online 服务器管理的 Player Data 独立,无 localStorage/本地文件自存;写入按事件节点、无定时写。
- 没有自做的音乐/音效开关、退出按钮、「点击开启声音」引导层。
- 关键 UI 避让 safeArea 与 capsuleMenuRect(
--safearea实测横竖屏)。 - 支付走
Builda.iap(不是 legacypay);success只表示付钱成功;grantBy=client才按 orderId 在客户端幂等发放并claim,grantBy=room把 orderId 交 Room。 - 文本输入走
showInputPanel,未自绘输入框。 - 使用排行榜的:manifest
rankBoards已声明且代码里rankId一致。 - 使用 assets 的:无写死 CDN URL;
res/**资源未重复打进主包;加载失败分支实测过。 - 使用 BuildaMic 的(Godot):
addons/builda/mic/web/已拷进导出目录随 zip 分发。 - 声控游戏:拒绝权限、中断/恢复、开 BGM 灵敏度等用例在外壳里逐条过。
常用的 AI 指令
装好工具链后,以下任务直接把这句话说给你的 agent(Claude Code / Cursor 等)即可:
| 指令 | 作用 |
|---|---|
| 打开本地预览 | 启动本地 mock 页面,可在 PC 网页端预览 App 内的效果 |
| 上传草稿 | 将游戏上传到 Builda App,随后可在 App 内发布 |
| 部署服务器(联机游戏) | 上传并激活 Room 服务端源码,由 Builda Online Skill 执行,见 Builda Online 用户文档 |
注意事项与红线汇总
红线(违反会被拒审或视为事故):
- 不要绕过 SDK 直调宿主桥——游戏代码禁止接触
window.__cysjHost、flutter_inappwebview.callHandler或任何底层 App 桥方法。- token 只在内存——
requestToken的 token 禁写 privateKV、本地文件、URL、日志、存档。- 麦克风 PCM 只许本地实时分析——禁止录音留存/上传。
- 无宿主不伪造成功——浏览器环境支付/广告返回
HOST_UNAVAILABLE是正确行为,不要在游戏侧兜底成「购买成功」。- 不要修改 SDK 分发文件——
builda-sdk.js、addons/builda/(mic/web/除外)、Assets/Builda/由sdk install/builda check升级时整体覆盖;归你所有的只有scripts/BuildaClient.gd、mic 分析 worker 目录和你自己的游戏代码。
持久化标识符(改动前必须想清楚)
- privateKV key:
^[A-Za-z0-9_:-]{1,64}$;payId/rankId:^[A-Za-z0-9_-]{1,64}$。新代码统一只用字母、数字、_、-。 - 改
payId= 改商品映射;改rankId= 换新榜清历史分。已上线的 ID 不要顺手重命名。 - 存量数据的旧 key(如带
:的)可以继续读写删,不要为格式统一强行迁移。
平台代劳、游戏别做的事
- 登录 / token 管理(
whoami直接拿人) - 音乐/音效开关、暂停/退出页(胶囊菜单 + 平台通用页)
- 文本审核与截断(
showInputPanel返回即终态) - 订单创建/查询(不要自拼订单 HTTP;未交付列表用
iap.getPendingOrderIds。补单由游戏自己做,见上方 iap 节) - 广告核销(
showRewardAd返回即结论) - 排行榜最优成绩比较与周期轮换(宿主按 sortType 保留)
- 音频解锁(真机无 autoplay 限制)
常见错误码
| code | 含义 |
|---|---|
HOST_UNAVAILABLE | 无 Builda App 宿主(浏览器直开 runtime 版时的正常失败态) |
PENDING | 等待宿主响应超时(默认 120s,可传 timeoutMs) |
BUSY | 同刻单例的面板已打开(如 input 面板未关重复调) |
UNSUPPORTED | 非 Web/WebGL 环境调用引擎包装层 |
BAD_KEY / BAD_VALUE / BAD_PAY_ID / BAD_RANK_ID / BAD_SCORE | 入参格式非法(SDK 本地前置校验) |
VALUE_TOO_LARGE / BATCH_TOO_LARGE / QUOTA_EXCEEDED | privateKV 超限 |
RANK_NOT_FOUND / SCORE_OUT_OF_RANGE | 榜单未发布 / 分数越界(宿主权威校验) |
PERMISSION_DENIED / MIC_UNAVAILABLE / MIC_NOT_STARTED | 麦克风权限拒绝 / 不可用 / 未 start 就 read |
WORKER_LOAD_FAILED | BuildaMic 分析 worker 未随包分发 |
JS_ERROR / BAD_RESULT / BAD_RESPONSE | 桥层异常(多为接入方式错误,检查 SDK 版本与调用姿势) |
进一步阅读
| 文档 | 内容 |
|---|---|
h5_bundle_spec.md | 游戏 zip 打包规范(入口、排除清单、分引擎校验规则) |
md_persistent_ids.md | 持久化标识符(KV key / payId / rankId)完整契约 |
md_rankboards.md | 排行榜 manifest 配置完整契约 |
md_bundle_size.md | 包体瘦身与 res/** 资源分包实战 |
md_godot_i18n.md / md_unity_webgl.md | 多语言与字体、Unity WebGL 导出专题 |
md_mobile_perf.md | 移动端性能 / 动态 DPR |
| Builda Online 用户文档 | 联机能力:玩家体验选择、匹配与房间、Builda App 自带的房间聊天与邀请、使用限制;AI 安装:builda sdk install --with-online,说明见 https://ai.builda.game/online/releases/0.12.3/setup.md |
md_* 专题文档随 SDK 0.12.3 一起发布在 https://ai.builda.game/game-sdk/releases/0.12.3/,也随三个引擎包的 guidance/ 安装到项目;h5_bundle_spec.md 随 Agent 工具链分发。