mcpField
B
build-ui

build-ui

github.com/dcl-regenesislabsデザイン・UI1738 回インストール21 週間前に更新
リポジトリ

build-ui について

Decentraland SDK7 のシーン制作者向けに、React-ECS と JSX で画面上の 2D オーバーレイを作るスキルです。HUD、メニュー、操作可能な UI の実装方法を扱います。

主な機能

  1. 01React-ECS によるオーバーレイ作成
  2. 02ラベル、ボタン、入力、ドロップダウン
  3. 03モジュール変数による状態管理
  4. 04独立した UI レンダラーのマウント
  5. 05レイアウト、ホバー、テクスチャスライス

活用シーン

  1. 01スコアボードや体力バーの作成
  2. 02メニュー、ダイアログ、インベントリの構築
  3. 03設定画面やデバッグ表示
  4. 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 + Billboardadvanced-rendering スキルを参照
Web ページを開くopenExternalUrlscene-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 を参照してください。

コメント