build-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Building UI with React-ECS

使用 React-ECS 构建 UI

Decentraland SDK7 uses a React-like JSX system for 2D UI overlays.
Decentraland SDK7 使用类 React 的 JSX 系统来构建 2D UI 叠加层。

When to Use Which UI Approach

何时使用哪种 UI 方案

NeedApproachComponent
Screen-space HUD, menus, buttonsReact-ECS (this skill)
UiEntity
,
Label
,
Button
,
Input
,
Dropdown
3D text floating in the worldTextShape + BillboardSee advanced-rendering skill
Open a web page
openExternalUrl
See scene-runtime skill
Clickable objects in 3D spacePointer eventsSee add-interactivity skill
Use React-ECS for any 2D overlay: scoreboards, health bars, dialogs, inventories, settings menus. Use TextShape for labels above NPCs or objects in the 3D world.
需求方案组件
屏幕空间 HUD、菜单、按钮React-ECS(此 skill)
UiEntity
、
Label
、
Button
、
Input
、
Dropdown
悬浮在 3D 世界中的 3D 文本TextShape + Billboard参见 advanced-rendering skill
打开网页
openExternalUrl
参见 scene-runtime skill
3D 空间中的可点击对象指针事件参见 add-interactivity skill
对于任何 2D 叠加层,都应使用 React-ECS,例如记分板、生命值条、对话框、背包和设置菜单。对于 NPC 或对象上方位于 3D 世界中的标签,则应使用 TextShape。

Setup

设置

Create
src/ui.tsx
with your UI component and call
ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
from
setupUi()
. Call
setupUi()
from
main()
in
src/index.ts
. The SDK template already includes the required JSX settings in tsconfig.json — do NOT modify it.
在
src/ui.tsx
中创建 UI 组件,并在
setupUi()
中调用
ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
。再从
src/index.ts
中的
main()
调用
setupUi()
。SDK 模板已经在 tsconfig.json 中包含所需的 JSX 设置——请勿修改它。

DEFAULT RULE: Always Set Virtual Screen Size to 1920x1080

默认规则:始终将虚拟屏幕尺寸设为 1920x1080

Whenever you generate UI code, you MUST pass
{ virtualWidth: 1920, virtualHeight: 1080 }
to
setUiRenderer
and
addUiRenderer
by default — without waiting for the user to ask.
Only deviate if the user explicitly requests a different reference resolution.
Why: Without a virtual size, UI is laid out in raw screen pixels and renders inconsistently across different resolutions and aspect ratios — fonts, spacing, and absolute-positioned elements drift between displays. Setting a virtual screen size makes the engine scale the UI proportionally to a fixed reference frame, so layouts look the same on every screen. 1920x1080 is the safe default — it matches the most common displays and the assumption made by most community examples.
The options argument is optional at the API level —
setUiRenderer(ui)
is valid, and several engine test scenes omit it. Passing it is still the default rule here; only omit it if the user explicitly wants raw-pixel layout.
API (verified against
@dcl/react-ecs
7.22.5, file
dist/system.d.ts
):
ts
type UiRendererOptions = { virtualWidth: number; virtualHeight: number }
setUiRenderer(ui: UiComponent, options?: UiRendererOptions): void
addUiRenderer(entity: Entity, ui: UiComponent, options?: UiRendererOptions): void
Canonical snippet (use this verbatim unless the user specifies otherwise):
tsx
import { ReactEcsRenderer } from '@dcl/sdk/react-ecs'

