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

プラグインをつくる

Studio のプラグインを自分でつくります。フォルダーの作成、パネルの作成、シーンの操作、公開までを説明します。

プラグインは、パソコン上の 1 つのフォルダーです。いつも使っているコードエディタで編集し、Studio はビルドなしでそれを動かします。できあがったら公開します。

公開したプロジェクトで動くコードについては、ランタイムコードを参照してください。

プラグインを作成する​

  1. Plugins パネルを開きます。
  2. Create plugin を押します。
  3. フォルダーを選びます。Studio がそこにスターターテンプレートを書き込みます。
my-plugin/
meta.json マニフェスト
main.html パネルのマークアップ
main.js あなたのコード。.ts、.tsx、.jsx でも動きます
plugin.d.ts API 全体の型(自動生成)
tsconfig.json IDE がその型を解決するための設定
.wasignore 公開時にアップロードしないもの

変更を確認するには、ファイルを保存して Reload を押します。バンドルやインストールは要りません。

TypeScript と JSX はそのまま動きます

.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イベントパネルの項目 → トリガーとステップ
settingsProject 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—
Tags12 個まで
Description500 文字まで
Screenshots4 枚まで
AccessFree、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 があるプラグインにだけ応答します。

必要な権限だけを求める

レビュー担当者はあなたの権限を読みますし、利用者も権限を見てプラグインを判断します。目的を果たせる最小限の権限だけを求めてください。

ほかの人のプラグインをインストールするときは、プロジェクト全体にアクセスできるものと考えてください。信頼できる理由があるものだけをインストールしましょう。信頼と検証を参照してください。


次へ: カスタムコンポーネント — オブジェクトに独自のコンポーネントを追加します。