build-ui について
build-ui は、Decentraland SDK7 と React-ECS で 2D UI を作るためのスキルです。Creator Hub の UI Designer で編集できる書き方を基本とし、表現できない場合に自由形式の UI を使います。
主な機能
- 01編集可能な React-ECS を優先
- 02Creator Hub UI Designer との互換性
- 03コード形式・ハイブリッドパネルの指針
- 04コンポーネントとアグリゲーターの規約
- 05エディターの解析制約と自己確認項目
活用シーン
- 01HUD、メニュー、ダイアログ、通知の作成
- 02Creator Hub でスタイルを調整できる UI の作成
- 03エディターの規約を超えるデータ駆動パネルへの対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill build-uiインストール後、Claude Code で/build-uiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/adapting-coded-ui.mdcoded-ui.mdcomponent-template.mddrag-slider.mddriver-pattern.mdeditor-behavior.mdinteraction-helper.mdui-components.md詳細
React-ECS で UI を構築する
Decentraland SDK7 では、React に似た JSX システム(@dcl/sdk/react-ecs)を使って 2D オーバーレイを描画します。このコードの書き方は 2 通りあり、デフォルトは編集可能な方法です。
| モード | 内容 | 用途 |
|---|---|---|
| 編集可能(デフォルト) | Creator Hub の 2D UI エディター(UI Designer)が解析し、キャンバスにレンダリングして書き戻せる仕様で記述した React-ECS:コンポーネントごとに src/ui/<Component>.tsx を 1 つ作成、state/props バインディング、useInteraction レイヤー、@ui-action ハンドラー、src/ui/ の外部にドライバーを置いてアニメーションを制御 | HUD、メニュー、ダイアログ、トースト、通知、スコアボード、設定パネル、ヘルスバー、タイマー、スライダーなど、デザイナーが後からコードに触れずにスタイルを変更する可能性のあるすべての UI |
| コード記述(例外的なケース) | 自由形式の React-ECS:データに対する .map()、条件付きサブツリー、計算されたスタイル、ヘルパーコンポーネント、あらゆるファイル配置 | UI が本質的にデータ駆動である場合、または仕様では表現できない形で動的な場合のみ —「モードの選択」を参照。詳細:{baseDir}/references/coded-ui.md |
どちらも通常の React-ECS であり、どこでも動作します。編集可能な仕様はその サブセット です。ユーザーに希望するモードを尋ねないでください。UI がコード記述専用の条件に該当しない限り、編集可能な方法を選び、該当する場合はその旨を伝えてください。
UI アプローチの使い分け
| 必要なもの | アプローチ | 参照先 |
|---|---|---|
| 画面空間に配置する HUD、メニュー、ボタン、入力 | React-ECS(このスキル) | 以下 |
| ワールド内に浮かぶ 3D テキスト | TextShape + Billboard | advanced-rendering |
| Web ページを開く | openExternalUrl | scene-runtime |
| 3D 空間内でクリック可能なオブジェクト | ポインターイベント | add-interactivity |
| InputActions を発火する画面上のボタン(モバイル) | UiEntity の uiInputBinding + TouchScreenControls | {baseDir}/references/ui-components.md、advanced-input |
モードの選択
デフォルトでは編集可能な方法を選んでください。エディターに表現方法がない以下のいずれかが必要な場合に限り、UI全体、またはそのパネル 1 つだけをコード記述にします。
- 実行時のコレクション(プレイヤー、インベントリアイテム、チャットメッセージに対する
.map())から作成される行で、作成時に件数が不明なもの。最大でも数行程度の固定行数であれば、手作業で展開して編集可能なままにできます。 - 呼び出しごとに内容を自由に指定するもの:呼び出しごとに異なる 構造 や、代替テクスチャの セット をレンダリングする関数(N 個の画像を受け取る
showPopup(title, body, imageSrc)など)。値ごとにテキストスロットを 1 つ、画像ごとにtexture.srcを 1 つ用意する方法であれば、編集可能です。 - 子要素/スロットを受け取る汎用ラッパー(
<Card>{children}</Card>など) — コンポーネント参照は子要素を受け取りません。 - 行ごとのドラフトオブジェクト、行ごとのクロージャ、非制御入力の再マウントによる回避策。
動的に見えるものの大半 — 塗りつぶしバー、タイマー、イージングを使った動き、自動非表示、キャンバスに応じた px サイズ、スピナー — は、ドライバーが管理する状態変数で実現でき、編集可能なままです。
コード記述が必要なパネルが 1 つだけの場合は、他の部分は編集可能なままにし、src/ui/ の外部にあるモジュールから ReactEcsRenderer.addUiRenderer を使ってコード記述のパネルを別途マウントしてください({baseDir}/references/coded-ui.md → ハイブリッドシーン)。どの部分が編集できないか、またその理由をユーザーに伝えてください。既存のコード記述 UI を変更する場合、結果は大きくなることが予想されます(ループやバリエーションを展開することが編集可能にするための代償です)。中途半端に移植するのではなく、そのトレードオフを伝えてください。
前提条件: ScreenInsetArea/InteractableArea とデバイスごとのデフォルト仮想画面を提供するバージョンである @dcl/sdk 7.26.0+ が必要です。UI エディターは Creator Hub(0.50.0+)で安定しており、常に有効です。Settings > Experimental の切り替えも、settings.guiEditor キーもありません。有効化するようユーザーに案内しないでください。7.26.0 より前のバージョンでは、2D モードに入るとキャンバスの代わりに SDK のアップグレードを促す通知と Update SDK ボタンが表示されます。
エディターでのコードの読み取り方法
エディターには独自の保存形式はありません。src/ui/ 直下にあるシーンの実際の .tsx ファイルが、そのままドキュメントです。ファイルをノードツリーに解析し、キャンバスに描画して、視覚的な編集内容を最小限のテキスト差分として即時に書き戻します(バッジには「すべての変更を保存しました」と表示されます。手動での保存はありません)。1 秒間隔のディスク監視により外部の編集も反映されるため、エディターを開いている間はファイルが常に最新の状態になります。したがって「編集可能かどうか」はコードを確認すれば分かる性質です。パーサーが静的に理解できないコードは、エラーを表示せずに機能が制限されます。
| 制限 | 条件 | 結果 |
|---|---|---|
| 固定ノード | uiTransform / uiBackground の値、または Label / Input / Dropdown の prop が、リテラルでも単純な state.x / props.x 参照でもない場合 | ノードはキャンバスに描画されますが、パネルでは一切編集できません |
| 不透明ノード | 不明な要素名、useInteraction の const 以外のスプレッド、条件式 / 論理式 / .map() を使った子要素、JSX コメント | グレーの読み取り専用ブロックになります。その子要素は走査されないため、サブツリーはキャンバスに表示されません |
以下の規約に沿って記述すれば、どちらも発生しません。コードを書く際に考慮すべきエディターの仕様は次のとおりです。
- **レイアウトは互いに独立した 2 つの軸で構成されます。**パネルの Flow(row / column / Free)が書き込むのは
flexDirectionだけです。Free はキーが存在しない状態を意味し、「free」という値ではありません。Ignore Layout Flow はpositionType: 'absolute'を書き込みます。ルートは常に absolute です。Free の親の下に子をドロップすると、ドロップ位置に absolute として初期設定されます。親を Free に切り替えると、既存の子はすべて、計測済みの位置にまとめて固定されます。 - Full Screen ウィジェットのプリセットは、
width/heightを指定せずにuiTransform={{ flexGrow: 1, alignSelf: 'stretch' }}を挿入します(100%は読み込み時に Fill ではなく Percent 単位として扱われ、100%の兄弟要素が 2 つあると、Yoga の空き領域からはみ出します)。Free の親の下では、positionType: 'absolute'とtop/right/bottom/left: 0を使用します。エディターで編集する手書きの全画面ラッパーにも、同じ組み合わせを使用してください。100%×100%のリテラル形式も解析できます。これは生成された aggregator ルートと、ゲーム内で検証済みのテンプレートで使われています。 - Bevy レンダラーでは、2D モードを開くとシーンが一時停止し(3D に戻ると再開します)。ユーザーが UI をレイアウトしている間、ドライバーのクロックは進みません。
エディター UI のその他の仕様(Scene Inset のドロップダウン、Opacity の名称、キャンバスツール、モードの保持、モバイルプレビューのガイド領域とその制限)については、{baseDir}/references/editor-behavior.md を参照してください。
セットアップ(編集可能)
ファイル構成
src/ui/
index.tsx <- GENERATED aggregator. Never hand-edit.
interaction.tsx <- reserved helper (useInteraction). Verbatim.
platform.tsx <- reserved helper (usePlatform). Verbatim.
MyHud.tsx <- a top-level UI root (rendered by the aggregator)
KitToast.tsx <- a reusable component (marked /** @ui-component */)
src/ui-behaviors.ts <- driver: outside src/ui/, never parsed by the editor
src/mobile-hud.ts <- GENERATED by the MobileHUD panel. Editor-owned. Never hand-edit.
新しい UI の手順:
- 1 ファイルにつき 1 コンポーネントとし、
src/ui/<ComponentName>.tsxに配置します。ベース名は、有効な PascalCase の識別子で、エクスポートされる関数名と同じである必要があります。それ以外のファイルはエディターに無視されます。1 ファイルにつきエクスポートするコンポーネントは 1 つだけにしてください(エディターは JSX を返す最初のエクスポート関数を読み込みます)。すべてのファイルの先頭に/** @jsx ReactEcs.createElement */を記述し、@dcl/sdk/react-ecsからReactEcsをインポートします。コメントには//のみを使用してください —{/* JSX comments */}は認識されません。 src/ui/interaction.tsxと(プラットフォームごとのバリエーションでは)src/ui/platform.tsxを、{baseDir}/references/interaction-helper.mdからそのまま作成します。小文字の名前にすることでルート一覧に含まれなくなります。エディターがこれらを自動生成し、UI として一覧表示することはありません。- 各コンポーネントを契約に従って記述します。
{baseDir}/references/component-template.mdをベースにしてください。 - 以下の生成済みの形と完全に一致するように
src/ui/index.tsxを記述し、src/index.tsのmain()からsetupUi()を呼び出します。(エディターがこの配線を自動で行います。テンプレートの//setupUi()行のコメントを解除し、インポートを追加します。ただし、手動で作成する場合は自分で行ってください。) - 時計、イージング、タイマー、フォーマッター、状態マシンはすべて
.tsファイルに記述し、src/ui/の外に配置します — ドライバーパターンを参照してください。 - すべてのファイルにセルフチェックリストを適用します。
従来の単一ファイル src/ui.tsx は、エディターがシーンを初めて開いたときに src/ui.tsx.bak にバックアップされ、削除されます — 必ず src/ui/ 以下に作成してください。既存のコード UI を移行するには、{baseDir}/references/adapting-coded-ui.md に従ってください(障害となる要素は次の順です:.map() → 展開する、条件分岐 → 非表示にする要素に active 表示ゲートを設定する、計算済みスタイル → ドライバー変数、補間テキスト → セグメントバインディング、手動で追跡するホバー → レイヤー、その後はすべての箇所に明示的なボックスを設定する)。
SDK テンプレートには、すでに tsconfig.json の JSX 設定が含まれています — 変更しないでください。
アグリゲーター(src/ui/index.tsx)
トップレベルのルート一覧から生成され、エディターでシーンを開いたとき、またはルートを追加、名前変更、削除、再インセットしたときに書き換えられます。手動編集した内容はすべて失われます。次の内容をそのまま出力してください。
/** @jsx ReactEcs.createElement */
import ReactEcs, { UiEntity, ReactEcsRenderer, ScreenInsetArea } from '@dcl/sdk/react-ecs'
import { MyHud } from './MyHud'
export function setupUi() {
ReactEcsRenderer.setUiRenderer(() => (
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
<ScreenInsetArea>
<MyHud />
</ScreenInsetArea>
</UiEntity>
))
}
画面インセットはルートごとに選択します(エディターの Scene Inset ドロップダウン)。デフォルトは device です。Creator Hub のアグリゲーター生成コード(UIDesigner/code/aggregator.ts)と @dcl/react-ecs の system.ts で確認済みです。
| エディターのインセット | index.tsx のラッパー | 用途 |
|---|---|---|
device(デフォルト) | <ScreenInsetArea> | 通常のシーン UI — デバイスのセーフエリア(ノッチ、ステータスバー、ホームインジケーター、角丸)内に収めます。デスクトップのインセットはゼロです |
interactable | <InteractableArea> | エクスプローラー独自の画面上のコントロール(ミニマップ、チャット、左側のコントロール)も避ける必要がある UI |
none | 裸の <Component /> | レターボックスのバー、全画面の背景、プラットフォーム UI が配置される場所への意図的な描画 |
この生成構造について、次の 2 点に注意してください。
setUiRendererはオプションなしで呼び出されるため、シーンには SDK のプラットフォーム既定の仮想キャンバス(デスクトップは1920x1080、モバイルは1600x720)と、レンダラーのscreenInsetの既定値'device'が適用されます。virtualWidth/virtualHeightを手動で追加しないでください(再生成時に削除されます)。これら 2 つのキャンバスサイズを基準に px 値を設計してください。コード UI のルールである「仮想サイズを常に明示的に渡す」を守れないのは、ここだけです。- レンダラーは
screenInsetが省略された場合、すでに UI 全体を独自のScreenInsetAreaでラップしており、アグリゲーターのラッパーはその内側にネストします。絶対位置指定は親ボックスを基準にするため、インセットがゼロでないスマートフォンでは、deviceのルートは 2 重にインセットされ、interactableのルートにはデバイスとインタラクタブルの両方のインセットが適用され、noneのルートにもキャンバス全体ではなくレンダラーレベルのデバイスインセットが 1 回適用されます。デスクトップではインセットがゼロなので、何も変わりません。これは@dcl/react-ecsのwrapWithScreenInsetと Creator Hub のaggregator.tsを読んで確認したもので、デバイス上での実測は行っていません。index.tsxで「修正」しないでください(再生成されます)。モバイルでセーフエリアに正確に配置する必要がある場合は、そのルートにnoneを選択し、実測してください。
どちらのラッパー要素も index.tsx のみに配置してください。コンポーネントファイル内では、パーサーがこれらの名前を認識できず、サブツリーが不透明になります。
要素
モデル化される要素名は UiEntity、Label、Input、Dropdown、Button の5つだけです。加えて、別の src/ui/ ファイルへの参照(<KitToast />)も、正式に認められた再利用単位です。それ以外の要素名(ローカルのヘルパーコンポーネント、ライブラリコンポーネント、ScreenInsetArea)は不透明です。すべてのプロパティ表:{baseDir}/references/ui-components.md。
UiEntity— コンテナーです。uiTransform(width、height、min/max、positionType、position、display、flexDirection、justifyContent、alignItems、alignContent、alignSelf、flexWrap、flexGrow、overflow、padding、margin、opacity、zIndex、borderWidth/Color/Radius、pointerFilter)、uiBackground(color、texture、textureMode、textureSlices、uvs、avatarTexture)、uiInputBinding、および4つのリスナーonMouseDown/onMouseUp/onMouseEnter/onMouseLeaveを指定できます。Label/Button— テキストを持てるのはこの2つだけです。value、fontSize、textAlign、color、font、textWrapを指定できます。Buttonではさらにvariant('primary'|'secondary')、disabled、onMouseUpを指定できます。UiEntityのuiText={{…}}はモデル化されません。代わりにLabelの子として構成してください(コードで記述する UI では引き続き使用できます)。valueは<b>のようなシンプルなマークアップを受け付けます。Input—placeholder、value、color、placeholderColor、disabled、textAlign、font、fontSize、onChange、onSubmitを指定できます。React の制御コンポーネントではありません。「注意点」を参照してください。Dropdown—acceptEmpty、emptyLabel、options(string[])、selectedIndex、disabled、color、textAlign、font、fontSize、onChange(index)を指定できます。- コンポーネント参照
<KitToast … />— 選択や移動ができ、インスタンスごとにプロパティを編集できます。外側から移動やサイズ変更はできません。各インスタンスに、明示的なwidth/heightと、マージン/位置を指定したラッパーUiEntityを用意してください。ネストした JSX の子要素は受け付けません。
4つのリスナーが UI イベントハンドラーのすべてです。いずれも () => void です。onMouseDrag / onMouseMove はなく、イベントオブジェクトもポインター座標もありません。また、すべて InputAction.IA_POINTER に固定されています。ドラッグ操作は PrimaryPointerInfo.screenDelta を使って実装できます — {baseDir}/references/drag-slider.md。
契約
State:バインディング対象
export interface State {
score: number
label: string
visible: boolean
panelWidth: number
labelColor: { r: number; g: number; b: number; a: number }
options: string[]
}
export const state: State = {
score: 0, label: '00.00', visible: false, panelWidth: 320,
labelColor: { r: 1, g: 1, b: 1, a: 1 }, options: ['A', 'B'],
}
モジュールレベルの export interface State と export const state: State が認識されるシグネチャです。各プロパティは編集可能な変数です。型:number、string、boolean、Color4(r / g / b のメンバーを持つことで照合されるため、名前を Color4 とせず、{ r; g; b; a } のように構造的に注釈します)、string[]。number[](例:uvs の4点組)もバインドされて動作しますが、パネルでは文字列リストとして誤表示されます。
stateはエクスポートされた通常のオブジェクトです。これにより、ドライバーから変更できます。- モジュールの状態は、ファイルのすべてのインスタンスで共有されます。インスタンスごとの値は親に保持され、props として渡されます。
- シーン UI では React フック(
useState、useEffect)を使いません。状態はモジュールレベルにあります。UI は毎フレーム再レンダリングされるため、変更はすぐに反映されます。
スタイルのバインディング
uiTransform / uiBackground のキーで、その値が参照のみ(state.x や props.x のようなもの。演算子、関数呼び出し、連結を含まないもの)であれば、編集可能なバインディングです。
uiTransform={{ width: state.panelWidth, position: { top: state.panelTop }, borderColor: state.frameColor }}
uiBackground={{ color: state.panelColor, texture: { src: state.iconSrc } }}
認識される位置:トップレベルのキー、エッジグループのメンバー(position / margin / padding → { left: state.x })、グループ全体(borderColor: state.c)、およびドット区切りのパス texture.src / avatarTexture.userId です。リテラルとバインドされた値は自由に混在でき、ネストは react-ecs と同様に1階層までです。要素の props も同じ方法でバインドされます:value={state.label}、color={state.labelColor}、fontSize={state.size}、selectedIndex={state.i}、options={state.options}。
混在テキスト
補間がすべて参照のみであるテンプレートリテラルは、順序付きのリテラル/バインディングセグメントとして往復変換されます。
<Label value={`Score: <b>${state.score}</b>`} … />
<Label value={`${state.mins}:${state.secs}`} … />
<Label value={`env: ${state.realm}\nplayers: ${state.count}`} … />
計算を含む補間(${state.a + 1}、${fmt(state.t)})が1つでもあると、そのノードは固定されます。ドライバー側で書式設定し、完成した値を補間してください。(実際の Label には、明示的な uiTransform ボックスも必要です。「サイズ設定とモバイル」を参照してください。)
アクション(イベントハンドラー)
type UiAction = { state: State; props: Parameters<typeof MyHud>[0]; value?: unknown }
/** @ui-action */
function openPanel({ state }: UiAction) {
state.visible = true
}
標準の thunk onMouseDown={() => openPanel({ state, props })} で接続します。値を伴うイベントには、onChange={(value) => setName({ state, props, value })} を使用します。認識されるのは、onMouseDown、onMouseUp、onMouseEnter、onMouseLeave、および Input/Dropdown の onChange/onSubmit です。ハンドラーの本体には任意のコードを記述できます。認識されないハンドラー式(ブロック本体を持つインラインのアロー関数)は、バインド済みとして表示されないだけで、ノードがフリーズすることはありません。アクションは状態を同期的に変更します。時間に依存する処理は、ドライバーが次のフレームで取得するフラグにします。
インタラクションレイヤー
useInteraction は、状態ごとのスタイル設定で認識される構文です。レイヤーは base → active → hover → press の優先順位でディープマージされます。第 2 引数は active を制御し、任意の式を指定できます(そのまま保存されます)。
const panel = useInteraction(
{
base: { uiTransform: { display: 'flex', width: 320, height: 200 } },
active: { uiTransform: { display: 'none' } },
},
state.visible !== true,
)
return <UiEntity {...panel}>…</UiEntity>
- 表示状態は常に
activeの表示ゲートで制御します。{state.visible && <X/>}(不透明)やdisplay: state.visible ? 'flex' : 'none'(フリーズ)は使用しないでください。要素はアンマウントせず、非表示にします。キャンバスではbaseレイヤーが描画されるため、編集時はすべての状態が重なって表示されます。これは想定どおりです。 - ホバー/押下時のフィードバックは常に
hover/pressレイヤーで表現し、onMouseEnter/onMouseLeaveで真偽値を手動管理しないでください。 {...someInteractionConst}は、パーサーが受け付ける唯一のスプレッドです。追加の属性はその隣に記述でき(<UiEntity {...panel} onMouseDown={…}>)、スプレッドより優先されます。- スプレッドには、4 つのポインターリスナーが常にすべて含まれます。
baseとactiveの表示ゲートだけを使う場合も同様です。ゲートはパネルなど、判定が適用される最小の要素に設定してください。全画面のラッパーには設定しないでください。詳しくは ポインターのブロックを参照してください。 - 終了時にアニメーションする UI では、変数を 2 つ使います。
visible(アクションで切り替える意図)とhidden(終了アニメーション後にのみドライバーが設定する描画ゲート)です。state.hidden === trueをゲートに指定してください。
コンポーネントの props
/** @ui-component */
export function KitToast(props: { message?: string; fillPx?: number; on?: boolean; onClose?: (value?: unknown) => void }) {
- エクスポートされた関数の前にマーカーを付けると、そのファイルは再利用可能なコンポーネントになります(別のルートにネストされた場所でのみ描画されます)。マーカーがない場合、そのファイルはトップレベルのルートになり、アグリゲーターによって描画されます。
- props は、唯一の
propsパラメーターに指定するインラインのオブジェクト型です。対応する型はnumber、string、boolean、コールバック(value?: unknown) => voidです。それ以外は読み取り専用として表示されます。props は必ず optional として宣言してください。 - コンポーネント内では、
props.xをバインディングの対象として使用できます。スタイルキー、テキスト値、active式(props.active === true)に指定できます。 - 色、テクスチャセット、children の props には対応していません。色を選択する
variantprop は表現できません。すべての視覚的なバリエーションを、active表示レイヤーで制御する兄弟要素として展開してください({baseDir}/references/component-template.md§6)。 value={props.label}は厳密な TS ではエラーになります(string | undefined)。value={`${props.label}`}を使用してください。これは引き続き認識されるバインディングであり、親から prop が渡されない場合はundefinedが描画されます。- 子のコールバックは、1 行のアクションで上位へ渡します。
/** @ui-action */ function forwardClose({ props }: UiAction) { props.onClose?.() }。
プラットフォーム別のバリエーション — 唯一の構造的な条件分岐
const platform = usePlatform()
return platform === 'mobile' ? <PhoneMenu /> : <DesktopBar />
コンポーネントの return と JSX の子要素として認識されます。!==、オペランドを逆にした式、インラインの usePlatform() === 'mobile' はいずれもパースされます。両方の分岐は、単一の JSX 要素またはリテラルの null である必要があり、少なくとも一方は要素でなければなりません。予約済みの src/ui/platform.tsx がこの機能を提供します。デバイスごとに構造を変える場合(サイドレールではなくボトムシートを使うなど)に使用してください。プロパティごとの上書きはモデル化されていません。仮想キャンバスはすでに比例拡大・縮小に対応しています。
表現できないこと
| 表現できないこと | 代替手段 |
|---|---|
データに対するループ/.map() | 各要素を手動ですべて展開します。約3回を超えて繰り返す行は、それ専用の @ui-component ファイルに切り出します |
共有テーマ定数(color: THEME.primary) | 各箇所に { r, g, b, a } リテラルをインラインで記述します。スタイルオブジェクト内の識別子はノードを固定するため、パレットの変更は検索と置換で行います |
計算されるスタイル値(算術演算、Math.*、関数呼び出し、連結、三項演算子) | 算出値ごとに、ドライバーが管理する状態変数を1つ用意します |
パーセント文字列のバインディング(width: state.pct + '%') | px 数値をバインドします。静的なパーセントのリテラル(width: '90%')は使用できます |
条件付きの要素プロパティ(disabled={state.i === -1}) | 事前に計算したブール値の状態変数を使います |
| 同じファイル内のローカルヘルパーコンポーネント | /** @ui-component */ を付けた別の src/ui/ ファイルに切り出します |
| コンポーネントの子要素/スロット | レイアウトをスクリーンファイル内にインラインで記述し、リーフウィジェットだけを切り出します |
| プロパティとして渡す色またはテクスチャセット | コンポーネント内でバリエーションを展開します(texture.src は文字列プロパティにバインドできますが、uvs はできません) |
| 行ごとの下書きとクロージャを持つデータ駆動の行 | 移植できません。src/ui/ の外にある独立したモジュールで、コード記述の UI にします |
ナインスライス背景(textureMode: 'nine-slices' と textureSlices)、リテラルの8浮動小数点値 uvs アトラスクロップ、overflow: 'scroll'(静的な行もスクロールします)、pointerFilter、ボーダーの各プロパティ、そしてフレックスモデル全体は往復変換できます。
ドライバーパターン
エディターは src/ui/ の外にあるファイルを解析しません。また、state はエクスポートされたプレーンオブジェクトです。構造、スタイル、その他の値はエディターが管理し、クロックと計算はドライバーが管理します。
// src/ui-behaviors.ts — outside src/ui/, invisible to the editor
import { engine } from '@dcl/sdk/ecs'
import { state as panel } from './ui/MyPanel'
const OPEN_WIDTH = 480
let anim = 0
export function registerUiBehaviors() {
engine.addSystem((dt: number) => {
const target = panel.visible ? 1 : 0
anim = Math.max(0, Math.min(1, anim + (target > anim ? dt : -dt) / 0.25))
const t = 1 - Math.pow(1 - anim, 3) // easeOutCubic
panel.panelWidth = Math.max(1, Math.round(OPEN_WIDTH * t))
panel.textColor.a = t
if (target === 0 && anim <= 0) panel.hidden = true // release the display gate
})
}
setupUi() と並べて main() から登録します。state 内のバインドされた変数の初期値は、それぞれデザイン可能な静止状態です。登録時に取得し、その値を基準にアニメーションさせることで、デザイナーはドライバーに触れずにパネルからスタイルを変更できます。ドライバーに含めるもの:クロック、トゥイーン、イージング、タイマー、自動非表示の期限(JS のグローバルではなく、@dcl/sdk/ecs の timers.setTimeout を使います)、padStart/丸め/テキスト書式設定、状態機械、算出値(パーセントからの px 値、UiCanvasInformation に基づくキャンバス対応サイズ)、ドラッグの累積値、TouchScreenControls の切り替え、ECS の読み取りが必要なものすべてです。アニメーション自体にはエディター上の表現がありません。エディターが認識するのは、バインドされたキーとその静止状態の値だけです。詳しい例:{baseDir}/references/driver-pattern.md。
よく使うウィジェット
すべてのウィジェットはプリミティブから作成します。ウィジェットライブラリはありません。まず編集可能なテンプレートを使い、必要に応じて {baseDir}/references/coded-ui.md のコード記述による代替手段を使ってください。
| 要望 | 編集可能なパターン |
|---|---|
| ダイアログ/プロンプト/確認画面 | 全画面位置指定のラッパー+表示を切り替えるパネル+ボタン:{baseDir}/references/component-template.md §4(承認/拒否用に2つ目のボタンを追加します) |
| トースト/通知/ポップアップ | visible プロパティを持つ KitToast コンポーネント(§2)。自動非表示にはドライバーのカウントダウンを使います(driver-pattern.md の *AutoHide) |
| 時限式のお知らせ/フラッシュメッセージ | Label を1つ含む、表示を切り替えるパネル。ドライバーが hideIn を減らし、visible を切り替えます |
| スコア/カウンター/タイマー表示 | 混合テキストの Label(§3)。ドライバーが文字列を整形します(driver-pattern.md §2) |
| 体力/進行状況/フィルバー | KitProgressBar(§5)。ドライバーが、リテラルのトラック幅に対するパーセント値から fillPx を設定します |
| メニュー/ホバー対応ボタン | hover/press レイヤー(§3) |
設定フォーム(Input/Dropdown) | value を指定した @ui-action |
| スライダー/スクラブバー/ドラッグハンドル | {baseDir}/references/drag-slider.md(タップで段階変更するゾーン+ドラッグ、表示を切り替えるリリースキャッチャー) |
| プレイを一時停止する全画面モーダル | 表示を切り替えるパネル+TouchScreenControls を切り替えるドライバー(配置を参照) |
| インベントリーグリッド/長さが不定のリスト | コード記述で実装します(coded-ui.md) |
サイズ指定とモバイル
すべてのボックスでサイズを明示します。 テキストの固有サイズや子要素に基づく自動サイズ調整は、このコードを描画する3つの環境では信頼できません。
| 環境 | 未設定の Label の寸法/自動サイズの親要素 |
|---|---|
| Bevy エクスプローラー | 描画されたテキストを測定し、その値をフレックスレイアウトに反映します — 正しく表示されます |
| Unity エクスプローラー | レイアウトへの寄与は約0です。グリフは高さ0のノード上に描画されるため、ラベル同士が重なり、親要素はパディングまで縮みます |
| Creator Hub キャンバス | 寸法が未設定のコンポーネントルートまたはrefラッパーは、実行時にYogaがレイアウトしていても、0として描画されます(インスタンスが縮み、パネルの高さが0と表示されます) |
実際のワールド内で並べたスクリーンショットにより確認済みです。高さを指定していないパネル内に、width: '100%'、textWrap="wrap" を指定し、height を指定していない2つのラベルを配置した720 pxのダイアログは、Bevyでは正しく表示されましたが、Unityでは潰れていました。ラベル同士が重なり、パネルはボタンとパディングの高さまで縮んでいました。修正方法は、パネルの height: 210、名前ラベルの height: 30、折り返し本文ラベルの height: 60、ボタンラベルの 100%/100% です。ある環境でプレビューが正しくても、ほかの環境で正しく表示される保証にはなりません。ルールは次のとおりです。
- すべての
/** @ui-component */ファイルのルートで、widthとheight(pxの数値またはパーセントリテラル)を宣言します。コンポーネント参照を囲むすべてのラッパーUiEntityでも、コンポーネントのルートサイズと一致するサイズを指定します。margin/positionしか持たないラッパーも、同じように縮みます。 - すべての
Label(およびButtonのテキスト)で、uiTransformのwidthとheightを宣言します。ラベルを縦に並べるすべてのコンテナーで、高さを明示します。折り返しテキストの場合、高さは行数 × 行の高さにします(fontSize: 20で2行なら60)。固定サイズの親要素を埋めるラベルには、100%/100%を指定します。 - バインドされたサイズまたは位置は、単純なpxの
numberにします。演算やパーセント文字列は使用しません。バインドされていないキーでは、静的なパーセントリテラルが可変レイアウトに適しています。 - デスクトップの
1920x1080とモバイルの1600x720を基準に設計します。モバイルキャンバスは高さが33%短いため、端にアンカーし、高さに応じて変化する軸には、特定の高さ向けに計算した絶対オフセットではなく、フレックス/パーセントを使います。7.26.0+では、従来の「モバイルではサイズを約3倍に拡大する」ルールを適用しないでください。スマートフォンではピクセルUIがすでに約2~3倍大きく表示されます(devicePixelRatioによってスケール係数が決まります)。さらにモバイルキャンバスで約1.2倍になります。まずデスクトップ向けのサイズから始め、デバイス上で小さく見えるものだけを拡大してください。 - タッチターゲットは仮想キャンバス上で約48 px以上にします。本文の
fontSizeは16以上、移動しながら読むテキストは20以上にします。大きくなる可能性のあるラベルには、textWrap="wrap"と明示的な幅を指定します。 - テキスト値に絵文字や装飾的なUnicode文字を使わないでください(
Label/Buttonのvalue、uiText.value、Inputのplaceholder、Dropdownの選択肢、バインドされた文字列)。グリフの対応状況はSDKではなく、各エクスプローラーにバンドルされたフォントによって決まります。Unityエクスプローラーには絵文字のグリフが含まれていないことが実際のワールド内で確認されています。value="✨ Particles"では、キラキラの文字が表示されませんでした。代わりに、ラベルの横にuiBackground={{ texture: { src: 'images/icon.png' } }}を指定した小さなUiEntityを配置するか、uvsを使ってアトラススプライトを配置してください。texture.srcはバインディングなので、アイコンは編集可能なままです。ASCII文字と、テキストに必要なアクセント付き文字を使ってください。 - テクスチャの
srcパスはシーンルートからの相対パス('images/panel.png')であり、src/からの相対パスではありません。 - モバイルでは
borderRadiusはサポートされていません。タッチ操作ではホバーレイヤーは機能しません — 唯一の操作手がかり、または値を読み取る唯一の方法にしないでください。
配置:左上隅に固定しない。モバイル UI をアクションボタンから離す
生成されたシーンで最もよくあるレイアウト上の誤りは、パネルを左上隅に固定することです(position: { top: 0, left: 0 }、または子要素が原点から配置される全キャンバスのルート)。この隅と、デスクトップでは左端全体は、エクスプローラー独自の UI(ミニマップ、チャット、モバイルでは仮想ジョイスティック)が描画される場所です。そこにあるシーン UI はクライアントの HUD の下に表示され、隠れてボタンがクリックできないことも多いのに、警告は出ません。推奨順は次のとおりです。
- 小さな HUD(スコア、タイマー、ボタン数個) → 右端に固定するか、中央に配置します。
positionType: 'absolute', position: { top: 40, right: 40 }を使うか、ルートにjustifyContent: 'center'を設定します。パネルを原点に置いたままにしないでください。操作可能なインセットがない状態でleft: 0を使わないでください。 - 確実なセーフエリアが必要な場合 →
interactableインセットを使います(編集可能な場合:ルートの Scene Inset。コードの場合:レンダラーにscreenInset: 'interactable'を指定します。7.26.0 以降で利用可能です。それより前のバージョンでは<InteractableArea>でラップします)。ミニマップ、チャット、左側のコントロールが除外されるため、その内側のleft: 0はデスクトップでは安全です。ただし除外されるのは左側の列のみです。右端はデバイス領域と共有しているため、右下のアクションクラスターは仕様上その内側にあります。 - モバイル:右下のアクションボタン(ジャンプ、E、F、ポインター)は操作可能領域の上に描画され、その場所へのタップはクライアントに送られます。ゲームプレイ中の HUD はこの領域を避けてください。右上または上中央に固定し、モバイルでは右下に操作可能な要素を置かないでください(正確な余白は
UiCanvasInformation.interactableAreaで確認できます。ボタンのスロット位置は固定です)。ゲームプレイを中断する UI(スコアボード、結果、ショップ、ルールカード)を開いている間は、タッチコントロールを非表示にします。開くときはTouchScreenControls.hideAll()とhideJoystick()、閉じるときはshowAll()とshowJoystick()を呼び出します(デスクトップでは何も行いません。showAll()はカスタムアイコンもクリアするため、再適用してください)。必要に応じて、同じ期間にengine.PlayerEntityのInputModifierdisableAllも使用します(デスクトップクライアントとモバイルクライアントで有効です。このケースではモバイルのドキュメントでも推奨されています。レガシー Web エクスプローラーでは効果がありません)。どの経路で閉じる場合も、必ず同じ関数を通すことで、コントロールが非表示のままにならないようにしてください。編集可能な場合、ドライバーはstate.visibleの切り替わりに応じて処理します。コードでのパターンは{baseDir}/references/coded-ui.md→ 全画面モーダルを参照してください。UI Designer の MobileHUD パネルを使うシーンでは、この実行時の切り替えがパネルの静的な呼び出しと競合します。MobileHUD を参照してください。 キーで閉じるカードには、モバイルでタップできる方法が必要です。IA_PRIMARYだけで進むタイトル、エンディング、「E を押して続行」カードは、タッチコントロールを非表示にすると、スマートフォンでは先に進めなくなります。ネイティブの E ボタンだけが進む手段だったためです(所有者からの報告、2026-10-12:「E ボタンを押せない」)。同じアクションを UI にバインドします。表示中の続行ボタンにuiInputBinding={{ actions: [InputAction.IA_PRIMARY] }}を指定するか、カード自体に指定してどこをタップしても進めるようにします({baseDir}/references/ui-components.md→ uiInputBinding:要素がキーを押した場合と同じようにアクションを発生させるため、既存のinputSystemチェックが引き続き機能します)。カード自体にタップ可能な操作がない場合は、タッチコントロールを表示したままにしてください。ボタンを使う方法はドキュメントで推奨されているモバイル向けパターンです。どこでもタップできる方法は、まだ実機で確認されていません。
ゲームを説明するパネルのコンテンツルール:スクロールなしで1画面に収め、短い行を3~5行にし、説明するよりも(アイコン、図、強調表示された最初のターゲットを)見せてください。game-design スキル → 「ルール:説明するより見せる」。
ポインターのブロック
リスナーを持つUI要素、または pointerFilter: 'block' を設定したUI要素は、表示されているピクセル部分だけでなく、矩形全体でポインター入力を捕捉します。そのため、3Dワールドや背後にあるすべてのUI要素へのクリックがブロックされます。背景を透明にしても何も変わりません。リスナーがなく、デフォルトの pointerFilter: 'none' が設定された要素は、クリックを通します。
ハンドラー、{...useInteraction} のスプレッド、または pointerFilter: 'block' を 100%×100% のラッパーに絶対に設定しないでください。 その矩形は画面全体を覆うため、レイアウトルートに意図しないリスナーが1つあるだけで、プレイヤーは他のUI要素やワールド内のあらゆるものをクリックできなくなります。それでも、表示されるパネルは小さいため、UIは見た目上正しく表示されます。useInteraction は表示ゲートとしてのみ使う場合も4つのリスナーすべてを返すため、スプレッドも対象です。ハンドラーとゲートは、それらを必要とする最小の要素(パネル、ボタン、行)に設定してください。レイアウトラッパーは、位置指定だけを行うリテラルスタイルの UiEntity のままにします。ハンドラーを追加するたびに、これを確認してください。変更前後の例:{baseDir}/references/component-template.md §4。
画面全体をブロックするオーバーレイが正当なケースは2つあります。どちらも副作用ではなく、意図的にゲートを設定して使う必要があります。開いている間クリックを吸収するモーダルの背景と、ドラッグ中にのみ表示するドラッグ終了時のキャッチャー({baseDir}/references/drag-slider.md)です。編集可能なUIでは、ゲートに active な display: 'none' レイヤーを使います。コードで記述するUIでは、条件付きレンダリングを使います。
MobileHUD(src/mobile-hud.ts)— エディター所有、手書き禁止
Creator Hub 0.50.0+。MobileHUD は、UI Designer の GUI レールに固定された先頭項目として表示されます(シーンにGUIが1つ以上ある場合)。MobileHUD は、読み取り専用のキャンバスプレビューを使って、シーンの TouchScreenControls(ネイティブのモバイル用ジョイスティック、照準、ゲームパッドボタン)を編集します。react-ecs のルートではありません。src/mobile-hud.ts に記述される単一の TouchScreenControls.createOrReplace(engine.RootEntity, { … }) 呼び出しであり、src/ui/ の外に置かれるため、ルートスキャナーがGUIとして扱うことはありません。setupMobileHud としてエクスポートされ、src/index.ts で setupUi() の隣にインポートされます。
src/mobile-hud.tsを手書きしたり、編集したりしないでください。 パネルは、この特定の形式を独自に解析してファイルを再生成します。解析できないコードは、警告なしにデフォルト値に戻されます。- そのようなシーンでは、別の場所に2つ目の
TouchScreenControls.createOrReplaceを絶対に記述しないでください。engine.RootEntity上のコンポーネントは1つだけであり、最後に書き込まれた値が有効になります。 - このファイルは遅延作成され、自動的に削除されます。SDK のデフォルトから設定が変更されている間だけ存在します。ファイルがないシーンでは、標準の動作が使われています。
- UI Designer を使うシーンでモバイルHUDを変更するには、パネルで変更してください。カットシーンやモーダル中にボタンを隠すといった、実際の実行時制御は
src/ui/の外にドライバーを置いて実装します。その場合、パネルの静的な呼び出しと競合することに注意してください。コンポーネント API については advanced-input を参照してください。
パネルで公開される項目と protobuf に関する注意点:{baseDir}/references/editor-behavior.md → MobileHUD。
コード記述 UI(特殊なケース)
モードの選択で UI をコード記述モードにすると、要素、サイズ、配置、ポインターのブロックに関する上記の内容はすべて引き続き適用されます。変更点は次のとおりです。
- ファイル構成は自由です(
src/ui.tsxの単一ファイルでも、編集可能なルートの隣にあるsrc/ui/の外部のモジュールでもかまいません)。条件付きレンダリング、.map()、ヘルパーコンポーネント、uiTextバッグ、計算されたスタイル、テーマ定数はすべて問題ありません。 setUiRenderer/addUiRendererには、必ず{ virtualWidth: 1920, virtualHeight: 1080 }を渡してください。これにより基準解像度が固定され、以下の 7.26.0 のバージョン境界の前後で動作が同じになります(それより前ではサイズを省略するとスケーリングが無効になり、7.26.0 以降では、モバイルで 16:9 のサイズがいずれにしても1600x720に上書きされます)。単一の寸法だけを出力してはいけません。screenInsetとレンダラーのzIndexオプション(7.29.0 以降)は、バージョン間で安全に使用できません。- ルートの
<UiEntity>にはwidth: '100%', height: '100%'を設定し(信頼できる絶対配置に必須)、ポインターを透過した状態に保ちます。 - シーンごとに
setUiRendererを呼び出せるのは 1 回だけです。2 回目の呼び出しは、最初の呼び出しをエラーなく上書きします。独立したモジュールではaddUiRenderer(ownerEntity, Widget, options)を使用し、removeUiRenderer(owner)で削除するか、オーナーのエンティティが破棄されたときに自動的に削除します。コード記述されたパネルを編集可能なsrc/ui/と共存させる場合もこの方法を使います。この場合、アグリゲーターがsetUiRendererを管理します。
仮想画面とバージョン境界の表、ハイブリッドシーン、条件付きレンダリングを伴うモジュールレベルの状態、ウィジェットをゼロから作成するパターンについては、{baseDir}/references/coded-ui.md を参照してください。
注意点(エンジンのテストシーンで検証済み)
InputとDropdownは、React の制御コンポーネントではありません。onChange/onSubmitは現在の値を伴って発火しますが、フィールドは毎フレームvalue/selectedIndexを読み直しません。Inputをクリアするには、valueに空ではないセンチネル値(' ')を1フレーム設定してから、''を設定します。レンダリングされたフィールドではなく、シーンの状態を信頼してください。制御されたInputはプログラムによるリセットに追従せず(クライアント側の未解決課題)、その後に同じ文字列を書き戻してもonChangeは発火しません。onSubmit(Enter)は値を確定してフィールドをクリアし、クライアントはonSubmitの後にonChangeを発火します。フォーカス中のInputを自身のハンドラー内で削除しても安全です。DropdownのonChangeが受け取るのはインデックスです。acceptEmptyを指定すると、空のエントリーのインデックスはシフトします。詳細:{baseDir}/references/ui-components.md→ Input。Buttonのdisabledはテキスト/背景のアルファ値を半分にし、onMouseDown/onMouseUpのみを無効にします。onMouseEnter/onMouseLeaveは引き続き発火します。zIndexは兄弟要素のグループごとに適用されます。別のブランチに属する要素より前面に移動させることはできません。ブランチをまたいだ重なり順にはツリーの順序(またはコード UI での配列の返却順)を使用し、レンダラー全体の重なり順にはレンダラーのzIndexオプションを使用してください(ui-components.md→ Renderer zIndex。Admin Tools のスマートアイテムは 1000 に配置されています)。opacityはツリーを下るにつれて乗算されます。 不透明度 0.5 のルート内にある不透明度 0.8 の子要素は、0.4 でレンダリングされます。textureMode: 'stretch'は縦横比の異なるアートを変形させます。パネル/ボタンには'nine-slices'とtextureSlicesを、元のサイズで表示するには'center'を使用してください。textureと並べて指定したcolorはテクスチャに色を付けます。⚠️ モバイルではナインスライスはまだ実装されていません(2026年10月時点で提供時期は未定)。モバイルクライアントは中央部分を引き伸ばさず、テクスチャをタイル状に表示するため、デスクトップで正しく見えるナインスライスのパネルも、スマートフォンではパターンが繰り返し表示されます。モバイルを対象とするシーンのパネルには、単色のcolor背景を使うか、タイル状に表示しても違和感のないアートを使用してください。- インセットはクリッピングしません。
ScreenInsetArea/InteractableAreaはoverflowを設定しないため、インセットを越えて配置されたコンテンツは予約領域にはみ出してレンダリングされます。セーフエリアの輪郭からはみ出すのは配置ミスなので、overflow: 'hidden'で隠すのではなく修正してください。 - 7.26.0 以降では、手動で配置した
ScreenInsetArea/InteractableAreaはレンダラー独自のscreenInset(デフォルトは'device')に重ねて適用されます。ラッパーを自分で配置する場合は、screenInset: 'none'を渡してください。7.26.0 より前では、ラッパーだけが唯一の仕組みです。生成されたアグリゲーターは変更できない例外です(アグリゲーターを参照)。 UiCanvasInformation.width/heightは仮想単位ではなく、キャンバスの生ピクセル数です。これらは判断(どのレイアウト、どのテクスチャを使うか)に使用し、サイズの計算には使用しないでください。
トラブルシューティング
まず配線に関する原因を確認してください。大半の問題はこれが原因です。
| 問題 | 原因 | 解決策 |
|---|---|---|
| UI が描画されない/画面に何も表示されない(最もよくある問題) | src/index.ts の main() から setupUi() が呼び出されていない | 呼び出しを追加してください。必ず最初に確認してください |
setupUi() が呼び出されているのに UI が描画されない | setupUi() に ReactEcsRenderer.setUiRenderer(...) がない | 集約側の形式(編集可能)または正規の呼び出し(コード記述式)に戻してください |
| 最初の数フレームは UI が空白で、その後表示される | ルートコンポーネントが最初のレンダリングで null/偽値を返している | 代替表示または条件付きのルートをレンダリングしてください |
| 複数の UI が競合する/UI が表示されない | 複数の setUiRenderer があり、最後のものが警告なく優先される | setUiRenderer は 1 つだけにし、追加モジュールにはそれぞれ専用のオーナーエンティティを指定して addUiRenderer を使ってください |
| 絶対位置指定された子要素のレイアウトが想定外になる | ルートの <UiEntity> に width: '100%', height: '100%' がない | 追加してください(実際に必要であることが確認されています。これがないと top-right の子要素が消えました) |
| UI 要素が重なる | flexDirection がない、またはボックスのないラベルがある | 親に flexDirection: 'column' を指定し、Label のボックスを明示してください |
| 左上/左端に表示されたボタンがクリックできない、または一部が隠れる | シーン UI がエクスプローラーの HUD(ミニマップ、チャット、モバイルジョイスティック)の下にあり、クリックを奪われている | 右または中央に配置するか、interactable のインセットを使ってください。配置を参照してください |
| モバイル:大きなパネルの右下をタップしても反応しない/ジャンプや E が発動する | ネイティブのアクションボタンが操作可能領域の上に描画されている | 操作要素を右下に置かないか、全画面 UI を開いている間はタッチ操作を非表示にしてください |
| ボタンのクリックが反応しない | onMouseDown がない | Button/UiEntity に追加してください(編集可能な UI では @ui-action の thunk) |
| 画面上の何もクリックできなくなった | 100%×100% のラッパーにリスナー、useInteraction のスプレッド、または pointerFilter: 'block' が設定されている | ラッパーから削除し、条件判定やハンドラーをパネル/ボタンに移してください |
| あるエクスプローラーでは正しく見えるが、Unity ではラベルが重なる/パネルが押しつぶされる | 明示的なボックスのない Label があり、コンテナーがテキストに合わせて自動サイズ調整されている | すべてのラベルと、ラベルを積み重ねるすべてのコンテナーに width/height を明示してください |
| 文字列の一部が欠ける、または □ と表示される | 絵文字/装飾用 Unicode | 削除し、テクスチャアイコンを使ってください |
| コンパイル時に JSX エラーが発生する | ファイルが .tsx ではなく .ts になっている | 名前を変更してください |
| テキストが表示されない | 色が背景と同じ | コントラストのある color を指定してください |
| エディターのキャンバスではコンポーネントがつぶれている/高さが 0 だが、ワールド内では正常 | コンポーネントのルートまたは ref ラッパーに明示的な width/height がない | 両方を宣言してください |
| ノードの編集がすべて拒否される(フリーズする) | uiTransform/uiBackground/Label のプロパティに計算値、識別子、または呼び出しがある | リテラルまたは裸の state.x/props.x を使い、計算はドライバーに移してください |
| サブツリーがキャンバスに表示されない(不透明) | {cond && …}、三項演算子、.map()、JSX コメント、不明な要素、useInteraction 以外のスプレッド | 表現できないものに従って置き換えてください |
| エディターに「UI Editor Unavailable」と表示される | @dcl/sdk が 7.26.0 より古い | シーンの SDK を更新してください |
| ドラッグスライダーが揺れる/速度が間違っている | 古いクロージャー経由で値を読み取っている、または TRACK_PX/スケール係数が一致していない | {baseDir}/references/drag-slider.md |
セルフチェックリスト
作成または編集した src/ui/ 以下の すべての .tsx ファイルで確認してください。各項目に違反すると、エディター上でエラーが表示されずに失敗します。
- ファイル名が
src/ui/<PascalCaseName>.tsxで、ベース名がエクスポートするコンポーネント名と一致し、1 行目が/** @jsx ReactEcs.createElement */で、エクスポートするコンポーネントが 1 つだけであること。 - すべての JSX 要素名が
UiEntity、Label、Input、Dropdown、Button、または別のsrc/ui/ファイルでエクスポートされたコンポーネントであること。 {cond && <X/>}およびcond ? <A/> : <B/>を使わないこと。ただし、usePlatform()の分岐で、それぞれが単一の要素またはnullである場合を除きます。.map()、ループ、配列から組み立てた子要素を使わないこと。{/* JSX comments */}を使わないこと。コメントは JSX の外側に//で記述します。uiTransform/uiBackground内の値がすべてリテラル、または単独のstate.x/props.xであること。これらのオブジェクトに対して?、+、*、Math.、(、単独の識別子を grep し、それぞれが固定値のノードか確認してください。- すべてのテキストが
Label/Buttonのvalueにあること。UiEntityにuiTextバッグを設定しないこと。 - props 内のテンプレートリテラルが、単独の参照のみを補間していること。
- いずれの要素でも、スプレッドは単一の
{...someUseInteractionConst}のみであること。 - 表示状態は、
display: 'none'を設定するuseInteractionのactiveレイヤーで制御すること。ホバー時/押下時の状態にはhover/pressレイヤーを使います。 - 画面全体を埋める
100%×100%の要素(またはサイズ指定がなく画面全体を埋める要素)には、ポインターハンドラー、{...useInteraction}のスプレッド、pointerFilter: 'block'を設定しないこと。例外は、どちらもアイドル時には無効化されるモーダルの背景要素とドラッグ終了時のキャッチャーのみです。 export interface Stateとexport const state: Stateがあり、すべてのプロパティがnumber、string、boolean、string[]、または{ r, g, b, a }形式の構造体カラーであること。- 宣言する props は、オプションの
number/string/boolean/コールバックメンバーのみを含むインラインオブジェクト型であること。 - すべてのハンドラーが
/** @ui-action */を付けた({ state, props, value }: UiAction)を受け取る関数であり、サンク経由で接続されていること。 - バインドするサイズ/位置はすべて px 数値であること。バインド値にパーセント文字列を使わないこと。
- コンポーネントのルートで
widthとheightの両方(px またはパーセントのリテラル)を宣言すること。また、コンポーネント参照を囲むすべてのラッパーUiEntityでも、コンポーネントのルートサイズに合わせて両方を宣言すること。 src/ui/内のどこにも、時計、Date.now()、setTimeout、イージング、丸め処理、文字列の書式設定を含めないこと。これらはドライバー側に置きます。src/ui/index.tsxの形が生成された形と完全に一致し、フルキャンバスの操作が明示的に要求されている場合を除き、各ルートがScreenInsetAreaで囲まれていること。- モバイルでの確認:レイアウトが
1600x720のキャンバスで崩れないこと、タッチ対象が約 48 px 以上であること、ホバー時のみの操作案内がないこと、絵文字がないこと、左上に固定された要素がないこと、右下に操作可能な要素がないこと。 - UI-Designer シーンで作成した箇所に
TouchScreenControlsの呼び出しがないこと。モバイル操作はsrc/mobile-hud.ts経由の MobileHUD パネルで行います。シーンを grep し、該当箇所が生成されたそのファイルだけであることを確認してください。
リファレンス
{baseDir}/references/component-template.md— エディターのシードファイル、最小限のコンポーネント、フル機能のコンポーネント(バインディング、混在テキスト、ホバー、2変数の終了ゲート)、ダイアログの変更前と変更後、props駆動のフィルバー、展開したバリアント、合成画面、アグリゲーター。{baseDir}/references/interaction-helper.md—src/ui/interaction.tsxとsrc/ui/platform.tsxの原文、およびパーサーが認識する使用パターン。{baseDir}/references/driver-pattern.md— イージング付きの開閉、書式設定されたタイマー、2変数の終了ゲート、アクションからドライバーへのハンドシェイク、派生値、命名規則に沿ったドライバー。{baseDir}/references/adapting-coded-ui.md— コードで記述された UI の移植手順(変更前と変更後)、および移植できないものをユーザーに伝える方法。{baseDir}/references/drag-slider.md— スライダーにPrimaryPointerInfo.screenDeltaが必要な理由、実機で動作確認済みの編集可能なスライダー(タップで段階変更+ドラッグ、ゲート付きリリースキャッチャー、ドライバー)、コードで記述するバリアント、SDK のバージョンに依存するスケール係数。{baseDir}/references/editor-behavior.md— Creator Hub の UI エディターが書き込み、表示する内容:レイアウト軸、ウィジェットのプリセット、Scene Inset、キャンバスツール、モードの永続化、モバイルプレビューとその制限、MobileHUD パネルのフィールド。{baseDir}/references/ui-components.md— 5つの要素に関するプロパティの完全なリファレンス、uiInputBinding、背景(テクスチャモード、ナインスライス、UV、色合い、アバター)、要素とレンダラーのzIndex、screenInset/ScreenInsetArea/InteractableArea、UiCanvasInformation、および正確な基準となるエンジンのテストシーン。{baseDir}/references/coded-ui.md— 自由形式の手法:仮想画面のデフォルトルールと 7.26.0 のバージョンゲート、addUiRendererとハイブリッドシーン、条件付きレンダリングを使ったモジュールレベルの状態管理、ウィジェットを一から作成するパターン、および各パターンに対応する編集可能な実装。
相互参照
- カスタムフォント:
Label、Button、Input、DropdownおよびuiTextで、シーンに含めた.ttfにfontSrcを指定し、フォールバックとして常にfontを指定します。7.30.1 の次の SDK リリースが必要です。モバイルでは表示されず、デスクトップではアセットバンドルからのみ表示されます(デプロイ済みの場合、または Compress Assets を有効にしたプレビューの場合)。fonts を参照してください。 - プラットフォームの検出:
@dcl/sdk/platformのgetPlatform()/isMobile()(編集可能な UI ではusePlatform()でラップされています)。advanced-input を参照してください。 - ローカライズされた UI テキスト:同じモジュールの
getPlayerLanguage()は、プレイヤーのクライアント言語を BCP-47 タグで返します。セッション中の言語切り替えにはonPlayerLanguageChangedを使います。エクスプローラーから応答があるまでは'en'を返すため、モジュールスコープではなく、システム(ドライバー)内で読み取ってください。advanced-input >「Player language」を参照してください。SDK 7.30.0+。 - プレイヤーをクライアントパネルに移動する:
openExplorerUiはマップ/バックパック/プレイス/イベントの各パネルを開き、openExplorerUiAndWait(@dcl/sdk/explorer-ui)はプレイヤーがパネルを閉じると解決します。ユーザー操作が必要です。scene-runtime >「openExplorerUi -- Open Explorer Panels」を参照してください。 - ネイティブのモバイル操作を置き換える:
TouchScreenControls(SDK 7.26.0 以降、advanced-input)はジョイスティック/照準/ゲームパッドのボタンを非表示にし、シーン UI で置き換えられるようにします。uiInputBindingで置き換えたボタンを InputActions に接続します。UI-Designer シーンでは、この設定は MobileHUD パネルで行います。クライアント独自の HUD(チャット、プロフィール、エモートホイール)は非表示にできません。 - ルール/説明のコンテンツ:game-design(「Rules: show, don't tell」)では、テキストの多いポップアップと、読みにくいワールド内のルールブックというアンチパターンを紹介しています。
- シーンを開いたままリアルタイムで編集する(エンティティ、スマートアイテム、設定):creator-hub-mcp。