mcpField
B
build-ui

build-ui

github.com/dcl-regenesislabs设计与 UI1738 次安装21 周前更新
代码仓库

关于 build-ui

面向 Decentraland SDK7 场景创作者的 React-ECS 技能,用于通过 JSX 构建屏幕上的 2D 覆盖界面。涵盖 HUD、菜单及交互式 UI 的实现方法。

主要功能

  1. 01使用 React-ECS 构建覆盖界面
  2. 02使用标签、按钮、输入框和下拉框
  3. 03通过模块变量管理状态
  4. 04挂载相互独立的 UI 渲染器
  5. 05使用布局、悬停和纹理切片模式

适用场景

  1. 01制作记分板和生命值条
  2. 02构建菜单、对话框和物品栏
  3. 03显示设置界面和调试覆盖层
  4. 04按需显示不同 UI 模块

安装

复制下面的命令,在终端中运行。

$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 使用类似 React 的 JSX 系统来构建 2D UI 覆盖层。

何时使用哪种 UI 方式

需求方式组件
屏幕空间 HUD、菜单、按钮React-ECS(本技能)UiEntity、Label、Button、Input、Dropdown
在世界中浮动的 3D 文本TextShape + Billboard参见 advanced-rendering 技能
打开网页openExternalUrl参见 scene-runtime 技能
3D 空间中的可点击对象指针事件参见 add-interactivity 技能

对于任何 2D 覆盖层(如记分板、生命值条、对话框、物品栏和设置菜单),请使用 React-ECS。对于 3D 世界中 NPC 或物体上方的标签,请使用 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 Hooks):

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)
  }
})
九宫格纹理

使用 textureSlices 创建可缩放的 UI 背景(按钮、面板),避免边角被拉伸:

<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() }}
/>
Flex 换行

允许 UI 子元素换到下一行:

<UiEntity uiTransform={{ flexWrap: 'wrap', width: 300 }}>
  {items.map(item => (
    <UiEntity key={item.id} uiTransform={{ width: 80, height: 80, margin: 4 }} />
  ))}
</UiEntity>
Dropdown 扩展项

Dropdown 组件支持其他 props:

<Dropdown
  options={['Option A', 'Option B', 'Option C']}
  selectedIndex={selectedIdx}
  onChange={(idx) => { selectedIdx = idx }}
  fontSize={14}
  color={Color4.White()}
  disabled={false}
/>
多个 UI 模块(addUiRenderer / removeUiRenderer)

如果你有多个独立的 UI 模块(例如 HUD、对话框系统、调试覆盖层),可以将它们合并到单个根组件下,或者使用 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 完全没有显示缺少 ReactEcsRenderer.setUiRenderer() 调用在 main() 或 setupUi() 中添加 ReactEcsRenderer.setUiRenderer(MyUI)
UI 元素重叠缺少 flexDirection 或布局错误在父容器上设置 flexDirection: 'column'
按钮点击未生效缺少 onMouseDown 处理程序为 Button 或 UiEntity 添加 onMouseDown={() => { ... }}
编译时出现 JSX 错误文件扩展名是 .ts,而不是 .tsx将文件重命名为 .tsx
多个 UI 互相冲突调用了多个 setUiRenderer主 UI 只使用一个 setUiRenderer;独立模块则改用 addUiRenderer(ownerEntity, ...)
文本不可见文本颜色与背景相同为 Label 或 uiText 设置对比明显的 color

需要的是世界交互,而不是屏幕 UI? 请参阅 add-interactivity 技能,了解 3D 对象的点击处理程序和指针事件。

重要说明

  • 不支持 React Hooks(useState、useEffect 等)——请使用模块级变量
  • UI 渲染器每帧都会重新渲染,因此状态变化会立即反映出来
  • UI 会作为 2D 覆盖层渲染在 3D 场景上方
  • 在 uiTransform 中使用 display: 'none' 隐藏元素,无需将其移除
  • 要支持 JSX,文件扩展名必须是 .tsx
  • 每个场景只能调用一次 ReactEcsRenderer.setUiRenderer()——将所有 UI 合并到一个根组件中

有关组件的完整 props(UiEntity、Label、Button、Input、Dropdown)、布局模式和响应式设计,请参阅 {baseDir}/references/ui-components.md。

评论

支持 Markdown