カスタムコンポーネント
プラグインは、オブジェクトに独自のコンポーネントを追加できます。そのインターフェースは、インスペクターの 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 - 公開したプロジェクトで動くプラグイン自身のコードも、同じことができます → ランタイムコード
信頼
カスタムコンポーネントは、それを含むプロジェクトを誰かが開くたびに起動します。そのため、信頼と検証で説明している、より厳しいチェックを受けます。
次へ: ランタイムコード — プラグインを公開したプロジェクトで動かします。