プラグインをつくる
Studio のプラグインを自分でつくります。フォルダーの作成、パネルの作成、シーンの操作、公開までを説明します。
プラグインは、パソコン上の 1 つのフォルダーです。いつも使っているコードエディタで編集し、Studio はビルドなしでそれを動かします。できあがったら公開します。
公開したプロジェクトで動くコードについては、ランタイムコードを参照してください。
プラグインを作成する
- Plugins パネルを開きます。
- Create plugin を押します。
- フォルダーを選びます。Studio がそこにスターターテンプレートを書き込みます。
my-plugin/
meta.json マニフェスト
main.html パネルのマークアップ
main.js あなたのコード。.ts、.tsx、.jsx でも動きます
plugin.d.ts API 全体の型(自動生成)
tsconfig.json IDE がその型を解決するための設定
.wasignore 公開時にアップロードしないもの
変更を確認するには、ファイルを保存して Reload を押します。バンドルやインストールは要りません。
.ts、.tsx、.jsx のファイルをそのまま書けます。自動生成される plugin.d.ts があるので、コードエディタで API 全体の補完が効きます。
マニフェスト
meta.json にプラグインの情報を書きます。
{
"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,…"
}
icon はファイルパスではなく、データ URL です。こうすることで、ローカルのフォルダーでも公開したプラグインでも、ツールバー、パネルのヘッダー、ライブラリのカードのどこでも同じアイコンが表示されます。公開フォームで、好きな画像からアイコンをつくることもできます。
マニフェストには、ほかにも次のものを追加できます。
| フィールド | 追加されるもの |
|---|---|
components | オブジェクト用のコンポーネント → カスタムコンポーネント |
runtime | 公開したプロジェクトで動くコード → ランタイムコード |
triggers, steps | イベントパネルの項目 → トリガーとステップ |
settings | Project settings のセクション → プロジェクト設定 |
locales | 翻訳 → プラグインを翻訳する |
actions | レイヤーとアセットの右クリックメニューの項目 → メニュー項目とショートカット |
commands | ほかのプラグインやアシスタントが呼び出せるコマンド → コマンド |
skills | プラグインのあるプロジェクトでアシスタントが読むガイド。それぞれがフォルダー内の .md ファイルです |
パネル
いちばんシンプルなパネルは、スクリプトを含む HTML ファイルです。
<div id="root">Loading…</div>
<script type="module">
init(async () => {
// ここでは editor、world、meta の準備ができています
document.getElementById('root').textContent = `${meta.name} — ${world.getEntities().length} entities`;
});
</script>
HTML を使わずに、entry で .tsx ファイルを指定することもできます。Studio はその default export をマウントします。パネルは、エディタ自身のコンポーネントを使って組み立てられます。
// 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() はすべての準備ができるまで待ちますinit() は、エディタとシーンの準備ができてから動きます。どちらも自分で確認する必要はありません。
コードで使えるもの
次のグローバルを使えます。
| グローバル | 内容 |
|---|---|
editor | エディタの API |
world | 常に最新の状態のシーン。エンティティとコンポーネントで表されます。マニフェストに scene:read がない場合は null で、オーバーレイでは常に null です |
tags | オブジェクトに付ける、あなたのプラグイン専用のラベル(下を参照)。world が null のときは null です |
meta | あなたのマニフェスト |
init | エディタとシーンの準備ができたら、あなたのコードを実行します |
インポートできるのは、@was/ecs、@was/signals、@was/engine、@was/editor-api、@was/ui、@was/icons、react、react-dom/client です。ほかのものをインポートするとエラーになるので、すぐに気づけます。
@was/ui でつくったパネルは既製のスタイルシートを使うので、そこにあるクラスしか効きません。text-[13px] のような任意の値は効きません。スケールにないサイズには、インラインの style を使ってください。
シーンを操作する
world を通じてエンティティを手で組み立てることもできます。ただ、シーンやスペースはエディタに頼んでつくってください。Studio で作業するときと同じデフォルト値、番号づけ、安全チェックが適用されます。
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 はアンカーのデータです。エディタと同じスキーマでチェックされるので、新しい種類のトリガーが増えても、プラグイン API を変えずにプラグインで使えます。画像トリガーで実際のサイズを指定しなかった場合は、画像から計算されます。
自分のオブジェクトをあとで見つける
シーンを生成するプラグインは、あとでそのシーンを見つけ直す必要があります。それには tags を使います。
tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');
タグはプラグインごとに別々に保存されるので、2 つのプラグインが互いのタグを上書きすることはありません。エディタがタグを表示したり変更したりすることもありません。
プロジェクトルーム
進行中のタイマー、開催中の投票、挙手など、プロジェクトではなく今のセッションに属する状態もあります。こうした状態は、元に戻したり、公開したり、プロジェクトと一緒に保存したりするものではありません。
プロジェクトルームは、キーと値を入れる小さな共有ストアです。今そのプロジェクトにいる全員に見えます。
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 時間で期限切れになります。
- キーは 64 個までです。キー名は 128 文字まで、値は JSON で 8 KB までです。制限を超える書き込みは無視されます。
- プロジェクトにいる全員が読めます。書き込めるのは、プロジェクトを編集できる人だけです。
パソコン同士の時計は何分もずれていることがあるので、now でサーバーの時刻を取得できます。now をもとに終了時刻を保存し、カウントダウンはそれぞれのエディタに任せましょう。「残り秒数」を保存してはいけません。そうするとタイマーが毎秒、全員に向けてルームに書き込むことになります。
サーバーは、どのキーを誰が書いたかを記録します。プラグインがこれを偽ることはできません。これを使うと公正な投票ができますが、チェックはプラグインの側で行う必要があります。サーバーは、編集できる人なら誰でも、どのキーにも書き込めるようにしているからです。サンプルの Session voting は、キー vote/<userId> をそのユーザー本人が書いた場合だけ、票として数えます。
ビューポートのオーバーレイ
パネルはあなただけのもので、閉じることもできます。全員に見せたいものがある場合は、"surface": "hud" を指定したパネルを宣言します。シーンの上に小さなオーバーレイとして表示されます。
editor.hud.set({ visible: true, width: 240, height: 96 });
editor.panels.open('main'); // オーバーレイから自分のパネルを開くこともできます
オーバーレイは最初は非表示です。表示する内容があるときだけ表示してください。空のオーバーレイでも、シーンへのクリックを横取りしてしまいます。最大サイズは 640×400 です。
オーバーレイでは、world は null です。表示に必要なものは、パネルからプロジェクトルームに入れておいてください。
プラグインを翻訳する
言語ごとの辞書を 1 つのフォルダーにまとめ、そのフォルダー名をマニフェストに書きます。
"name": "${meta.name}",
"locales": "locale",
"panels": [{ "id": "main", "title": "${panel.title}", "entry": "main.tsx" }]
my-plugin/
locale/
en.json { "meta": { "name": "Shape Spawner" }, "panel": { "title": "Shapes" } }
de.json { "meta": { "name": "Formen-Werkzeug" }, "panel": { "title": "Formen" } }
name、description、パネルとコンポーネントのタイトルには、${…}のテンプレートを使えます。値は辞書から読み込まれます。- パネルでは、
@was/editor-apiのuseEditorLocale()かeditor.i18n()で、エディタの言語に合わせたt(key, vars)を使えます。言語が切り替わると、表示も更新されます。 - 辞書のない言語では英語が使われます。英語もなければ、プラグインにあるいずれかの言語が使われます。
- 公開すると、ライブラリには言語ごとの名前と説明が表示されます。
設定を記憶する
パネルでは localStorage を使えません。次に開いたときのために何かを覚えておくには、editor.storage を使います。
await editor.storage.set('lastShape', 'sphere');
const shape = await editor.storage.get('lastShape');
editor.storage.subscribe((values) => render(values));
ほかに getAll()、delete(key)、clear() もあります。値は JSON として、その人とそのプラグイン専用に保存されます。上限は 1 プラグインあたり 256 KB、1 人のすべてのプラグインを合わせて 1 MB です。プロジェクトの全員に見せたい値には、プロジェクトルームを使ってください。
通知と確認
editor.notify('Scene generated');
editor.notify({ message: 'Could not load the model', kind: 'error' });
const ok = await editor.confirm({ title: 'Delete 12 objects?', danger: true });
- 通知には、
detail、kind(success、warning、error)、durationも指定できます。 confirmはtrueかfalseを返します。外側をクリックすると、「いいえ」として扱われます。- どちらにも、プラグインの名前が表示されます。画面に残る通知は 3 つまでで、答えを待てる確認は 1 つだけです。
alert()やconfirm()の代わりに使ってください。パネルではこれらを表示できません。
カメラ
const view = await editor.viewport.camera.get();
await editor.viewport.camera.set({ position: [0, 2, 5], target: [0, 0, 0] });
await editor.viewport.camera.focus([entityId]);
setは、近い視点にはなめらかに移動し、遠い視点には一気に切り替えます。常に一気に切り替えるには、animate: falseを渡します。focusは、1 つまたは複数のオブジェクトを画面に収めます。表示するものがないときはfalseを返します。- 2D では、
setはパンとズームだけを行います。 - これには
editor:viewport権限が必要です。
元に戻す
プラグインがシーンに加えた変更は、その人自身の変更と同じように元に戻せます。0.3 秒未満の間隔で続いた変更は、1 つのステップにまとまります。
editor.history.transaction(fn)は、fnが行う変更をすべて 1 つのステップにまとめます。fnが非同期でも同じです。editor.history.skip(fn)は、変更を履歴に残しません。プラグインがシーンから計算して求めるデータに使います。そうしないと、元に戻す操作でそのデータが消え、プラグインがまたそれを書き込むので、その人はいつまでも自分の編集まで戻れません。
メニュー項目とショートカット
actions は、Layers とアセット一覧の行の右クリックメニューに項目を追加します。
"actions": [
{ "id": "check", "title": "Check the scene", "panel": "main",
"targets": ["entity"], "shortcut": "Mod+Alt+KeyD" }
]
editor.actions.on('check', ({ target, ids }) => {
// 右クリックされたもの、または選択されているもの
});
targetsは、レイヤーの行ならentity、アセットの行ならresourceです。1 つのプラグインに追加できるアクションは 20 個までです。- クリックすると、
panelで指定したパネルが閉じていれば開き、そこでハンドラーが呼び出されます。 shortcutにはMod、Alt、またはその両方をMod+Alt+Shiftの順で含め、そのあとにKeyDのようなキーのコードを書きます。ショートカットは、いま選択しているものに対して実行されます。エディタ自身のショートカットが優先され、Mod+KeyWのようにブラウザが使うキーは受け付けられません。
コマンド
commands を使うと、ほかのプラグインやアシスタントから、あなたのプラグインを呼び出せます。
"commands": [
{ "name": "diagnose", "description": "Lists the problems in the scene", "panel": "main" }
]
editor.commands.handle('diagnose', async (input, { caller }) => {
return { problems: await findProblems() };
});
- コマンドの名前は
plugin.<plugin id>.<name>になります。ほかのプラグインは、editor.commands.list()でコマンドを見つけます。1 つのプラグインに追加できるコマンドは 20 個までで、名前は camelCase にします。 - JSON を返すか、エラーを投げてください。入力は自分でチェックしてください。代わりにチェックしてくれる仕組みはありません。
- パネルが閉じている場合は、先にパネルが開きます。ただし、その人がすでに許可したプラグインに限ります。
permissionsには、呼び出す側のプラグインが持っている必要のある権限を書きます。指定しない場合、呼び出す側には、あなたのプラグインのすべての権限が必要です。destructive: trueを指定すると、誰が呼び出す場合でも、呼び出しのたびにまずその人が確認します。
公開する
公開フォームでは、次の項目を入力します。
| 項目 | 制限 |
|---|---|
| Icon | 好きな画像からつくられ、256×256 の正方形に切り抜かれます |
| Name | — |
| Tags | 12 個まで |
| Description | 500 文字まで |
| Screenshots | 4 枚まで |
| Access | Free、Premium、Subscribers only のいずれか。評価と有料プラグインを参照 |
プラグインにロケールファイルがある場合、名前と説明は、言語ごとにそこから読み込まれます。
そのほかの制限:1 プラグイン 64 ファイルまで、1 ファイル 512 KB まで、1 アカウント 50 プラグインまで。 画像などのバイナリファイルは、それぞれ約 384 KB 未満にする必要があります。
もう一度公開すると、同じプラグインが更新されます。公開したプラグインを編集するには、ダウンロードします。Studio が、選んだフォルダーにそのファイルを書き出します。
公開したプラグインのファイルは普通に配信されるので、./icon.png のようなパスが使えます。ローカルのフォルダーで読み込まれるのは、いちばん上の階層にある .ts、.tsx、.js、.jsx、.json のファイルと、マニフェストでエントリーに指定した .html ファイルだけです。画像、スタイル、サブフォルダー内のファイルは、公開してはじめて表示されます。
権限
プラグインに必要な権限を、マニフェストに列挙します。scene:read、scene:write、resources:write、spaces:write、collaboration:read、collaboration:write、editor:panels などがあります。サーバーは、知らない権限を拒否します。
Studio は、これらの権限のほとんどを制限していません。どのプラグインもそれらの権限を持ちます。例外は 2 つです。パネルが world を受け取れるのは、マニフェストに scene:read がある場合だけです。プロジェクトのデータ(editor.data)は、マニフェストに data:read か data:write があるプラグインにだけ応答します。
レビュー担当者はあなたの権限を読みますし、利用者も権限を見てプラグインを判断します。目的を果たせる最小限の権限だけを求めてください。
ほかの人のプラグインをインストールするときは、プロジェクト全体にアクセスできるものと考えてください。信頼できる理由があるものだけをインストールしましょう。信頼と検証を参照してください。
次へ: カスタムコンポーネント — オブジェクトに独自のコンポーネントを追加します。