export function setupUi() {
  ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
}
**每当你生成 UI 代码时,都必须默认向
setUiRenderer
和
addUiRenderer
传入
{ virtualWidth: 1920, virtualHeight: 1080 }
,无需等待用户提出要求。**只有当用户明确要求使用不同的参考分辨率时,才可以偏离此规则。
原因:如果不设置虚拟尺寸,UI 会按原始屏幕像素进行布局,在不同分辨率和宽高比下呈现不一致——字体、间距和绝对定位元素会在不同显示器之间发生偏移。设置虚拟屏幕尺寸后,引擎会按固定参考画框对 UI 进行等比例缩放,使布局在所有屏幕上保持一致。1920x1080 是安全的默认值——它与最常见的显示器相匹配,也是大多数社区示例所采用的基准。
在 API 层面,options 参数是可选的——
setUiRenderer(ui)
是有效用法,而且多个引擎测试场景都省略了该参数。但此处仍将其作为默认规则;只有当用户明确需要原始像素布局时,才可以省略。
API(已对照
@dcl/react-ecs
7.22.5 的
dist/system.d.ts
验证):
ts
type UiRendererOptions = { virtualWidth: number; virtualHeight: number }
setUiRenderer(ui: UiComponent, options?: UiRendererOptions): void
addUiRenderer(entity: Entity, ui: UiComponent, options?: UiRendererOptions): void
规范代码片段(除非用户另有说明,否则请原样使用):
tsx
import { ReactEcsRenderer } from '@dcl/sdk/react-ecs'

export function setupUi() {
  ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
}

Core Components

核心组件

UiEntity — Container element. Key props:
uiTransform
(width, height, positionType, position, flexDirection, justifyContent, alignItems, alignContent, alignSelf, padding, margin, display, overflow, flexWrap, flexGrow,
opacity
,
zIndex
,
borderWidth
,
borderColor
,
borderRadius
,
pointerFilter
),
uiBackground
(color, texture, textureMode, textureSlices, uvs, avatarTexture),
uiText
(value, fontSize, color, textAlign, font). Events:
onMouseDown
,
onMouseUp
,
onMouseEnter
,
onMouseLeave
.
  • These four are the complete set of UI event handlers, and each is
    () => void
    .
    There is no
    onMouseDrag
    /
    onMouseMove
    , and no arguments — no pointer coordinates, no event object — are passed to a handler. All four are hardcoded to
    InputAction.IA_POINTER
    ; you cannot bind a UI element to right-click or a key. Drag interactions are still fully possible via
    PrimaryPointerInfo.screenDelta
    — see "Sliders" below.
  • opacity
    (number 0–1): fades the element. Set on the root to fade the whole UI; cascades multiplicatively to children.
  • zIndex
    (number, incl. negative): controls stacking order among sibling elements. Higher = on top. Does not cross parent boundaries.
  • borderWidth
    /
    borderColor
    (
    Color4
    ) /
    borderRadius
    : also valid on
    Button
    ,
    Input
    ,
    Dropdown
    via their
    uiTransform
    .
  • width
    /
    height
    accept a number (px),
    '50%'
    ,
    '400px'
    , or
    'auto'
    .
    position
    /
    padding
    /
    margin
    values accept the same string forms;
    margin
    also accepts a CSS shorthand string, e.g.
    margin: '16px 0 8px 270px'
    .
