关于 build-ui
面向 Decentraland SDK7 场景创作者的 React-ECS 技能,用于通过 JSX 构建屏幕上的 2D 覆盖界面。涵盖 HUD、菜单及交互式 UI 的实现方法。
主要功能
- 01使用 React-ECS 构建覆盖界面
- 02使用标签、按钮、输入框和下拉框
- 03通过模块变量管理状态
- 04挂载相互独立的 UI 渲染器
- 05使用布局、悬停和纹理切片模式
适用场景
- 01制作记分板和生命值条
- 02构建菜单、对话框和物品栏
- 03显示设置界面和调试覆盖层
- 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。