Caputchin
自定义游戏开发

构建一个自托管游戏

到这篇教程结束时,你将有一个真正的 Caputchin 游戏:一个跑在组件沙箱化 iframe 里、通过游戏 SDK 和宿主对话、并报告一条服务器能重跑的 轨迹 的 JavaScript 包。不像 手动模式,一个自托管游戏能 给一个站点密钥设关卡,因为它的回合可回放。这篇教程讲构建并托管这个游戏;回放与设关卡 讲那个真正把关卡打开的工件。

这适配在哪里,见 跑你自己的游戏。你需要一个托管一个静态 JavaScript 文件的地方(任何 CDN 或静态主机),以及你页面上的组件。

一个自托管游戏的形状

一个自托管游戏是一个自包含的 JavaScript 包,它:

  1. 导入 @caputchin/game-sdkregister 函数,并注册单一一个 游戏工厂
  2. 渲染进组件交给工厂的那个容器,用随它一起传来的解析出的语言、皮肤和配置。
  3. 在逐回合的种子下确定性地玩出来,把这次游玩记录成一条 轨迹
  4. 在一次获胜时,带着那条记录调用 bridge.pass({ trace })

组件把你的包加载进它的 iframe 并调用你的工厂;工厂被调用 就是 那个开始信号(没有一个单独的开始事件要等)。

1. 注册一个游戏工厂

安装 SDK,并带着你的工厂调用 register 一次。你 传清单;服务器解析预设并把它们交给工厂。

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;
});
npm install @caputchin/game-sdk

2. 读取逐回合上下文

工厂的第三个参数,ctx,携带你游戏为这位访客所需的一切:

  • ctx.seed:逐回合的回放种子。从这里派生所有随机性。 当游戏在一个已核验会话之外跑时为 null。
  • ctx.locale:解析出的语言对象:ctx.locale._lang 加上你已翻译的字符串键。
  • ctx.skin:解析出的皮肤对象:ctx.skin._theme(始终是 lightdark)加上你的颜色和资源键。
  • ctx.config:解析出的玩法配置(或 null)。

这些是为这位访客 解析出的预设,由 Caputchin 从你仪表盘定义的 模式与预设 产出。你从它们上面读你自己的键;你自己不解析任何东西。

如果你的布局需要一个明确的尺寸,就在你第一次绘制之后告诉宿主一次:

bridge.setSize(360, 480);

3. 让游玩确定性

这是那条让一个游戏可设关卡的规则:给定种子和玩家的输入,游戏必须确定性。 不要为任何影响结果的东西调用 Math.random() 或读墙上时钟;从 ctx.seed 派生每一个随机选择。服务器在同一个种子和同一条记录下来的轨迹下重跑你的逻辑,并且必须得到同一个裁定。

// A tiny seeded PRNG; feed it ctx.seed so the server reproduces the run.
function makeRng(seed) {
  let s = hashToInt(seed);
  return () => (s = (s * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
}

在你游玩时,把驱动结果的那些输入(玩家按顺序击中了哪些目标,如果时机要紧也带上时机)记录下来。把那条记录序列化成一个字符串:那个字符串就是你的轨迹。

4. 带着轨迹通过

当访客获胜时,把轨迹交给宿主。pass 接受一个带单一一个 trace 字符串的对象:

function onWin(traceString) {
  bridge.pass({ trace: traceString });
}

这条轨迹 对平台不透明:它就是一个只由你的游戏定义的字符串,使得你的逻辑、和种子结合起来,重现这个结果。游戏在这里不报告一个分数;计分,如果有的话,是你自己在 iframe 内的事。

再两个 bridge 事实:

  • 第一次 pass 兑现这个回合;一个失败或被放弃的回合,靠干脆 不调用 pass 来示意(这个 bridge 上没有 fail 方法)。
  • bridge.error({ code, message }) 是给一个游戏内部失败的(一个资源加载失败、一个异常),而非一次玩家落败。它向宿主浮现一个 error 事件。

在宿主兑现这次通过之后,它释放组件令牌,而你的后端一如往常 核验那个令牌

5. 为沙箱打包

组件恰好加载一个脚本 URL,所以一切都必须在那单一个文件里。把你的打包器(esbuild、rollup、vite、webpack;它们中任何一个)配置为一个自包含的产物:

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

那些在沙箱里面 起作用的模式,因为这个 iframe 是不透明来源、带一个严格的 CSP:

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

6. 托管这个包并把组件指向它

把构建出的文件通过 https 从你自己的静态主机提供(本地开发允许回环 http),然后把组件指向它:

<caputchin-game
  sitekey="cpt_pub_..."
  game-src="https://cdn.example.com/my-game/game.js"
></caputchin-game>

组件把你的包加载进它沙箱化的 iframe、带着上下文调用你的工厂、并等你的 pass。在这一刻游戏跑并验证,但它还不被允许给一个密钥 设关卡

7. 让它可设关卡

要把你的自托管游戏用作一个验证关卡,还需要两样东西,接下来都讲到:

  1. 在仪表盘上把它注册为一个自定义游戏(一个你选的 id),并定义它的字段 模式与预设,好让上下文携带真实的 locale/skin/config。
  2. 上传一个回放工件:一个你游戏逻辑的 headless 构建,服务器跑它来从种子和轨迹重新推导那个裁定。见 回放与设关卡

在那个回放工件被上传并通过它的自检之前,这个自定义游戏显示 不可回放,且无法设关卡。

另见

本页内容