Label — Text display. Key props:
value
,
fontSize
,
color
,
textAlign
(e.g.
'middle-center'
),
font
(
'sans-serif'
|
'serif'
|
'monospace'
),
uiTransform
.
Button — Clickable button. Key props:
value
,
variant
(
'primary'
|
'secondary'
),
fontSize
,
onMouseDown
,
uiTransform
.
Input — Text input field. Key props:
placeholder
,
fontSize
,
color
,
onChange
,
onSubmit
,
uiTransform
.
Dropdown — Selection dropdown. Key props:
options
(string[]),
selectedIndex
,
onChange
,
fontSize
,
uiTransform
,
disabled
.
ScreenInsetArea — Wrapper that keeps children inside the device's hardware-reserved margins (notch, status bar, home indicator, rounded corners). On mobile, it positions itself absolutely using the insets the device reports. On desktop the insets are
(0,0,0,0)
, so it's a no-op — safe to leave in cross-platform UI. It owns its own
positionType
and
position
; any values you pass for those in
uiTransform
are ignored. All other
uiTransform
props (
padding
,
flexDirection
,
alignItems
, …) and components (
uiBackground
,
onMouseDown
, …) work as usual. Wrap any mobile-sensitive HUD in it; a child sized
width: '100%', height: '100%'
fills the safe area exactly. Distinct from the Decentraland system HUD reserved zones (joystick, chat, profile, interaction button) — those still need to be avoided manually; use both together. UI designed for desktop typically needs sizes scaled ~3× for mobile readability.
InteractableArea — Wrapper that keeps children inside the renderer-reported interactable area — the part of the screen NOT covered by the client's own UI (minimap, chat window, platform overlays). Reads
UiCanvasInformation.interactableArea
and constrains children via absolute positioning; on the Unity desktop client the left ~25% of the screen is reserved, so children fill the remaining ~75%. Like
ScreenInsetArea
, it owns
positionType
/
position
(values you pass are ignored) and falls back to zero insets (no-op) when unavailable. Import from
@dcl/sdk/react-ecs
; usage
<InteractableArea><MyHud /></InteractableArea>
. Distinct from
ScreenInsetArea
(which avoids device hardware margins, not client UI). See
{baseDir}/references/ui-components.md
→ InteractableArea.
UiEntity — 容器元素。关键属性:
uiTransform
(width、height、positionType、position、flexDirection、justifyContent、alignItems、alignContent、alignSelf、padding、margin、display、overflow、flexWrap、flexGrow、
opacity
、
zIndex
、borderWidth、borderColor、borderRadius、pointerFilter)、
uiBackground
(color、texture、textureMode、textureSlices、uvs、avatarTexture)、
uiText
(value、fontSize、color、textAlign、font)。事件:
onMouseDown
、
onMouseUp
、
onMouseEnter
、
onMouseLeave
。
  • 这四个属性构成完整的 UI 事件处理函数集合,且每个处理函数都是
    () => void
    。不存在
    onMouseDrag
    /
    onMouseMove
    ,也
    不会向处理函数传递任何参数——没有指针坐标,也没有事件对象
    。这四个处理函数均固定使用
    InputAction.IA_POINTER
    ;无法将 UI 元素绑定到右键或按键。不过,仍可使用
    PrimaryPointerInfo.screenDelta
    完整实现拖拽交互——参见下方“滑块”。
  • opacity
    (数值 0–1):淡出元素。在根元素上设置可淡出整个 UI;透明度会乘算并传递给子元素。
  • zIndex
    (数值,可为负数):控制同级元素之间的堆叠顺序。数值越高,元素越靠上,但不会跨越父级边界。
  • borderWidth
    /
    borderColor
    (
    Color4
    )/
    borderRadius
    :也可通过
    Button
    、
    Input
    、
    Dropdown
    的
    uiTransform
    设置。
  • width
    /
    height
    接受数值(px)、
    '50%'
    、
    '400px'
    或
    'auto'
    。
    position
    /
    padding
    /
    margin
    的值接受相同的字符串形式;
    margin
    还接受 CSS 简写字符串,例如
    margin: '16px 0 8px 270px'
    。
Label — 文本显示。关键属性:
value
、
fontSize
、
color
、
textAlign
(例如
'middle-center'
)、
font
(
'sans-serif'
|
'serif'
|
'monospace'
)、
uiTransform
。
Button — 可点击按钮。关键属性:
value
、
variant
(
'primary'
|
'secondary'
)、
fontSize
、
onMouseDown
、
uiTransform
。
Input — 文本输入字段。关键属性:
placeholder
、
fontSize
、
color
、
onChange
、
onSubmit
、
uiTransform
。
Dropdown — 选择下拉菜单。关键属性:
options
(string[])、
selectedIndex
、
onChange
、
fontSize
、
uiTransform
、
disabled
。
ScreenInsetArea — 包装器,用于将子元素保持在设备硬件预留边距内(刘海、状态栏、Home 指示条、圆角)。在移动端,它会使用设备报告的 insets 对自身进行绝对定位。在桌面端,insets 为
(0,0,0,0)
,因此它是一个 no-op——可以安全地保留在跨平台 UI 中。它拥有自己的
positionType
和
position
;传给这些
uiTransform
属性的值会被忽略。其他所有
uiTransform
属性(
padding
、
flexDirection
、
alignItems
等)和组件(
uiBackground
、
onMouseDown
等)均照常工作。请将所有对移动端敏感的 HUD 包裹在其中;一个设置为
width: '100%', height: '100%'
的子元素会恰好填满安全区域。它不同于 Decentraland 系统 HUD 的保留区域(摇杆、聊天、资料、交互按钮)——这些区域仍需手动避开;应将两者结合使用。为桌面端设计的 UI 通常需要将尺寸放大约 3 倍,才能在移动端清晰显示。
InteractableArea — 包装器,用于将子元素保持在渲染器报告的可交互区域内,即未被客户端自身 UI 覆盖的屏幕部分(小地图、聊天窗口、平台叠加层)。它读取
UiCanvasInformation.interactableArea
,并通过绝对定位限制子元素;在 Unity 桌面客户端中,屏幕左侧约 25% 会被保留,因此子元素填充其余约 75% 的区域。与
ScreenInsetArea
一样,它拥有自己的
positionType
/
position
(传入的值会被忽略),并且在无法获取区域信息时回退为零 insets(no-op)。从
@dcl/sdk/react-ecs
导入;用法为
<InteractableArea><MyHud /></InteractableArea>
。它与
ScreenInsetArea
不同,后者用于避开设备硬件边距,而不是客户端 UI。参见
{baseDir}/references/ui-components.md
中的 InteractableArea。

