プラグインを書く
プラグインは、小さな型付きの契約を通して debuff を拡張します(ADR 0004)。このガイドでは、ブラウザで動く ランタイムプラグイン を扱います。ツール側のプラグイン(アセットインポーター、CLI コマンド、シナリオのステップ)はフェーズ 2 で登場します。
契約
ts
import type { Plugin } from "@debuffjp/core";
interface Plugin {
readonly name: string; // unique, kebab-case
install(game: Game): (() => void) | undefined; // optional cleanup, run on game.dispose()
}game.use(plugin) で組み込みます。戻り値は game なので、呼び出しをつなげられます。プラグインが触ってよいのは Game の公開 API だけです。
| 使うもの | 用途 |
|---|---|
game.hooks.beforeUpdate / afterUpdate(fn(dt)) | シーンの update の前後で行うシミュレーション処理 |
game.hooks.beforeRender / afterRender(fn(ctx, alpha)) | シーンの下や上への描画。afterRender は論理単位で、カメラの外側です |
game.hooks.sceneChange(fn(next, previous)) | シーンの切り替えへの反応 |
game.params.define("<name>", schema) | 調整できる設定。ツールやエージェントからも見えます |
game.inspect.register("<name>", () => json) | dev ツールやシナリオ向けの状態(__DEBUFF_DEV__.state().plugins.<name>) |
game.camera、game.random、game.clock、game.stats | 共有サービス。ゲームプレイや見た目に影響するものには game.random を使い、実行の決定性を保ちましょう |
フックの登録はすべて、登録解除用の関数を返します。それらをすべて呼ぶ後片付け関数を返してください。
例
ts
import type { Plugin } from "@debuffjp/core";
export interface DebugGridOptions {
size?: number;
color?: string;
}
export function debugGrid(options: DebugGridOptions = {}): Plugin {
return {
name: "debug-grid",
install(game) {
const params = game.params.define("debug-grid", {
size: { type: "number", default: options.size ?? 16, min: 4, max: 128, units: "px" },
visible: { type: "boolean", default: true },
});
let lines = 0;
const offRender = game.hooks.afterRender((draw) => {
if (!params.values.visible) return;
lines = 0;
for (let x = 0; x <= game.width; x += params.values.size, lines++) draw.line(x, 0, x, game.height, 1, options.color ?? "#ffffff1a");
});
const offInspect = game.inspect.register("debug-grid", () => ({ lines }));
return () => {
offRender();
offInspect();
};
},
};
}ルール
- すべてプラグイン名で命名する。 プラグイン、そのパラメータグループ、inspect のプロバイダーには同じ ID を使います。
- きっちり後片付けする。
game.dispose()のあとに、あなたのコードが何かを listen していたり DOM にくっついていたりしてはいけません。 - グローバル状態とモンキーパッチは禁止。
Gameやほかのプラグインを書き換えないでください。 - dev 専用のコード は
import.meta.env.DEVの中に置きます。漏れていたらdebuff buildが失敗します。 - 使った分だけ払う。
"sideEffects": false付きの ESM で出荷し、import 時には何も処理しないこと。 - ファクトリーはオプションを受け取る。 組み立て済みのオブジェクトではなく
myPlugin(options): Pluginをエクスポートして、ゲーム側で設定できるようにします。
公開
- 名前:
debuff-plugin-<name>(または@scope/debuff-plugin-<name>)。公式プラグインは@debuffjp/plugin-<name>を使います。 debuff-pluginキーワードを付け、@debuffjp/coreを peer dependency にします。- ブラウザ用のコードは
"."で公開します。ツール用のコード(フェーズ 2)は"./tooling"に置く予定です。
リファレンス実装
@debuffjp/dev は dev-tools プラグインとして提供されています(game.use(devTools({ input })))。後片付けまで含めた完全な例として、ソースを読んでみてください。
アセットインポーター
ツール側のプラグインは debuff.config.ts に書きます。インポーターは担当するファイルの拡張子を宣言し、マニフェストのエントリーを返します。TypeScript の関数でもいいですし、stdin から SourceFile を JSON で読み、stdout に ImportResult を JSON で出力するプログラムなら何でも構いません。
ts
import { defineConfig } from "@debuffjp/cli";
export default defineConfig({
plugins: [
{
name: "maps",
apiVersion: 1,
assets: [{ name: "maps", extensions: [".map"], command: ["python", "tools/import_map.py"] }],
},
],
});結果はコンテンツのハッシュでキャッシュされます。インポーターはファイルを生成したり、隣にあるソースファイルを自分の担当として宣言したりもできます(そうすれば、スプライトシートの PNG が普通の画像として二重に取り込まれることはありません)。