build-ui について
Decentraland SDK7 のシーン制作者向けに、React-ECS と JSX で画面上の 2D オーバーレイを作るスキルです。HUD、メニュー、操作可能な UI の実装方法を扱います。
主な機能
- 01React-ECS によるオーバーレイ作成
- 02ラベル、ボタン、入力、ドロップダウン
- 03モジュール変数による状態管理
- 04独立した UI レンダラーのマウント
- 05レイアウト、ホバー、テクスチャスライス
活用シーン
- 01スコアボードや体力バーの作成
- 02メニュー、ダイアログ、インベントリの構築
- 03設定画面やデバッグ表示
- 04UI モジュールの条件付き表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill build-uiインストール後、Claude Code で/build-uiを入力して読み込まれたか確認します。
同梱ファイルreferences/ui-components.md
詳細
React-ECS での UI 構築
Decentraland SDK7 では、2D UI オーバーレイに React のような JSX システムを使用します。
UI アプローチの使い分け
| 必要なもの | アプローチ | コンポーネント |
|---|---|---|
| 画面上の HUD、メニュー、ボタン | React-ECS(このスキル) | UiEntity、Label、Button、Input、Dropdown |
| ワールド内に浮かぶ 3D テキスト | TextShape + Billboard | advanced-rendering スキルを参照 |
| Web ページを開く | openExternalUrl | scene-runtime スキルを参照 |
| 3D 空間内のクリック可能なオブジェクト | ポインターイベント | add-interactivity スキルを参照 |
スコアボード、ヘルスバー、ダイアログ、インベントリ、設定メニューなど、2D オーバーレイには React-ECS を使用してください。NPC や 3D ワールド内のオブジェクトの上に表示するラベルには TextShape を使用してください。
セットアップ
ファイル:src/ui.tsx
import ReactEcs, { ReactEcsRenderer, UiEntity, Label, Button } from '@dcl/sdk/react-ecs'
const MyUI = () => (
<UiEntity
uiTransform={{
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Label value="Hello Decentraland!" fontSize={24} />
</UiEntity>
)
export function setupUi() {
// ALWAYS pass virtualWidth + virtualHeight — the renderer scales the layout
// to fit the player's window using these as the reference. Without them,
// sizes are interpreted in raw pixels and won't behave consistently across
// resolutions and aspect ratios.
ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
}
ファイル:src/index.ts
import { setupUi } from './ui'
export function main() {
setupUi()
}
tsconfig.json(/init によって設定済み)
SDK テンプレートには必要な JSX 設定がすでに含まれているため、tsconfig.json は変更しないでください。
"jsx": "react-jsx""jsxImportSource": "@dcl/sdk/react-ecs-lib"
コアコンポーネント
UiEntity(コンテナー)
import { Color4 } from '@dcl/sdk/math'
<UiEntity
uiTransform={{
width: 300, // Pixels or '50%'
height: 200,
positionType: 'absolute', // 'absolute' or 'relative' (default)
position: { top: 10, right: 10 }, // Only with absolute
flexDirection: 'column', // 'row' | 'column'
justifyContent: 'center', // 'flex-start' | 'center' | 'flex-end' | 'space-between'
alignItems: 'center', // 'flex-start' | 'center' | 'flex-end' | 'stretch'
padding: { top: 10, bottom: 10, left: 10, right: 10 },
margin: { top: 5 },
display: 'flex' // 'flex' | 'none' (hide)
}}
uiBackground={{
color: Color4.create(0, 0, 0, 0.8) // Semi-transparent black
}}
/>
Label(テキスト)
import { Color4 } from '@dcl/sdk/math'
<Label
value="Score: 100"
fontSize={18}
color={Color4.White()}
textAlign="middle-center"
font="sans-serif"
uiTransform={{ width: 200, height: 30 }}
/>
Button
<Button
value="Click Me"
variant="primary" // 'primary' | 'secondary'
fontSize={16}
uiTransform={{ width: 150, height: 40 }}
onMouseDown={() => {
console.log('Button clicked!')
}}
/>
Input
import { Input } from '@dcl/sdk/react-ecs'
import { Color4 } from '@dcl/sdk/math'
<Input
placeholder="Type here..."
fontSize={14}
color={Color4.White()}
uiTransform={{ width: 250, height: 35 }}
onChange={(value) => {
console.log('Value changing:', value)
}}
onSubmit={(value) => {
console.log('Submitted:', value)
}}
/>
Dropdown
import { Dropdown } from '@dcl/sdk/react-ecs'
<Dropdown
options={['Option A', 'Option B', 'Option C']}
selectedIndex={0}
onChange={(index) => {
console.log('Selected:', index)
}}
uiTransform={{ width: 200, height: 35 }}
fontSize={14}
/>
状態管理
UI の状態にはモジュールレベルの変数を使用してください(React フックは使用できません)。
import { Color4 } from '@dcl/sdk/math'
let score = 0
let showMenu = false
const GameUI = () => (
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
{/* HUD - always visible */}
<Label
value={`Score: ${score}`}
fontSize={20}
uiTransform={{
positionType: 'absolute',
position: { top: 10, left: 10 }
}}
/>
{/* Menu - conditionally shown */}
{showMenu && (
<UiEntity
uiTransform={{
width: 300,
height: 400,
positionType: 'absolute',
position: { top: '50%', left: '50%' }
}}
uiBackground={{ color: Color4.create(0.1, 0.1, 0.1, 0.9) }}
>
<Label value="Game Menu" fontSize={24} />
<Button
value="Resume"
variant="primary"
onMouseDown={() => { showMenu = false }}
uiTransform={{ width: 200, height: 40 }}
/>
</UiEntity>
)}
</UiEntity>
)
// Update state from game logic
export function addScore(points: number) {
score += points
}
export function toggleMenu() {
showMenu = !showMenu
}
一般的な UI パターン
ヘルスバー
import { Color4 } from '@dcl/sdk/math'
let health = 100
const HealthBar = () => (
<UiEntity
uiTransform={{
width: 200, height: 20,
positionType: 'absolute',
position: { bottom: 20, left: '50%' }
}}
uiBackground={{ color: Color4.create(0.3, 0.3, 0.3, 0.8) }}
>
<UiEntity
uiTransform={{ width: `${health}%`, height: '100%' }}
uiBackground={{ color: Color4.create(0.2, 0.8, 0.2, 1) }}
/>
</UiEntity>
)
画像の背景
<UiEntity
uiTransform={{ width: 200, height: 200 }}
uiBackground={{
textureMode: 'stretch',
texture: { src: 'images/logo.png' }
}}
/>
画面サイズ
UiCanvasInformation を使って画面サイズを読み取ります。
import { UiCanvasInformation } from '@dcl/sdk/ecs'
engine.addSystem(() => {
const canvas = UiCanvasInformation.getOrNull(engine.RootEntity)
if (canvas) {
console.log('Screen:', canvas.width, 'x', canvas.height)
}
})
ナインスライステクスチャ
角を引き伸ばさずに拡大縮小できる UI 背景(ボタンやパネル)には、textureSlices を使用します。
<UiEntity
uiTransform={{ width: 200, height: 100 }}
uiBackground={{
textureMode: 'nine-slices',
texture: { src: 'images/panel.png' },
textureSlices: { top: 0.1, bottom: 0.1, left: 0.1, right: 0.1 }
}}
/>
ホバーイベント
マウスの入退出に応じて、ホバー効果を適用します。
<UiEntity
uiTransform={{ width: 100, height: 40 }}
onMouseEnter={() => { isHovered = true }}
onMouseLeave={() => { isHovered = false }}
uiBackground={{ color: isHovered ? Color4.White() : Color4.Gray() }}
/>
フレックスラップ
UI の子要素が次の行に折り返されるようにします。
<UiEntity uiTransform={{ flexWrap: 'wrap', width: 300 }}>
{items.map(item => (
<UiEntity key={item.id} uiTransform={{ width: 80, height: 80, margin: 4 }} />
))}
</UiEntity>
Dropdown の追加機能
Dropdown コンポーネントは追加のプロパティに対応しています。
<Dropdown
options={['Option A', 'Option B', 'Option C']}
selectedIndex={selectedIdx}
onChange={(idx) => { selectedIdx = idx }}
fontSize={14}
color={Color4.White()}
disabled={false}
/>
複数の UI モジュール(addUiRenderer / removeUiRenderer)
HUD、ダイアログシステム、デバッグオーバーレイなど、複数の独立した UI モジュールがある場合は、単一のルートにまとめるか、addUiRenderer を使用して各モジュールを所有者エンティティにマウントします。所有者エンティティが削除されると、UI レンダラーも自動的に削除されます。
import { engine, ReactEcsRenderer } from '@dcl/sdk/react-ecs'
const hudOwner = engine.addEntity()
ReactEcsRenderer.addUiRenderer(hudOwner, () => <HudOverlay />)
// later, when the HUD should disappear:
engine.removeEntity(hudOwner) // also removes the UI renderer
// or explicitly:
ReactEcsRenderer.removeUiRenderer(hudOwner)
addUiRenderer でマウントした各 UI は、それぞれ独立してレンダリングされます。巨大な単一ルートコンポーネントの各サブツリーに条件を手動で設定せずに、ゲームの状態に応じて表示・非表示にしたい動的な UI に便利です。
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| UI がまったく表示されない | ReactEcsRenderer.setUiRenderer() の呼び出しがない | main() または setupUi() に ReactEcsRenderer.setUiRenderer(MyUI) を追加します |
| UI 要素が重なって表示される | flexDirection がない、またはレイアウトが正しくない | 親コンテナーに flexDirection: 'column' を設定します |
| ボタンのクリックが認識されない | onMouseDown ハンドラーがない | Button または UiEntity に onMouseDown={() => { ... }} を追加します |
| コンパイル時に JSX エラーが発生する | ファイル拡張子が .tsx ではなく .ts になっている | ファイル名を .tsx に変更します |
| 複数の UI が競合する | setUiRenderer が複数回呼び出されている | メイン UI には setUiRenderer を 1 回だけ使用します。独立したモジュールには代わりに addUiRenderer(ownerEntity, ...) を使用します |
| テキストが表示されない | テキストの色が背景色と同じ | Label または uiText に背景と対照的な color を設定します |
画面 UI ではなくワールド内の操作を実装しますか? 3D オブジェクトのクリックハンドラーやポインターイベントについては、add-interactivity スキルを参照してください。
重要な注意事項
- React フック(
useState、useEffectなど)は使用できません。モジュールレベルの変数を使用してください - UI レンダラーは毎フレーム再レンダリングされるため、状態の変更はすぐに反映されます
- UI は 3D シーン上に 2D オーバーレイとしてレンダリングされます
- 要素を削除せずに非表示にするには、
uiTransformでdisplay: 'none'を使用します - JSX をサポートするには、ファイル拡張子を
.tsxにする必要があります - シーンごとに
ReactEcsRenderer.setUiRenderer()を呼び出せるのは 1 回だけです。すべての UI を 1 つのルートコンポーネントにまとめてください
すべてのコンポーネントのプロパティ(UiEntity、Label、Button、Input、Dropdown)、レイアウトパターン、レスポンシブデザインについては、{baseDir}/references/ui-components.md を参照してください。