Adding Independent UI Renderers (addUiRenderer)

添加独立 UI 渲染器(addUiRenderer)

Use
ReactEcsRenderer.addUiRenderer(ownerEntity, MyWidget, { virtualWidth: 1920, virtualHeight: 1080 })
to render a UI module independently without replacing the main UI. Useful for smart items or modular scene components. Remove with
ReactEcsRenderer.removeUiRenderer(owner)
. If the owner entity is destroyed, the UI is removed automatically.
使用
ReactEcsRenderer.addUiRenderer(ownerEntity, MyWidget, { virtualWidth: 1920, virtualHeight: 1080 })
可以独立渲染 UI 模块,而不替换主 UI。这对于智能物品或模块化场景组件非常有用。使用
ReactEcsRenderer.removeUiRenderer(owner)
移除它。如果 owner entity 被销毁,该 UI 也会自动移除。

State Management

状态管理

Use module-level variables for UI state — React hooks (
useState
,
useEffect
, etc.) are NOT available. The UI renderer re-renders every frame, so state changes are reflected immediately. Export functions to update state from game logic.
使用模块级变量管理 UI 状态——React hooks(
useState
、
useEffect
等)不可用。UI 渲染器会在每一帧重新渲染,因此状态变化会立即反映出来。导出函数,以便从游戏逻辑中更新状态。

Common UI Patterns

常见 UI 模式

  • Health bar — Nested UiEntity with width as percentage
  • Image background —
    uiBackground
    with
    texture
    and
    textureMode: 'stretch'
  • Screen dimensions — Read via
    UiCanvasInformation.getOrNull(engine.RootEntity)
  • Nine-slice textures —
    textureMode: 'nine-slices'
    with
    textureSlices
    for scalable panels
  • Texture UVs / Sprite sheets —
    uvs
    array (8 numbers) to select texture regions
  • Hover events —
    onMouseEnter
    /
    onMouseLeave
    on UiEntity
  • Flex wrap —
    flexWrap: 'wrap'
    for grid layouts
  • Scrollable containers —
    overflow: 'scroll'
    on a fixed-size parent to scroll through overflowing content (drag or mouse wheel). Use
    overflow: 'hidden'
    to clip overflow without scrolling. Use
    flexGrow: 1
    on scrollable entities to fill remaining space
  • Texture tint — set
    color
    alongside
    texture
    in
    uiBackground
    to tint the image (works with
    stretch
    and
    nine-slices
    )
  • Multiple stacked layers — the renderer function may return an array of elements, e.g.
    setUiRenderer(() => [PanelA(), PanelB()])
    ; later items in the array render on top of earlier ones
  • Opacity / z-index —
    opacity
    and
    zIndex
    on
    uiTransform
    (see Core Components); root
    opacity
    fades the whole HUD
  • 生命值条 — 嵌套
    UiEntity
    ,内层宽度使用百分比
  • 图片背景 — 在
    uiBackground
    中设置
    texture
    和
    textureMode: 'stretch'
  • 屏幕尺寸 — 通过
    UiCanvasInformation.getOrNull(engine.RootEntity)
    读取
  • 九宫格纹理 — 使用
    textureMode: 'nine-slices'
    和
    textureSlices
    创建可缩放面板
  • 纹理 UV/Sprite sheet — 使用包含 8 个数值的
    uvs
    数组选择纹理区域
  • 悬停事件 — 在 UiEntity 上使用
    onMouseEnter
    /
    onMouseLeave
  • Flex 换行 — 使用
    flexWrap: 'wrap'
    实现网格布局
  • 可滚动容器 — 在固定尺寸的父元素上设置
    overflow: 'scroll'
    来滚动溢出内容(拖拽或鼠标滚轮)。使用
    overflow: 'hidden'
    仅裁剪溢出内容。在可滚动元素上使用
    flexGrow: 1
    ,使其填满剩余空间
  • 纹理着色 — 在
    uiBackground
    中设置
    texture
    的同时设置
    color
    (适用于
    stretch
    和
    nine-slices
    )
  • 多个堆叠图层 — 渲染器函数可以返回元素数组,例如
    setUiRenderer(() => [PanelA(), PanelB()])
    ;数组中较后的元素会渲染在较前元素上方
  • 透明度/z-index — 在
    uiTransform
    上设置
    opacity
    和
    zIndex
    (参见“核心组件”);根元素上的
    opacity
    会淡出整个 HUD

