这一组范例处理作品外壳:标题、存读档、玩家按键、音量和全屏。普通 HUD 不需要系统插槽;只有要替换引擎固定入口时才声明 supportsSlot

替换标题画面

最终效果:自定义标题画面显示真实作品名,并能打开当前项目绑定的读档、设置和鉴赏界面。 组合能力supportsSlot · ctx.system · ctx.game · 扩展设置
@extension({
  id: "title-screen",
  label: "标题画面",
  supportsSlot: INTERNAL_SYSTEM_SLOT.Title,
})
export class TitleScreen extends Extension {
  static settings = settings((s: SettingsBuilder) => ({
    startLabel: s.string("开始按钮文案").default("开始游戏"),
    showGallery: s.boolean("显示鉴赏入口").default(true),
    galleryLabel: s
      .string("鉴赏按钮文案")
      .default("鉴赏")
      .enabledWhen("showGallery"),
  }));

  render() {
    return { component: TitleMenu, props: {} };
  }
}

const TitleMenu: React.FC = () => {
  const ctx = useExtensionContext();
  const [startLabel] = ctx.settings.useValue<string>("startLabel");
  const [showGallery] = ctx.settings.useValue<boolean>("showGallery");
  const [galleryLabel] = ctx.settings.useValue<string>("galleryLabel");

  return (
    <main>
      <h1>{ctx.game.title()}</h1>
      <button onClick={() => void ctx.ui.hide("title-screen")}>{startLabel}</button>
      <button onClick={() => void ctx.system.invoke(INTERNAL_SYSTEM_SLOT.Load)}>
        读取存档
      </button>
      <button onClick={() => void ctx.system.invoke(INTERNAL_SYSTEM_SLOT.Settings)}>
        设置
      </button>
      {showGallery && (
        <button onClick={() => void ctx.system.invoke(INTERNAL_SYSTEM_SLOT.Gallery)}>
          {galleryLabel}
        </button>
      )}
      <button onClick={() => ctx.game.exit()}>退出</button>
    </main>
  );
};
调用系统插槽而不是写死其他扩展的 UI id。作者后来替换存档或设置界面时,标题画面不需要修改。

可由玩家改键的快速存读档

最终效果:默认按 F5 快存、F9 快读;动作会出现在 Studio 的「输入按键」设置中,玩家可以重新映射。 组合能力ctx.input.registerAction · ctx.archive · autonomous 注册
@extension({ id: "quick-archive", label: "快速存读档", autonomous: true })
export class QuickArchive extends Extension {
  static onRegister(ctx: ExtensionContext): void {
    ctx.input.registerAction({
      id: "quick-archive.save",
      label: "快速存档",
      defaultKeys: ["F5"],
    });
    ctx.input.registerAction({
      id: "quick-archive.load",
      label: "快速读档",
      defaultKeys: ["F9"],
    });

    ctx.input.onAction("quick-archive.save", async () => {
      try {
        await ctx.archive.quickSave();
        console.info("[quick-archive] 已快速存档");
      } catch (error) {
        console.error("[quick-archive] 快速存档失败", error);
      }
    });

    ctx.input.onAction("quick-archive.load", async () => {
      const loaded = await ctx.archive.quickLoad();
      if (!loaded) {
        console.warn("[quick-archive] 还没有快速存档");
      }
    });
  }
}
语义动作 id 必须以当前 @extension 的 id 开头。bindShortcut 适合弹窗打开期间的临时 Escape 键,不适合需要玩家改键的长期动作。

打开存档页前缓存游戏截图

最终效果:存档槽缩略图显示打开存档页之前的游戏画面,不会截到存档界面自己的蒙层。 组合能力onInit / onClose · 截图缓存 · 存档系统插槽
@extension({
  id: "save-screen",
  label: "存档画面",
  supportsSlot: INTERNAL_SYSTEM_SLOT.Save,
})
export class SaveScreen extends Extension {
  onInit(): void {
    // render 之前执行,此时存档界面还没有盖住主画面。
    void this.context.archive.cacheGameSnapshot();
  }

  onClose(): void {
    this.context.archive.clearGameSnapshot();
  }

  render() {
    return { component: SaveSlotList, props: {} };
  }
}

const SaveSlotList: React.FC = () => {
  const ctx = useExtensionContext();
  const slots = ctx.archive.useSlots();

  return (
    <section>
      {slots.map((slot) => (
        <button key={slot.id} onClick={() => void ctx.archive.save(slot.id)}>
          <img src={slot.snapshotDataUri} alt="" />
          槽位 {slot.id}
        </button>
      ))}
    </section>
  );
};
缓存只服务于本次存档界面,关闭时要清理。真正调用 save() 时,宿主会优先复用这张缓存。

做一个玩家控制中心

最终效果:一个面板同时控制主音量、跳过策略和全屏。配置变化立即反映到引擎,窗口不支持全屏时自动隐藏全屏开关。 组合能力ctx.config · ctx.game.window · React Hook
@extension({ id: "player-controls", label: "玩家控制中心" })
export class PlayerControls extends Extension {
  render() {
    return { component: PlayerControlPanel, props: {} };
  }
}

const PlayerControlPanel: React.FC = () => {
  const ctx = useExtensionContext();
  const [masterVolume, setMasterVolume] = ctx.config.useValue("masterVolume");
  const [skipMode, setSkipMode] = ctx.config.useValue("skipMode");
  const [fullscreen, setFullscreen] = ctx.game.window.useFullscreen();

  return (
    <section>
      <label>
        主音量
        <input
          type="range"
          min="0"
          max="100"
          value={masterVolume}
          onChange={(event) => setMasterVolume(Number(event.currentTarget.value))}
        />
      </label>

      <label>
        跳过策略
        <select
          value={skipMode}
          onChange={(event) =>
            setSkipMode(event.currentTarget.value as "all" | "read")
          }
        >
          <option value="read">只跳过已读</option>
          <option value="all">跳过全部</option>
        </select>
      </label>

      {ctx.game.window.canFullscreen() && (
        <label>
          <input
            type="checkbox"
            checked={fullscreen}
            onChange={(event) => setFullscreen(event.currentTarget.checked)}
          />
          全屏
        </label>
      )}
    </section>
  );
};
ctx.config 管理玩家设置,不要用扩展的 ctx.settings 代替。后者是作者在 Studio 中配置扩展规则的入口。