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

カスタムコンポーネント

プラグインは、オブジェクトに独自のコンポーネントを追加できます。そのインターフェースは、インスペクターの Transform や Material と並んで表示されます。

ツールが 1 つのオブジェクトに属するときに使いましょう。たとえば、オブジェクトへのメモ、独自のコントロール、つくったものと一緒に残るジェネレーターなどです。このようなプラグインには、パネルは要りません。

コンポーネントを宣言する​

マニフェストの components に列挙します。panels は空でかまいません。

{
"id": "entity-notes",
"entry": "main.tsx",
"panels": [],
"components": [{ "id": "notes", "title": "Notes", "entry": "main.tsx" }]
}

コンポーネントを書く​

import {
Button,
Column,
Text,
defineProps,
useEntity,
useSignals,
useSyncedState,
} from '@was/extension';
import { TransformComponent } from '@was/engine';

const useProps = defineProps({ step: { type: 'number', default: 0.5 } });

export default function Notes() {
useSignals(); // ここで読んだシーンのデータが変わったら再描画する
const props = useProps(); // インスペクターのフォームの値
const entity = useEntity(); // このコンポーネントがついているオブジェクト
const [note, setNote] = useSyncedState('note', '');

return (
<Column gap={2}>
<Text muted>{note}</Text>
<Button
onClick={() =>
setNote(`y=${entity.getComponent(TransformComponent)?.get('position').y}`)
}
>
Remember
</Button>
</Column>
);
}

おもな働きをするのは、次のフックです。

フック得られるもの
useEntity()コンポーネントがついているオブジェクト
useWorld()そのオブジェクトがあるシーン
useProps()defineProps で宣言したフォームの値
useSignals()描画中に読んだシーンのデータが変わったときの再描画
useSyncedState()オブジェクトに保存され、ほかのコンポーネントと同じように同期されるステート
useSelected()オブジェクトがエディタで選択されているかどうか
useViewportPointer()ビューポートでのマウスイベント。ポインターの下にあるオブジェクトと位置もわかります

defineProps では、スクリプトのプロパティと同じフィールドの型が使えます。数値、文字列、真偽値、色、選択肢、エンティティ、シーン、スペース、リソース、レコード、配列、グループです。そのため、何もしなくても、インスペクターにきちんとしたフォームが表示されます。

コンポーネントから値を変更する​

props.$set(patch) は、インスペクターと同じように、コンポーネント自身のフィールドを変更します。変更は全員に同期され、元に戻すこともできます。値は defineProps に照らしてチェックされます。数値は min と max の範囲に収まり、宣言していないフィールドを指定するとエラーになります。ビューポートでドラッグして値を変えるときに使います。

<Handle axis={[0, 1, 0]} onDrag={(p) => props.$set({ height: p[1] })} />

$ で始まるフィールド名はこのために予約されているので、defineProps では使えません。

ステートを宣言する​

defineState は、defineProps がフォームを宣言するのと同じように、コンポーネントのステートをデフォルト値つきで宣言します。

const useScore = defineState({ points: { type: 'number', default: 0 } });

const [score, setScore] = useScore(); // コンポーネントの中で

スクリプトは、このステートを型つきで、同じデフォルト値のまま読めます。

翻訳する​

@was/extension の i18n() か useEditorLocale() を使うと、プラグインのロケールファイルをもとにした t(key, vars) を、エディタの言語で使えます。

動作のしくみ​

コンポーネントは本物の React 19 です。フック、useEffect、コンテキスト、Suspense は、どれもいつもどおりに動きます。

エディタは、あなたのコンポーネントを自分の要素で描画します。コンポーネントがエディタの一部のように見えるのはそのためです。

ここから、2 つのことが言えます。

  • 組み立てには、用意された要素しか使えません。 任意の HTML は使えません。インスペクターでは Column、Row、Text、Button、Divider、InputText、InputNumber、InputColor、Slider、Checkbox、Select を使います。ビューポートでは Overlay、Gizmo、Group、Box、Sphere、Line、Handle、Control を使います。Gizmo や Overlay に when="selected" を指定すると、選択したオブジェクトにだけ表示されます。
  • イベントハンドラーはいつもどおりに書けます。 onClick は普通に動きます。できないのは、DOM イベントをどこかに渡すことだけです。

コンポーネントの追加方法​

コンポーネントは 2 つの方法で追加できます。

追加する場所結果
+ new component の Extensions グループすでに選択しているオブジェクトに追加されます
ツールバーの + の下、Plugins グループコンポーネントつきの新しいオブジェクトがつくられ、選択されます

ツールバーの Plugins という見出しがあるので、見慣れないボタンがどこから来たのかがわかります。

パソコンから接続したプラグインのフォルダーも、パネルがあればツールバーのそのリストに表示されます。毎回 Plugins パネルを経由しなくても開けます。オーバーレイしかないフォルダーは、開くものがないので表示されません。

スクリプトと公開したプロジェクトで使う​

コンポーネントのインターフェースは、エディタの中でだけ動きます。値はオブジェクトに保存されるので、プロジェクトの再生中にも使えます。

  • スクリプトは、ctx.plugin('<plugin id>/<component id>') でコンポーネントを読み書きします → ctx API
  • 公開したプロジェクトで動くプラグイン自身のコードも、同じことができます → ランタイムコード

信頼​

クリックしなくても起動します

カスタムコンポーネントは、それを含むプロジェクトを誰かが開くたびに起動します。そのため、信頼と検証で説明している、より厳しいチェックを受けます。


次へ: ランタイムコード — プラグインを公開したプロジェクトで動かします。