Gotchas (verified against engine test scenes)

常见问题(已对照引擎测试场景验证)

  • Input
    and
    Dropdown
    are uncontrolled.
    onChange
    /
    onSubmit
    fire with the current value, but the field does not read back from the
    value
    /
    selectedIndex
    prop you pass each frame the way React does. To programmatically clear an
    Input
    , briefly set
    value
    to a non-empty sentinel (e.g.
    ' '
    ) for one frame, then back to
    ''
    . Do not expect setting
    value
    to force the displayed text every frame.
  • zIndex
    is per-sibling-group.
    It orders siblings within the same parent; it does not lift an element above elements in a different branch of the tree. Use array-return ordering or tree structure for cross-branch stacking.
  • opacity
    multiplies down the tree.
    A child at
    opacity: 0.8
    inside a root at
    opacity: 0.5
    renders at 0.4 effective. Don't stack opacities unintentionally.
  • textureMode: 'stretch'
    deforms non-uniform art
    ; use
    'nine-slices'
    (with
    textureSlices
    ) for panels/buttons that must scale without distorting borders, and
    'center'
    to draw the texture at native size centered in the element.
  • Texture
    src
    paths are relative to the scene root
    (e.g.
    'images/panel.png'
    ), not to
    src/
    .
  • No pointer coordinates in UI handlers.
    onMouseDown
    /
    onMouseUp
    /
    onMouseEnter
    /
    onMouseLeave
    are
    () => void
    — the reconciler discards the
    PBPointerEventsResult
    before calling your callback, so "where on this element did they click" is unavailable. Track movement instead of position:
    PrimaryPointerInfo.screenDelta
    reports per-frame mouse travel and drives drag interactions fine. See
    {baseDir}/references/ui-sliders.md
    .
  • UI elements with a handler become pointer-blocking. Adding
    onMouseDown
    makes the element block clicks to the 3D world behind it; elements without one let clicks through. Override either way with
    uiTransform.pointerFilter: 'block' | 'none'
    (default
    'none'
    ).
  • Input
    和
    Dropdown
    是非受控组件。
    onChange
    /
    onSubmit
    会携带当前值触发,但字段不会像 React 那样每帧读取你传入的
    value
    /
    selectedIndex
    属性。要以编程方式清空
    Input
    ,先将
    value
    短暂设置为一个非空哨兵值(例如
    ' '
    )保持一帧,然后再设回
    ''
    。不要期望设置
    value
    就能在每帧强制更新显示文本。
  • **
    zIndex
    仅在同一父级下生效。**它用于排列同一父元素中的兄弟节点,无法将某个元素提升到树中其他分支之上。对于跨分支堆叠,请使用数组返回顺序或调整树结构。
  • **
    opacity
    会沿树结构乘算。**根元素的
    opacity: 0.5
    包含一个
    opacity: 0.8
    的子元素时,子元素的最终有效透明度为 0.4。不要无意中叠加透明度。
  • **
    textureMode: 'stretch'
    会使非统一缩放的内容变形;**对于缩放时必须保持边框不失真的面板和按钮,应使用
    'nine-slices'
    (配合
    textureSlices
    );使用
    'center'
    可按原始尺寸居中绘制纹理。
  • 纹理
    src
    路径相对于场景根目录
    (例如
    'images/panel.png'
    ),而不是
    src/
    。
  • UI 处理函数中没有指针坐标。
    onMouseDown
    /
    onMouseUp
    /
    onMouseEnter
    /
    onMouseLeave
    都是
    () => void
    ——reconciler 会在调用回调前丢弃
    PBPointerEventsResult
    ,因此无法获知“用户点击了元素中的哪个位置”。应跟踪移动量而非位置:
    PrimaryPointerInfo.screenDelta
    会报告每帧鼠标移动量,足以驱动拖拽交互。参见
    {baseDir}/references/ui-sliders.md
    。
  • **带有处理函数的 UI 元素会阻止指针事件。**添加
    onMouseDown
    后,该元素会阻止点击穿透到身后的 3D 世界;没有处理函数的元素则允许点击穿透。可使用
    uiTransform.pointerFilter: 'block' | 'none'
    覆盖这一行为(默认值为
    'none'
    )。

