メインコンテンツまでスキップ

プラグインを書く

プラグインとはディスク上のフォルダーです。ふだんの IDE で編集し、ビルド工程なしでエディター内で 動き、できあがったら公開します。

はじめかた

Plugins パネルで Create を押します。Studio がフォルダーを尋ね、そこにテンプレートを 書き込みます。

my-plugin/
meta.json マニフェスト
main.html パネルのマークアップ
main.js コード本体。.ts・.tsx・.jsx も使えます
plugin.d.ts API 全体の生成済み型定義
tsconfig.json IDE がその型を解決できるように
.wasignore 公開時にアップロードしないもの

ファイルを編集して Reload from disk を押せば、変更がそのまま反映されます。バンドラーも インストール工程もありません。

.ts.tsx.jsx は読み込み時にコンパイルされます

TypeScript と JSX をそのまま書けます。型は生成された plugin.d.ts から来るので、エディターが API 全体を補完します。

マニフェスト

{
"id": "acme.shape-spawner",
"name": "Shape Spawner",
"version": "1.0.0",
"entry": "main.tsx",
"permissions": ["scene:read", "scene:write"],
"panels": [{ "id": "main", "title": "Shapes", "entry": "main.tsx" }],
"icon": "data:image/webp;base64,…"
}

アイコンはファイルパスではなく data URL です。そのため、ローカルのフォルダーでも公開済みの プラグインでも、現れる場所すべて(ツールバー、パネルのヘッダー、ライブラリのカード)で同じに 見えます。公開フォームが任意の画像から生成してくれます。

パネル

いちばん簡単なパネルは、HTML ファイルとスクリプトです。

<div id="root">Loading…</div>
<script type="module">
import { signal } from '@was/signals';

const clicks = signal(0);

init(async () => {
// ここでは editor・world・meta がそろっています
document.getElementById('root').textContent = `${meta.name}${world.getEntities().length} entities`;
});
</script>

HTML を使わず、entry.tsx ファイルを指定してもかまいません。ランタイムがその default export をマウントし、エディター自身の component ライブラリでパネルを組めます。

// main.tsx
import { useState } from 'react';
import { Button } from '@was/ui';

export default function Panel() {
const [clicks, setClicks] = useState(0);
return <Button onClick={() => setClicks(clicks + 1)}>{clicks}</Button>;
}
init() はワールドが本当に用意できるまで待ちます

エディターへの接続が開き、かつシーンの同期コピーが用意できてから実行されるので、どちらも ポーリングする必要はありません。

グローバルは常に 4 つ使えます。editor(エディター API)、world(entity と component としての、生きた同期済みシーンのコピー)、meta(マニフェスト)、そして init です。

読み込めるのは @was/ecs@was/signals@was/engine@was/editor-api@was/ui@was/iconsreactreact-dom/client です。それ以外は、黙って何も解決しないのではなく、 はっきり失敗します。

使えるスタイルは固定です

@was/ui で組んだパネルは、あらかじめビルドされたスタイルシートを使うため、そこに含まれる クラスしか存在しません。text-[13px] のような任意の値は含まれないので、スケール外のサイズには インラインの style を使ってください。

シーンを扱う

world を通じて手で entity を組み立てることもできますが、シーンや space にはもっとよい方法が あります。エディターに頼めば、エディター自身の既定値・採番・安全策がそのまま得られます。

await editor.scenes.list();
await editor.scenes.create({ name: 'Chapter 2', trigger: { type: 'image', imageId } });
await editor.scenes.setTrigger(sceneId, { type: 'surface', bindingType: 'wall' });
await editor.scenes.delete(sceneId);
await editor.spaces.create({ name: 'Lobby' });

trigger はアンカーのデータそのもので、エディターと同じスキーマで検証されます。だから新しい トリガーの種類も、プラグインのプロトコルを変えずに動きます。画像トリガーは、物理サイズを 指定しなければ画像自体から割り出します。

自分が作ったものを後から見つける

シーンを生成するプラグインは、あとでそれを見つけられる必要があります。そのための tags です。 自動的にプラグインごとの名前空間に入るので、2 つのプラグインが互いの印を上書きすることはなく、 エディターは表示も変更もしません。

tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');

共有されるセッションの状態

状態のなかには、ドキュメントではなく「その集まり」に属するものがあります。走っているタイマー、 開催中の投票、挙がっている手。取り消しの対象にするのも、公開するのも、プロジェクトに保存するのも、 どれも違います。

プロジェクトルームは、いまそのプロジェクトにいる全員が見られる、小さな共有キー・バリュー ストアです。

const { now, entries } = await editor.room.get();
await editor.room.set('timer', { running: true, endsAt: now + 60_000 });
editor.on('room.changed', (state) => render(state.entries.timer));
await editor.room.delete('timer');

ページを再読み込みしても残り、12 時間で期限切れになり、8 KB のキーを最大 64 個まで持てます。

now がサーバーの時計であることには意味があります

2 人のパソコンの時刻は数分ずれていることがあります。サーバーの時計に基づく絶対的な終了時刻を 保存し、カウントダウンは各クライアントに任せてください。「残り何秒」を保存してはいけません。 それでは、タイマーが走っている間じゅう、プロジェクトにいる全員のために毎秒ルームへ書き込むことに なります。

サーバーはどのキーを誰が書いたかも記録します。これがあるから、正直な投票が成立します。 vote/<userId> というキーが本当にそのユーザーによって書かれたときだけ、票として数えられます。

ビューポートのオーバーレイ

パネルは自分専用で、閉じることもできます。全員に見えていてほしいものには向きません。 "surface": "hud" を指定して宣言したパネルは、代わりにシーンの上に小さなオーバーレイとして 描かれます。

editor.hud.set({ visible: true, width: 240, height: 96 });
editor.panels.open('main'); // オーバーレイから自分のパネルを呼び出せます

最初は隠れていて、見せるものができたときに現れます。シーンの上にある見えない透明な枠は、 クリックを飲み込んでしまうからです。サイズの上限は 640×400 です。

オーバーレイにはシーンのコピーが渡されません

全員に、常時出ているものなので、意図的に軽く作られています。表示に必要なものは、パネルが ルームに入れておきます。

公開する

公開フォームで指定できるのは、アイコン、名前、タグ 12 個まで、500 文字までの説明、 スクリーンショット 4 枚まで、そしてサブスクリプション限定のフラグです。

上限はファイル 1 つあたり 512 KB、64 ファイル、1 アカウントあたり 50 プラグインです。

もう一度公開すると同じエントリーが更新されます。公開済みのプラグインを編集用に ダウンロードすることもでき、選んだフォルダーにファイルが書き戻されます。

アセットが効くのは公開後だけです

公開したプラグインのファイルはきちんと配信されるので、./icon.png は解決します。ローカルの開発 フォルダーがインライン化するのは .ts.js.json だけで、パスで参照した画像は公開するまで 表示されません。

権限

マニフェストには、プラグインが必要とするもの — scene:readscene:writeresources:writespaces:writecollaboration:readcollaboration:writeeditor:panels など — を並べます。 未知のものはサーバーが拒否します。

必要なものだけを宣言する

権限は、レビューする人が読むものであり、人がプラグインを判断する材料です。仕事が成り立つ いちばん狭い範囲を求めてください。

そして他人のプラグインを導入するときは、プロジェクト全体に手が届くものと考えてください。 信頼する理由のあるものだけを入れましょう。→ 信頼と検証


次へ: component の拡張