Caputchin
应用市场游戏开发

构建一个应用市场游戏

到这篇教程结束时,你将有一个完整的、可玩的 Caputchin 游戏,装在一个 JavaScript 包里,准备好被做成可回放并发布。这是那个枯燥的、每一步都做的版本;先读 向应用市场交付一个游戏 看它适配在哪里。

你需要 Node 和一个打包器(esbuild、rollup、vite 或 webpack,它们中任何一个)。构建不需要一个 Caputchin 账户,只有发布才需要。

1. 安装 SDK

npm install @caputchin/game-sdk

SDK 很小:一个 register 助手加上 TypeScript 类型。它不捆绑组件运行时,所以你的游戏保持小巧。

2. 注册一个游戏工厂

一个游戏就是带着一个 工厂函数register 的单一一个调用。组件在沙箱化的 iframe 里调用你的工厂;那个调用本身就是那个开始信号(没有一个单独的开始事件要等)。

import { register } from "@caputchin/game-sdk";

register((container, bridge, ctx) => {
  // container - a DOM element inside the sandboxed iframe; render into it.
  // bridge    - push-only channel to the host (pass / error / setSize).
  // ctx       - the per-round context (seed + resolved locale/skin/config).

  const cleanup = startGame(container, bridge, ctx);

  // Return an optional cleanup function; the widget calls it on unmount.
  return cleanup;
});

把清单传给 register;服务器在索引时读 caputchin.json,并把解析出的预设作为 ctx 送到你的工厂里。完整的面见 SDK 参考

3. 从逐回合上下文渲染

工厂的第三个参数,ctx,携带逐访客变化的一切:

  • ctx.seed:逐回合的种子。从这里派生所有随机性(见第 5 步)。在一个已核验会话之外为 null
  • ctx.locale:解析出的语言字符串(ctx.locale._lang 加上你的键),或 null
  • ctx.skin:解析出的颜色和资源 URL(ctx.skin._theme,始终是 lightdark,加上你的键),或 null
  • ctx.config:解析出的玩法配置,或 null

从这些上面读你自己的键;你自己从不解析预设。当你的清单声明没有相匹配的块时,每一个都是 null,所以总是回退到一个内置默认:

function startGame(container, bridge, ctx) {
  const title = ctx.locale?.title ?? "Tap the targets";
  const accent = ctx.skin?.accent_color ?? "#2da44e";
  const targetCount = ctx.config?.target_count ?? 5;
  // ...render with these...
}

如果你的布局需要一个明确的尺寸,就在你第一次绘制之后调用 bridge.setSize(width, height) 一次。

4. 获胜、落败和错误

你的游戏决定玩家何时获胜,并通过 bridge 告诉宿主:

  • 在一次获胜时,带着这个回合的 轨迹(第 5 步)调用 bridge.pass({ trace })
  • 在一次落败或放弃时,什么都不调用;沉默就是那个失败信号。
  • 如果游戏自己坏了(一个资源失败、一个异常),就调用 bridge.error({ code, message })。那是给一个游戏内部失败的,不是给一个玩家落败的。
function onWin(traceString) {
  bridge.pass({ trace: traceString });
}
function onAssetFailure(err) {
  bridge.error({ code: "asset-load-failed", message: String(err) });
}

5. 记录一条轨迹

这是那个让游戏 可回放 的步骤。当玩家行动时,把驱动结果的那些输入(他们按顺序击中哪个目标,如果时机要紧也带上时机)记录下来,并把那条记录序列化成一个字符串或字节数组:那就是你的 轨迹。和种子结合起来,这条轨迹必须能让你的逻辑重现那个确切的结果,因为服务器重跑它。

两条规则让那成为可能:

  1. ctx.seed 派生每一个随机选择。 绝不为任何影响结果的东西调用 Math.random() 或读墙上时钟。
  2. 记录足够多以便回放。 轨迹加上种子就是你游戏逻辑的完整输入。
function makeRng(seed) {
  // seed is ctx.seed; feed it so the server reproduces the same sequence.
  let s = hashSeed(seed);
  return () => (s = (s * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
}

seed、轨迹和裁定的形状由 回放契约 定义;那一页讲把这次记录下来的游玩变成服务器回放的那个 headless run 工件。如果手写确定性逻辑听起来麻烦,那个 引擎套件 可选地替你做这件事。

6. 为沙箱打包

组件恰好加载一个脚本 URL,所以一切都必须在那一个文件里。把你的打包器配置为一个自包含的产物:

  • 把资源内联成 data URL(精灵图、声音、字体);
  • 禁用代码拆分;
  • 如果你用 WASM,把它作为 base64 嵌入,并从字节实例化。

那些在不透明来源、严格 CSP 的 iframe 里面 起作用的模式:

  • 路径相对的 fetch('./sprite.png'):没有一个可供拉取的路径;
  • 动态的 import('./chunk.js'):那第二个 URL 被挡掉;
  • new Worker('./worker.js'):改从内联的 Blob URL 生成 worker;
  • 运行时的外部 CDN 拉取:connect-src 把它们挡掉。

7. 在本地测试它

没有特殊的测试床。把组件嵌入一个静态 HTML 页面,把 game-src 指向你本地的包产物,并在一个浏览器里玩它:

<caputchin-game sitekey="cpt_pub_..." game-src="http://localhost:8080/game.js"></caputchin-game>

这和一个 自定义游戏 所用的 game-src 路径相同;对于应用市场,你将改为发布这个仓库,好让平台替你固定这个包。

接下来的步骤

另见

本页内容