Common Widgets — Build From Scratch

常见 Widget——从头构建

Build every widget from React-ECS primitives (
UiEntity
,
Label
,
Button
). There is no pre-built widget library to install.
  • Prompt / dialog / confirmation? → full-screen overlay + centered panel +
    Button
    s. See the Modal Dialog pattern in
    references/ui-components.md
    .
  • Health bar, progress bar, score? → nested
    UiEntity
    with the inner one sized
    width: 
    ${pct}%``. See the Health Bar patterns in
    references/ui-components.md
    and
    references/ui-patterns.md
    ; a score is a
    Label
    bound to a module-level variable.
  • Flash announcement (timed, centered)? → a centered
    Label
    gated on a module-level flag, cleared with
    timers.setTimeout
    . See Timed Announcement in
    references/ui-patterns.md
    .
  • Slider / drag handle / scrub bar? → drag sliders work. UI handlers get no pointer coordinates, so instead:
    onMouseDown
    on the track starts a drag, and a system accumulates
    PrimaryPointerInfo.screenDelta.x
    (divided by the UI scale factor) into the value. A full-screen
    pointerFilter: 'block'
    overlay rendered only while dragging catches the release. Verified in-world on both the Unity and Bevy explorers. Desktop only —
    screenDelta
    is always 0 on mobile, so pair the track with
    -
    /
    +
    stepper
    Button
    s. Full implementation in
    {baseDir}/references/ui-sliders.md
    .
  • Custom panel, inventory, complex layout? → React-ECS directly (see
    references/ui-patterns.md
    ).
每个 widget 都应使用 React-ECS 原语(
UiEntity
、
Label
、
Button
)构建。没有可安装的预构建 widget 库。
  • 提示/对话框/确认框? → 全屏叠加层 + 居中面板 +
    Button
    。参见
    references/ui-components.md
    中的 模态对话框模式。
  • 生命值条、进度条、分数? → 嵌套
    UiEntity
    ,内层元素设置为
    width: 
    ${pct}%``。参见
    references/ui-components.md
    和
    references/ui-patterns.md
    中的 生命值条模式;分数则使用绑定到模块级变量的
    Label
    。
  • 定时居中的闪烁公告? → 使用模块级标志控制居中的
    Label
    ,并通过
    timers.setTimeout
    清除。参见
    references/ui-patterns.md
    中的 定时公告。
  • 滑块/拖拽手柄/拖动进度条? → **拖拽滑块可用。**UI 处理函数不会获得指针坐标,因此应在轨道上使用
    onMouseDown
    开始拖拽,再由一个 system 将
    PrimaryPointerInfo.screenDelta.x
    累加到数值中(除以 UI 缩放系数)。仅在拖拽期间渲染的、覆盖全屏且设置了
    pointerFilter: 'block'
    的叠加层可捕获释放事件。已在 Unity 和 Bevy explorer 的场景中完成实机验证。仅支持桌面端——移动端上的
    screenDelta
    始终为 0,因此应将轨道与
    -
    /
    +
    步进
    Button
    配合使用。完整实现参见
    {baseDir}/references/ui-sliders.md
    。
  • 自定义面板、背包、复杂布局? → 直接使用 React-ECS(参见
    references/ui-patterns.md
    )。

Troubleshooting

故障排除

Work through the wiring causes in this table in order before speculating about layout-level causes (sizing,
display: 'none'
, off-screen positioning, color-on-color) — wiring problems are the cause by a wide margin.
ProblemCauseSolution
UI not rendering / invisible / nothing on screen (most common)
setupUi()
is not called from
main()
in
src/index.ts
— users sometimes remove or comment out this call
Add the
setupUi()
call inside
main()
. Always check this first.
UI not rendering even though
setupUi()
is called
ReactEcsRenderer.setUiRenderer(...)
missing from
setupUi()
itself
Add
ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
UI blank on first frames, sometimes appears laterRoot component returns
null
(or falsy) on first render with no fallback
Render a placeholder or hidden root instead of returning
null
Multiple UIs fighting / UI missingMore than one
setUiRenderer
call — later calls replace earlier ones, so only the last one wins
Only call
setUiRenderer
once — combine all UI into a single root component, or use
addUiRenderer
with separate owner entities
Absolute-positioned children laid out unexpectedlyRoot
<UiEntity>
has no
width
/
height
— without a full-canvas root, some absolute-positioned children may not render
Add
uiTransform={{ width: '100%', height: '100%' }}
to the root — see "Convention" section below for empirical evidence.
UI elements overlappingMissing
flexDirection
or wrong layout
Set
flexDirection: 'column'
on the parent container
Button clicks not registeringMissing
onMouseDown
handler
Add
onMouseDown={() => { ... }}
to the Button or UiEntity
JSX errors at compile timeFile extension is
.ts
instead of
.tsx
Rename the file to
.tsx
Text not visibleText color matches backgroundSet contrasting
color
on Label or
uiText
在推测布局层面的原因(尺寸、
display: 'none'
、屏幕外定位、颜色相同)之前,请先按顺序检查下表中的连接问题。连接问题出现的概率远高于布局问题。
问题原因解决方案
UI 未渲染/不可见/屏幕上没有任何内容(最常见)未从
src/index.ts
的
main()
调用
setupUi()
——用户有时会删除或注释掉此调用
在
main()
内添加
setupUi()
调用。始终首先检查此项
即使调用了
setupUi()
,UI 仍未渲染
setupUi()
本身缺少
ReactEcsRenderer.setUiRenderer(...)
添加
ReactEcsRenderer.setUiRenderer(MyUI, { virtualWidth: 1920, virtualHeight: 1080 })
前几帧为空白,有时稍后才出现根组件首次渲染时返回
null
(或其他 falsy 值),且没有后备内容
渲染占位内容或隐藏的根元素,而不是返回
null
多个 UI 相互争用/UI 缺失调用了多次
setUiRenderer
——后一次调用会替换前一次调用,因此只有最后一次生效
仅调用一次
setUiRenderer
——将所有 UI 合并到单个根组件中,或使用
addUiRenderer
搭配不同的 owner entity
绝对定位的子元素布局异常根
<UiEntity>
没有
width
/
height
——没有全画布根元素时,某些绝对定位的子元素可能无法渲染
为根元素添加
uiTransform={{ width: '100%', height: '100%' }}
——有关实证依据,请参见下方的“约定”部分
UI 元素重叠缺少
flexDirection
或布局设置错误
在父容器上设置
flexDirection: 'column'
按钮点击没有响应缺少
onMouseDown
处理函数
在 Button 或 UiEntity 上添加
onMouseDown={() => { ... }}
编译时出现 JSX 错误文件扩展名是
.ts
而不是
.tsx
将文件重命名为
.tsx
文本不可见文本颜色与背景颜色相同在 Label 或
uiText
上设置具有对比度的
color

Convention: root
<UiEntity>
must set
width: '100%', height: '100%'

约定:根
<UiEntity>
必须设置
width: '100%', height: '100%'

Set
uiTransform={{ width: '100%', height: '100%' }}
on the root
<UiEntity>
returned to
setUiRenderer
/
addUiRenderer
whenever the UI uses absolute positioning. Do this by default.
Note: this is required specifically so absolute-positioned children get a full-screen positioning context. Some engine test scenes that lay everything out with flow/
margin
(no absolute children) use a smaller root (e.g.
90%
or
50%
) and render fine — but a full-canvas root is the safe default and never hurts.
Rationale (empirically verified — tested in-engine June 2026):
  • Without a full-canvas root, absolute-positioned children using
    position: { top, right }
    may fail to render entirely. In testing, a root with no explicit
    width
    /
    height
    caused a
    top-right
    positioned child to disappear while a
    bottom-left
    child rendered correctly. Adding
    width: '100%', height: '100%'
    to the root fixed the issue.
  • A full-canvas root gives absolute-positioned children (
    positionType: 'absolute'
    with
    position: { top, left, ... }
    ) a known, full-screen positioning context. This matches the implicit assumption most HUD code makes.
  • It avoids edge-case layout surprises with Yoga's default sizing for unspecified
    width
    /
    height
    .
只要 UI 使用了绝对定位,就必须在传给
setUiRenderer
/
addUiRenderer
的根
<UiEntity>
上设置
uiTransform={{ width: '100%', height: '100%' }}
。默认应始终这样做。
注意:这是专门为了确保绝对定位的子元素获得全屏定位上下文而要求的。部分引擎测试场景使用流式布局/
margin
布局(没有绝对定位的子元素),其根元素尺寸较小(例如
90%
或
50%
),也能正常渲染;但全画布根元素是安全的默认方案,不会带来任何负面影响。
理由(已通过实测验证——于 2026 年 6 月在引擎中测试):
  • 没有全画布根元素时,使用
    position: { top, right }
    的绝对定位子元素可能完全无法渲染。测试中,未显式设置
    width
    /
    height
    的根元素导致右上角定位的子元素消失,而左下角定位的子元素仍能正常渲染。添加
    width: '100%', height: '100%'
    后问题得到解决。
  • 全画布根元素可为绝对定位的子元素(
    positionType: 'absolute'
    配合
    position: { top, left, ... }
    )提供明确的全屏定位上下文。这符合大多数 HUD 代码的隐含假设。
  • 它可以避免 Yoga 对未指定
    width
    /
    height
    的默认尺寸计算所带来的边缘布局问题。

Example scenes

示例场景

Engine-team test scenes exercised against the real renderer (ground truth for the APIs above):
For full code examples and implementation patterns, see
{baseDir}/references/ui-patterns.md
. For component prop details, see
{baseDir}/references/ui-components.md
. For sliders and the limits of UI pointer input, see
{baseDir}/references/ui-sliders.md
.
引擎团队的测试场景已针对真实渲染器进行验证,是上述 API 的可靠依据:
有关完整代码示例和实现模式,请参见
{baseDir}/references/ui-patterns.md
。有关组件属性详情,请参见
{baseDir}/references/ui-components.md
。有关滑块和 UI 指针输入的限制,请参见
{baseDir}/references/ui-sliders.md
。

Cross-references

交叉引用

  • Platform detection: Use
    getPlatform()
    /
    isMobile()
    from
    @dcl/sdk/platform
    to branch UI for mobile vs. desktop. See the advanced-input skill.
  • Mobile UI limitations:
    borderRadius
    is unsupported on mobile. Design for touch (larger tap targets, no hover states). See the mobile considerations in the advanced-input skill.
  • 平台检测:使用
    @dcl/sdk/platform
    中的
    getPlatform()
    /
    isMobile()
    ,根据移动端或桌面端区分 UI。参见 advanced-input skill。
  • 移动端 UI 限制:移动端不支持
    borderRadius
    。应面向触控进行设计(使用更大的点击目标,不提供悬停状态)。参见 advanced-input skill 中的移动端注意事项。