关于 build-ui
build-ui 指导开发者使用 Decentraland SDK7 和 React-ECS 构建 2D 界面,默认采用 Creator Hub UI Designer 可编辑的代码约定;仅在编辑器无法表达时使用自由编码。
主要功能
- 01优先采用可编辑的 React-ECS 写法
- 02兼容 Creator Hub UI Designer
- 03指导编码式与混合式面板
- 04说明组件文件和聚合器约定
- 05列出编辑器解析限制与自检项
适用场景
- 01构建 HUD、菜单、对话框和通知
- 02创建可在 Creator Hub 中调整样式的界面
- 03处理超出编辑器约定的数据驱动面板
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill build-ui安装后在 Claude Code 中输入/build-ui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/adapting-coded-ui.mdcoded-ui.mdcomponent-template.mddrag-slider.mddriver-pattern.mdeditor-behavior.mdinteraction-helper.mdui-components.md详细介绍
使用 React-ECS 构建 UI
Decentraland SDK7 使用类 React 的 JSX 系统(@dcl/sdk/react-ecs)绘制 2D 覆盖层。编写这类代码有两种方式,默认采用可编辑方式:
| 模式 | 定义 | 适用场景 |
|---|---|---|
| 可编辑(默认) | 按照 Creator Hub 的 2D UI 编辑器(UI Designer)可解析、在画布上渲染并写回的约定编写 React-ECS:每个组件对应一个 src/ui/<Component>.tsx 文件、使用 state/props 绑定、useInteraction 层、@ui-action 处理器,以及放在 src/ui/ 之外的驱动程序中的动画 | 所有 HUD、菜单、对话框、Toast、通知、记分板、设置面板、生命值条、计时器、滑块——以及任何设计师之后可能想要在不改动代码的情况下重新设置样式的内容 |
| 代码编写(特殊情况) | 自由编写 React-ECS:对数据使用 .map()、条件子树、计算样式、辅助组件、任意文件布局 | 仅用于 UI 本身由数据驱动或动态生成,且其方式无法由约定表达的情况——请参阅 选择模式。详情:{baseDir}/references/coded-ui.md |
两种方式都是常规的 React-ECS,可以在任何地方运行;可编辑约定只是一个子集。不要询问用户想用哪种模式:除非 UI 触发了只能用代码编写的条件,否则选择可编辑模式;如果触发了,就说明原因。
何时使用哪种 UI 方案
| 需求 | 方案 | 参阅 |
|---|---|---|
| 屏幕空间 HUD、菜单、按钮、输入框 | React-ECS(本技能) | 下文 |
| 漂浮在世界中的 3D 文字 | TextShape + Billboard | advanced-rendering |
| 打开网页 | openExternalUrl | scene-runtime |
| 3D 空间中的可点击对象 | 指针事件 | add-interactivity |
| 可触发 InputActions 的屏幕按钮(移动设备) | 在 UiEntity 上使用 uiInputBinding + TouchScreenControls | {baseDir}/references/ui-components.md、advanced-input |
选择模式
默认选择可编辑。只有在 UI(或其中的一个面板)需要下列编辑器不支持的功能时,才改用代码编写:
- 根据运行时集合构建行(对玩家、库存物品、聊天消息使用
.map()),且行数在创作时未知。数量固定且不超过少数几行时,可以手动逐行编写,并保持可编辑。 - 每次调用都传入任意内容:函数每次调用都会渲染不同的结构或一组可选纹理(如
showPopup(title, body, imageSrc),其中有 N 张图片)。每个值占一个文本槽位、每张图片占一个texture.src,则仍可编辑。 - 带有子节点/插槽的通用封装组件(
<Card>{children}</Card>)——组件引用不接受子节点。 - 每行一个草稿对象、每行一个闭包、为非受控输入框使用重新挂载技巧。
几乎所有其他看似动态的内容——填充条、计时器、缓动动画、自动隐藏、随画布自适应的 px 尺寸、加载旋转图标——都可由驱动程序维护状态变量,并保持可编辑。
如果只有一个面板需要代码编写,就让其余部分保持可编辑,并从 src/ui/ 之外的模块使用 ReactEcsRenderer.addUiRenderer 单独挂载该面板({baseDir}/references/coded-ui.md → 混合场景)。告诉用户哪些部分无法编辑以及原因。改造现有的代码编写式 UI 时,应预期最终结果会更大(展开循环和变体是实现可编辑性的代价);应说明这一权衡,而不是只迁移一部分。
**前提条件:**使用 @dcl/sdk 7.26.0+——该版本提供 ScreenInsetArea / InteractableArea 和各设备默认虚拟屏幕。Creator Hub(0.50.0+)中的 UI 编辑器已稳定并始终启用:无需在 Settings 中开启 Experimental,也不存在 settings.guiEditor 键——不要告诉用户需要启用编辑器。低于 7.26.0 时,进入 2D 模式会显示 SDK 升级提示和 Update SDK 按钮,而不是画布。
编辑器如何读取代码
编辑器没有自己的保存格式:src/ui/ 下场景实际使用的 .tsx 文件就是文档。编辑器会将其解析为节点树,在画布上渲染,并立即将视觉编辑以最小文本片段的形式写回(徽标显示“All changes saved”;无需手动保存)。1 秒磁盘监视器会反映外部编辑,因此编辑器打开时,文件内容会实时更新。因此,“可编辑”是代码的可检查属性。解析器无法静态理解的代码会静默降级:
| 降级情况 | 触发条件 | 效果 |
|---|---|---|
| 冻结节点 | 任意 uiTransform / uiBackground 值,或任意 Label/Input/Dropdown 属性,既不是字面值,也不是裸 state.x / props.x 引用 | 节点会在画布上渲染,但面板会拒绝对此节点进行任何编辑 |
| 不透明节点 | 未知元素名称、除一个 useInteraction 常量外的展开语法、条件/逻辑/.map() 子元素、JSX 注释 | 灰色只读块;不会遍历其子元素,因此整个子树会从画布上消失 |
遵守以下约定即可避免这两种情况。影响代码编写的编辑器特性:
- **布局有两个相互独立的轴。**面板中的 Flow(row / column / Free)只写入
flexDirection——Free 表示该键不存在,而不是某个“free”值。Ignore Layout Flow 会写入positionType: 'absolute'。根节点始终为 absolute。将子节点拖到 Free 父节点下时,会根据放置点将其设为 absolute;将父节点切换为 Free 时,会在一次批量编辑中将每个现有子节点固定在其测量位置。 - Full Screen 小组件预设会插入
uiTransform={{ flexGrow: 1, alignSelf: 'stretch' }},不包含width/height(100%读回时是 Percent 单位,而非 Fill,且两个100%同级元素会超出 Yoga 的可用空间);在 Free 父节点下,它会使用positionType: 'absolute'和top/right/bottom/left: 0。对于需要由编辑器编辑的手写全屏包装元素,建议采用相同组合。字面形式100%×100%也可以解析——生成的聚合器根节点和经过游戏内验证的模板使用的就是这种形式。 - 在 Bevy 渲染器下,进入 2D 模式会冻结场景(返回 3D 后恢复)。用户布置 UI 时,驱动程序的时钟不会前进。
关于编辑器 UI 的其他所有内容(Scene Inset 下拉菜单、Opacity 命名、画布工具、模式持久化、移动端预览的参考区域及其限制):{baseDir}/references/editor-behavior.md。
设置(可编辑)
文件布局
src/ui/
index.tsx <- GENERATED aggregator. Never hand-edit.
interaction.tsx <- reserved helper (useInteraction). Verbatim.
platform.tsx <- reserved helper (usePlatform). Verbatim.
MyHud.tsx <- a top-level UI root (rendered by the aggregator)
KitToast.tsx <- a reusable component (marked /** @ui-component */)
src/ui-behaviors.ts <- driver: outside src/ui/, never parsed by the editor
src/mobile-hud.ts <- GENERATED by the MobileHUD panel. Editor-owned. Never hand-edit.
新建 UI 的步骤:
- 每个文件放一个组件:
src/ui/<ComponentName>.tsx。文件名必须是有效的 PascalCase 标识符,且必须与导出的函数名相同——编辑器会忽略其他文件。每个文件恰好导出一个组件(编辑器会读取第一个返回 JSX 的导出函数)。每个文件都以/** @jsx ReactEcs.createElement */开头,并从@dcl/sdk/react-ecs导入ReactEcs。只使用//注释——{/* JSX comments */}会被视为不透明内容。 - 从
{baseDir}/references/interaction-helper.md原样创建src/ui/interaction.tsx,并在有平台变体时创建src/ui/platform.tsx。小写名称可使这些文件不出现在根目录列表中;编辑器会自行创建它们,且不会将其列为 UI。 - 按照契约编写每个组件。以
{baseDir}/references/component-template.md为模板。 - 按照下方生成的确切结构编写
src/ui/index.tsx,并在src/index.ts的main()中调用setupUi()。(编辑器会自行完成这项接线——它会取消模板中//setupUi()行的注释并添加导入;但手动编写时,请自行完成。) - 将所有时钟、缓动、计时器、格式化器和状态机放在
src/ui/之外的.ts文件中——参见驱动程序模式。 - 对每个文件执行自检清单。
首次打开场景时,旧版单文件 src/ui.tsx 会备份为 src/ui.tsx.bak,然后被删除——始终在 src/ui/ 下编写代码。要移植已有的代码 UI,请遵循 {baseDir}/references/adapting-coded-ui.md(按顺序处理阻碍项:将 .map() 展开;将条件判断改为隐藏元素上的 active 显示门控;将计算样式移至驱动程序变量;将插值文本改为分段绑定;将手动跟踪悬停改为图层;最后在所有位置显式指定框)。
SDK 模板已在 tsconfig.json 中包含 JSX 设置——不要修改它。
聚合器(src/ui/index.tsx)
根据顶层根节点列表生成;每当编辑器打开场景,或添加、重命名、移除或重新插入根节点时,都会重写——任何手动编辑都会丢失。请原样输出:
/** @jsx ReactEcs.createElement */
import ReactEcs, { UiEntity, ReactEcsRenderer, ScreenInsetArea } from '@dcl/sdk/react-ecs'
import { MyHud } from './MyHud'
export function setupUi() {
ReactEcsRenderer.setUiRenderer(() => (
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
<ScreenInsetArea>
<MyHud />
</ScreenInsetArea>
</UiEntity>
))
}
屏幕内边距是每个根节点单独选择的(编辑器的 Scene Inset 下拉菜单);device 是默认值。已对照 Creator Hub 的聚合器生成器(UIDesigner/code/aggregator.ts)和 @dcl/react-ecs 的 system.ts 验证:
| 编辑器内边距 | index.tsx 中的包装器 | 适用场景 |
|---|---|---|
device(默认值) | <ScreenInsetArea> | 普通场景 UI——限制在设备安全区域内(刘海、状态栏、主屏幕指示条、圆角);桌面端内边距为零 |
interactable | <InteractableArea> | 还必须避开浏览器自身屏幕控件(小地图、聊天、左侧控件)的 UI |
none | 裸露的 <Component /> | 信箱黑边、全屏背景,以及有意绘制在平台 UI 所在区域的内容 |
关于这种生成结构,有两点需要注意:
- 调用
setUiRenderer时不带任何选项,因此场景会使用 SDK 平台默认虚拟画布——桌面端为1920x1080,移动端为1600x720——以及渲染器级别的screenInset默认值'device'。切勿手动添加virtualWidth/virtualHeight(重新生成时会将它们移除);请根据这两种画布设计像素值。这里是唯一无法遵守“始终显式传入虚拟尺寸”这一编码 UI 规则的地方。 - 如果省略
screenInset,渲染器已经会用自己的ScreenInsetArea包装整个 UI,而聚合器的包装器会嵌套在其内部。绝对定位是相对于父级框计算的,因此在内边距非零的手机上,device根节点会应用两次内边距,interactable根节点会应用设备内边距 + 可交互区域内边距,而none根节点仍会应用一次渲染器级别的设备内边距,而非整个画布。在桌面端,内边距为零,因此不会有任何变化。这是根据@dcl/react-ecs中的wrapWithScreenInset和 Creator Hub 的aggregator.ts得出的;尚未在设备上测量。请勿在index.tsx中“修复”此问题(该文件会重新生成)。如果移动端需要精确放置在安全区域内,请为该根节点选择none并进行测量。
这两个包装器元素只能放在 index.tsx 中——在组件文件内部,解析器无法识别它们的名称,会导致子树变得不透明。
元素
只对五种元素名称进行建模:UiEntity、Label、Input、Dropdown、Button,以及对另一个 src/ui/ 文件的引用(<KitToast />),这是获准的复用单元。其他任何元素名称(本地辅助组件、库组件、ScreenInsetArea)都视为不透明内容。完整属性表:{baseDir}/references/ui-components.md。
UiEntity—— 容器。uiTransform(width、height、min/max、positionType、position、display、flexDirection、justifyContent、alignItems、alignContent、alignSelf、flexWrap、flexGrow、overflow、padding、margin、opacity、zIndex、borderWidth/Color/Radius、pointerFilter)、uiBackground(color、texture、textureMode、textureSlices、uvs、avatarTexture)、uiInputBinding,以及四个监听器onMouseDown/onMouseUp/onMouseEnter/onMouseLeave。Label/Button—— 只有这两种元素可承载文本:value、fontSize、textAlign、color、font、textWrap;Button还支持variant('primary'|'secondary')、disabled、onMouseUp。UiEntity上的uiText={{…}}属性包未纳入建模——应将其重构为Label子元素(编码后的 UI 仍可使用它)。value支持<b>等简单标记。Input——placeholder、value、color、placeholderColor、disabled、textAlign、font、fontSize、onChange、onSubmit。它不是 React 受控组件,参见注意事项。Dropdown——acceptEmpty、emptyLabel、options(string[])、selectedIndex、disabled、color、textAlign、font、fontSize、onChange(index)。- 组件引用
<KitToast … />—— 可选择、可移动,其属性可按实例编辑。无法从外部移动或调整大小:为每个实例提供一个设置了明确width/height以及其 margin/position 的包装UiEntity。不接受嵌套的 JSX 子元素。
这四个监听器构成 UI 事件处理器的完整集合,每个签名均为 () => void:没有 onMouseDrag / onMouseMove,没有事件对象,没有指针坐标,并且全部硬编码为 InputAction.IA_POINTER。拖动交互仍可通过 PrimaryPointerInfo.screenDelta 实现,参见 {baseDir}/references/drag-slider.md。
契约
状态:绑定接口
export interface State {
score: number
label: string
visible: boolean
panelWidth: number
labelColor: { r: number; g: number; b: number; a: number }
options: string[]
}
export const state: State = {
score: 0, label: '00.00', visible: false, panelWidth: 320,
labelColor: { r: 1, g: 1, b: 1, a: 1 }, options: ['A', 'B'],
}
模块级 export interface State + export const state: State 是可识别的签名;每个属性都是可编辑变量。类型包括:number、string、boolean、Color4(以 { r; g; b; a } 的形式进行结构性标注——通过是否包含 r / g / b 成员来匹配,而不是根据 Color4 这个名称)和 string[]。number[](例如 uvs 四元组)可以绑定并正常工作,但面板会将其误标为字符串列表。
state是一个普通的导出对象——驱动程序正是借此修改它。- 模块状态由同一文件的所有实例共享。每个实例的值存放在父级中,并作为 props 传入。
- 场景 UI 中不能使用 React hooks(
useState、useEffect);状态必须位于模块级。UI 每帧都会重新渲染,因此变更会立即显示。
样式绑定
uiTransform / uiBackground 中任何值为裸引用的键——state.x 或 props.x,不含运算符、函数调用或字符串拼接——都是可编辑绑定:
uiTransform={{ width: state.panelWidth, position: { top: state.panelTop }, borderColor: state.frameColor }}
uiBackground={{ color: state.panelColor, texture: { src: state.iconSrc } }}
可识别的位置:顶层键、边缘分组的成员(position / margin / padding → { left: state.x })、整个分组(borderColor: state.c),以及点分路径 texture.src / avatarTexture.userId。字面值与绑定值可自由混用;嵌套深度为一层,与 react-ecs 本身一致。元素属性也以相同方式绑定:value={state.label}、color={state.labelColor}、fontSize={state.size}、selectedIndex={state.i}、options={state.options}。
混合文本
插值部分全部为裸引用的模板字符串会按顺序往返保留为字面文本/绑定片段:
<Label value={`Score: <b>${state.score}</b>`} … />
<Label value={`${state.mins}:${state.secs}`} … />
<Label value={`env: ${state.realm}\nplayers: ${state.count}`} … />
只要有一个计算型插值(${state.a + 1}、${fmt(state.t)}),该节点就会被冻结——请在驱动程序中完成格式化,再插入最终值。(每个真实的 Label 还必须显式设置 uiTransform 盒子,参见尺寸与移动端。)
Actions(事件处理程序)
type UiAction = { state: State; props: Parameters<typeof MyHud>[0]; value?: unknown }
/** @ui-action */
function openPanel({ state }: UiAction) {
state.visible = true
}
使用标准 thunk onMouseDown={() => openPanel({ state, props })} 绑定事件;对于带值的事件,使用 onChange={(value) => setName({ state, props, value })}。可识别的事件包括:onMouseDown、onMouseUp、onMouseEnter、onMouseLeave,以及 Input / Dropdown 上的 onChange / onSubmit。处理程序的主体可以是任意代码;无法识别的处理程序表达式(带代码块主体的内联箭头函数)只会被视为未绑定,不会冻结节点。Actions 会同步修改状态——任何基于时间的行为都应通过标志由驱动程序在下一帧处理。
交互层
useInteraction 是用于按状态设置样式的可识别结构。各层按 base → active → hover → press 的优先级深度合并;第二个参数用于驱动 active,可以是任意表达式(按原样存储)。
const panel = useInteraction(
{
base: { uiTransform: { display: 'flex', width: 320, height: 200 } },
active: { uiTransform: { display: 'none' } },
},
state.visible !== true,
)
return <UiEntity {...panel}>…</UiEntity>
- 可见性始终通过
active显示门控控制。 不要使用{state.visible && <X/>}(不透明),也不要使用display: state.visible ? 'flex' : 'none'(会被冻结)。元素会被隐藏,而不是卸载——画布渲染base层,因此编辑时所有状态都会堆叠显示;这是预期行为。 - 悬停 / 按压反馈始终使用
hover/press层,不要在onMouseEnter/onMouseLeave上手动跟踪布尔值。 {...someInteractionConst}是解析器唯一接受的展开语法。它旁边可以放置其他属性(<UiEntity {...panel} onMouseDown={…}>),且其他属性优先。- 该展开会无条件附带全部四个指针监听器——即使是仅含
base层和active可见性门控时也是如此。应将门控放在面板上,也就是该判断所作用的最小元素上,切勿放在全屏包装元素上——参见指针阻挡。 - 对于需要动画退出的 UI,请使用两个变量:
visible(意图,由 actions 切换)和hidden(渲染门控,仅在退出动画结束后由驱动程序设置)。使用state.hidden === true作为门控条件。
组件 props
/** @ui-component */
export function KitToast(props: { message?: string; fillPx?: number; on?: boolean; onClose?: (value?: unknown) => void }) {
- 导出函数前的标记会将文件设为可复用组件(仅在其他根节点嵌套它时才会渲染)。没有该标记时,文件就是由聚合器渲染的顶层根节点。
- Props 是唯一
props参数上的内联对象类型;支持的类型为number、string、boolean和回调(value?: unknown) => void。其他类型会显示为只读。始终将 props 声明为可选。 - 在组件内部,
props.x会加入绑定范围:样式键、文本值和active表达式(props.active === true)。 - 不支持颜色、纹理集或 children prop。无法通过
variantprop 选择颜色——应将每个视觉变体展开为同级元素,并通过active显示层进行门控({baseDir}/references/component-template.md§6)。 value={props.label}不符合严格 TS(string | undefined);应使用value={`${props.label}`}——这仍是可识别的绑定(如果父级未提供该 prop,则会渲染undefined)。- 使用单行 action 向上转发子组件的回调:
/** @ui-action */ function forwardClose({ props }: UiAction) { props.onClose?.() }。
平台变体——唯一的结构性条件语句
const platform = usePlatform()
return platform === 'mobile' ? <PhoneMenu /> : <DesktopBar />
可在组件的 return 中以及作为 JSX 子元素使用;!==、反转操作数以及内联 usePlatform() === 'mobile' 都可以解析。两个分支都必须是单个 JSX 元素或字面量 null,并且至少一个分支必须是元素。其实现依赖预留的 src/ui/platform.tsx。仅在设备间结构确实不同时使用它(例如使用底部抽屉而不是侧边栏);系统不会为每个属性建模覆盖——虚拟画布本身已按比例缩放。
无法表达的内容
| 无法表达 | 替代方案 |
|---|---|
循环 / 对数据使用 .map() | 手动展开每个元素;如果某一行重复超过约 3 次,就将其提取到单独的 @ui-component 文件中 |
共享主题常量(color: THEME.primary) | 在每处内联 { r, g, b, a } 字面量——样式对象中的任何标识符都会冻结节点;更改调色板时,使用查找并替换 |
计算样式值(算术、Math.*、调用、拼接、三元表达式) | 每个派生值使用一个由驱动程序维护的状态变量 |
百分比字符串绑定(width: state.pct + '%') | 绑定 px 数值;静态百分比字面量(width: '90%')可以使用 |
条件元素属性(disabled={state.i === -1}) | 预先计算的布尔状态变量 |
| 同一文件中的本地辅助组件 | 单独创建一个 src/ui/ 文件,并标记为 /** @ui-component */ |
| 组件的子元素 / 插槽 | 将布局保留在屏幕文件中内联;仅提取叶子小部件 |
| 作为属性传入的颜色或纹理集 | 在组件内部展开各个变体(texture.src 可以绑定到字符串属性;uvs 不可以) |
| 每行带有草稿数据和闭包的数据驱动行 | 不可移植——将代码编写的 UI 放在 src/ui/ 之外的独立模块中 |
九切片背景(textureMode: 'nine-slices' + textureSlices)、字面量 8 浮点数 uvs 图集裁剪、overflow: 'scroll'(静态行仍可滚动)、pointerFilter、边框属性以及整个 flex 模型都支持往返转换。
驱动程序模式
编辑器不会解析 src/ui/ 之外的文件,而 state 是一个普通的导出对象:编辑器管理结构、样式和其余值;驱动程序管理时钟和计算。
// src/ui-behaviors.ts — outside src/ui/, invisible to the editor
import { engine } from '@dcl/sdk/ecs'
import { state as panel } from './ui/MyPanel'
const OPEN_WIDTH = 480
let anim = 0
export function registerUiBehaviors() {
engine.addSystem((dt: number) => {
const target = panel.visible ? 1 : 0
anim = Math.max(0, Math.min(1, anim + (target > anim ? dt : -dt) / 0.25))
const t = 1 - Math.pow(1 - anim, 3) // easeOutCubic
panel.panelWidth = Math.max(1, Math.round(OPEN_WIDTH * t))
panel.textColor.a = t
if (target === 0 && anim <= 0) panel.hidden = true // release the display gate
})
}
在 main() 中与 setupUi() 一起注册。state 中每个绑定变量的初始值都是其可设计的静止状态——在注册时捕获该值,并围绕它进行动画处理,这样设计人员就能在面板中调整样式,而无需修改驱动程序。驱动程序负责:时钟、补间、缓动、计时器和自动隐藏截止时间(使用 @dcl/sdk/ecs 中的 timers.setTimeout,不要使用 JS 全局对象)、padStart / 舍入 / 文本格式化、状态机、派生值(从百分比计算 px、根据 UiCanvasInformation 计算画布响应式尺寸)、拖动累积、TouchScreenControls 开关,以及任何读取 ECS 的内容。动画本身没有对应的编辑器表示——编辑器只能看到绑定键及其静止值。完整示例:{baseDir}/references/driver-pattern.md。
常用小部件
使用基础元素构建所有小部件——没有小部件库。优先使用可编辑模板;代码编写的备用方案见 {baseDir}/references/coded-ui.md。
| 需求 | 可编辑模式 |
|---|---|
| 对话框 / 提示 / 确认 | 全屏定位包装器 + 受控显示的面板 + 按钮:{baseDir}/references/component-template.md §4(添加第二个按钮以供接受 / 拒绝) |
| Toast / 通知 / 弹出窗口 | 带 visible 属性的 KitToast 组件(§2);自动隐藏使用驱动程序倒计时(driver-pattern.md,*AutoHide) |
| 定时公告 / 闪现消息 | 带一个 Label 的受控显示面板;驱动程序递减 hideIn 并切换 visible |
| 分数 / 计数器 / 计时器读数 | 混合文本 Label(§3);驱动程序负责格式化字符串(driver-pattern.md §2) |
| 生命值 / 进度 / 填充条 | KitProgressBar(§5):驱动程序根据相对于轨道字面量宽度的百分比设置 fillPx |
| 菜单 / 带悬停效果的按钮 | hover / press 图层(§3) |
设置表单(Input / Dropdown) | 带 value 的 @ui-action |
| 滑块 / 拖动条 / 拖动手柄 | {baseDir}/references/drag-slider.md(点按步进区域 + 拖动,受控显示的释放捕获区) |
| 暂停游戏的全屏模态框 | 受控显示的面板 + 驱动程序切换 TouchScreenControls(参见位置) |
| 物品栏网格 / 长度未知的列表 | 使用代码实现——见 coded-ui.md |
尺寸与移动端
每个尺寸都必须显式声明。 在渲染此代码的三种界面中,文本固有尺寸和根据子项自动调整尺寸都不可靠:
| 界面 | 未设置 Label 尺寸/自动调整尺寸的父级 |
|---|---|
| Bevy explorer | 测量渲染后的文本,并将其传入 flex 布局——显示正常 |
| Unity explorer | 对布局的贡献约为 0;字形仍会绘制在零高度节点上 → 标签堆叠后相互重叠,父级收缩至仅剩内边距 |
| Creator Hub canvas | 未设置尺寸的组件根节点或 ref 包装器会渲染为 0(实例收缩,面板显示高度为 0),即使 Yoga 在运行时能正确布局 |
在场景中通过并排截图验证:一个宽 720 px 的对话框,其中两个标签设置了 width: '100%'、textWrap="wrap",但未设置 height,且所在面板也未设置 height;在 Bevy 上显示正常,在 Unity 上则被压扁——两个标签绘制在彼此之上,面板收缩至仅剩内边距和按钮。修复方法:将面板 height 设为 210,名称标签 height 设为 30,换行正文标签 height 设为 60,按钮标签设为 100%/100%。在一个界面上的预览正确,并不能说明其他界面也正确。规则如下:
- 每个
/** @ui-component */文件的根节点都要声明width和height(px 数值或百分比字面量)。组件 ref 外的每个包装器UiEntity也要如此,并与组件根节点尺寸匹配——只带margin/position的包装器同样会收缩。 - 每个
Label(以及Button文本)都要在uiTransform中声明width和height;每个堆叠标签的容器都要显式设置高度。换行文本:高度 = 行数 × 行高(fontSize: 20时,两行 → 60)。填满固定父级的标签使用100%/100%。 - 每个绑定的尺寸或位置都必须是 px
number,不得使用算术表达式或百分比字符串。未绑定键中的静态百分比字面量才适合用于流式布局。 - 针对桌面端
1920x1080和移动端1600x720进行设计。移动端画布的高度少 33%:将元素锚定到边缘,并在可变化的轴上使用 flex/百分比布局,不要使用针对某个高度计算的绝对偏移量。7.26.0+ 中不要再应用旧的“移动端尺寸放大约 3 倍”规则——手机上的像素 UI 本来就大约放大了 2–3 倍(devicePixelRatio不再参与缩放系数),而移动端画布还会额外放大约 1.2 倍;从桌面端尺寸开始,只放大在设备上实际显示偏小的元素。 - 虚拟画布上的触控目标至少约为 48 px;正文
fontSize至少为 16,移动时需要阅读的内容至少为 20;任何可能增长的标签都要设置textWrap="wrap"和显式宽度。 - 任何文本值都不得包含 emoji 或装饰性 Unicode 字符(
Label/Button的value、uiText.value、Input的placeholder、Dropdown选项、绑定字符串)。字形覆盖取决于各 explorer 捆绑的字体,而非 SDK;Unity explorer 不包含 emoji 字形——已在场景中验证:value="✨ Particles"中的闪光符号会消失。可在标签旁放置一个小型UiEntity,并使用uiBackground={{ texture: { src: 'images/icon.png' } }},或通过uvs使用图集精灵;texture.src是绑定,因此图标仍可编辑。文本只使用 ASCII 字符,以及文案所需的重音字母。 - 纹理
src路径相对于场景根目录('images/panel.png'),而非src/。 - 移动端不支持
borderRadius。触摸操作下悬停层不起作用——绝不能将其作为唯一的提示方式或唯一的值读取方式。
摆放位置:切勿锚定在左上角;移动端 UI 要避开操作按钮
生成场景时最常见的布局错误,是将面板固定在左上角(position: { top: 0, left: 0 },或让子项从原点开始排列的全画布根节点)。左上角以及桌面端的整个左边缘,都是浏览器自身绘制 UI 的位置(小地图、聊天窗口,以及移动端的虚拟摇杆)。放在那里的场景 UI 会被客户端 HUD 遮挡,按钮经常无法点击,而且没有任何警告。按优先顺序:
- 小型 HUD(分数、计时器、几个按钮) → 锚定到右侧边缘或将其居中:
positionType: 'absolute', position: { top: 40, right: 40 },或在根节点上使用justifyContent: 'center'。切勿将面板留在原点;没有可交互内边距时,不要使用left: 0。 - 需要可靠的安全区域 → 使用
interactable内边距(可视化编辑:根节点的 Scene Inset;代码编写:在渲染器上设置screenInset: 'interactable',需 7.26.0 或更高版本;低于此版本时,用<InteractableArea>包裹)。它会避开小地图、聊天窗口和左侧控件,因此在桌面端,其内部的left: 0是安全的。它仅避开左侧列,右边缘与设备区域共用,所以右下角操作按钮组按设计位于其范围内。 - 移动端:右下角操作按钮(跳跃、E、F、指针)绘制在 interactable 区域上方,该处的点击会交给客户端处理。游戏内 HUD 应保持在该区域之外:锚定在右上方或顶部居中;移动端右下角不要放置交互元素(读取
UiCanvasInformation.interactableArea可获取精确边距;按钮槽位是固定的)。会中断游戏流程的 UI(记分板、结算界面、商店、规则卡片)打开时应隐藏触控控件——打开时调用TouchScreenControls.hideAll()和hideJoystick(),关闭时调用showAll()和showJoystick()(桌面端调用无效;showAll()还会清除自定义图标,需重新应用),也可以在此期间对engine.PlayerEntity使用InputModifier的disableAll(桌面端和移动端客户端均适用;移动端文档建议用于此情况;在旧版 Web 浏览器中无效)。所有关闭路径都必须调用同一个函数,确保控件不会一直处于隐藏状态。可视化编辑:驱动程序会在state.visible变化时执行此操作;代码模式:{baseDir}/references/coded-ui.md→ 全屏模态框。如果场景使用 UI Designer 的 MobileHUD 面板,此运行时切换会与面板的静态调用冲突——参见 MobileHUD。 需要玩家按键关闭的卡片,必须在移动端提供点击操作方式。 标题、结尾或“按 E 继续”卡片如果只能通过IA_PRIMARY推进,那么在隐藏触控控件后,手机上就会卡住——原生 E 按钮是唯一的继续方式(拥有者于 2026-10-12 报告:“无法按 E 按钮”)。将相同操作绑定到 UI:在可见的继续按钮上使用uiInputBinding={{ actions: [InputAction.IA_PRIMARY] }},或者绑定到卡片本身,让玩家点击任意位置即可继续({baseDir}/references/ui-components.md→ uiInputBinding:该元素会像按下按键一样触发操作,因此现有的inputSystem检查仍可正常工作)。如果卡片本身没有可点击的控件,则保持触控控件可见。按钮方式是文档记录的移动端模式;任意位置点击的方式尚未在设备上验证。
用于说明游戏玩法的面板,其内容规则:一屏显示,不滚动,包含 3–5 行简短文字;展示(图标、示意图、高亮显示的首个目标),而非仅用文字说明——game-design 技能 → “规则:展示,不要只说”。
指针拦截
带有任何监听器的 UI 元素,或设置了 pointerFilter: 'block' 的 UI 元素,都会在其整个矩形区域内捕获指针输入,而不只是可见像素,并阻止点击传到 3D 世界和其后方的所有 UI 元素。透明背景不会改变这一点。没有监听器且使用默认值 pointerFilter: 'none' 的元素会让点击穿透。
绝不要在尺寸为 100%×100% 的包装元素上添加处理程序、{...useInteraction} 展开,或 pointerFilter: 'block'。 它的矩形区域就是整个屏幕,因此布局根节点上只要有一个多余的监听器,就会导致玩家无法点击任何其他 UI 元素或世界中的任何东西——但 UI 看起来仍然正常,因为可见面板很小。展开也会产生这一效果,因为 useInteraction 始终会返回全部四个监听器,即使只将它用作可见性门控也是如此。将处理程序和门控添加到确实需要它们的最小元素上(面板、按钮、行);布局包装元素应保持为仅用于定位、采用字面样式的 UiEntity。每次添加处理程序时都要检查这一点。前后对照:{baseDir}/references/component-template.md §4。
有两种合理的全屏拦截层,而且都必须是有意设置并经过门控的,而不能是副作用:打开时用于拦截点击的模态背景层,以及仅在拖动期间显示的拖动释放拦截层({baseDir}/references/drag-slider.md)。在可编辑 UI 中,门控是设置为 display: 'none' 的 active 层;在代码编写的 UI 中,则使用条件渲染。
MobileHUD(src/mobile-hud.ts)——由编辑器管理,不要手写
Creator Hub 0.50.0+。MobileHUD 是 UI Designer 的 GUI 栏中固定的第一项(场景至少有一个 GUI 后才会显示)。它通过只读画布预览来编辑场景的 TouchScreenControls——原生移动摇杆、准星和游戏手柄按钮。它不是 react-ecs 根节点:它是在 src/mobile-hud.ts 中写入的一条 TouchScreenControls.createOrReplace(engine.RootEntity, { … }) 调用(位于 src/ui/ 之外,因此根节点扫描器不会将其视为 GUI),导出为 setupMobileHud,并在 src/index.ts 中与 setupUi() 一起导入。
- 绝不要手写或编辑
src/mobile-hud.ts。 面板会根据自身对该精确格式的解析重新生成此文件;无法解析的代码会被静默还原为默认值。 - 在此类场景中的其他位置,绝不要再写第二条
TouchScreenControls.createOrReplace——engine.RootEntity上只会保留一个组件,并且最后一次写入生效。 - 该文件会按需写入并自动删除:只有配置偏离 SDK 默认值时才会存在。没有此文件的场景使用原生默认行为。
- 若要更改 UI Designer 场景中的移动 HUD,请在面板中更改。真正的运行时控制(例如在过场或模态期间隐藏按钮)应放在
src/ui/之外的驱动程序中,但这会与面板的静态调用冲突——组件 API 见 advanced-input。
面板提供的选项及 protobuf 注意事项:{baseDir}/references/editor-behavior.md → MobileHUD。
编码式 UI(边界情况)
当选择模式将 UI 设为编码式时,上文关于元素、尺寸、位置和指针拦截的所有内容仍然适用。变化如下:
- 文件布局不限(
src/ui.tsx单文件,或放在src/ui/之外、可编辑根目录旁的模块)。条件渲染、.map()、辅助组件、uiText对象、计算样式和主题常量都可以使用。 - 始终将
{ virtualWidth: 1920, virtualHeight: 1080 }传给setUiRenderer/addUiRenderer。这会固定参考分辨率,且在 7.26.0 版本分界(下文)两侧的行为相同(低于此版本时,省略尺寸会禁用缩放;在 7.26.0+ 中,移动端无论如何都会将 16:9 尺寸覆盖为1600x720)。绝不要只指定一个维度。screenInset和渲染器的zIndex选项(7.29.0+)不兼容所有版本。 - 根
<UiEntity>设置width: '100%', height: '100%'(这是可靠绝对定位的必要条件),并保持指针透明。 - 每个场景只能调用一次
setUiRenderer——第二次调用会静默覆盖第一次。独立模块使用addUiRenderer(ownerEntity, Widget, options),并通过removeUiRenderer(owner)移除,或在所属实体销毁时自动移除;编码式面板与可编辑的src/ui/共存时也应如此,因为其聚合器拥有setUiRenderer。
虚拟屏幕和版本分界表、混合场景、带条件渲染的模块级状态,以及从零开始构建 Widget 的模式:{baseDir}/references/coded-ui.md。
注意事项(已通过引擎测试场景验证)
Input和Dropdown不是 React 受控组件。onChange/onSubmit触发时会传入当前值,但字段不会在每一帧重新读取value/selectedIndex。要清空Input,先将value设为一个非空哨兵值(' ')一帧,再设为''。请以场景状态为准,不要依赖渲染后的字段:受控Input不会响应程序化重置(客户端的未解决问题),之后再次写入相同字符串也不会触发onChange。onSubmit(Enter)会提交并清空字段,且客户端先触发onSubmit,然后触发onChange。在Input自身的处理器中移除已聚焦的Input是安全的。Dropdown的onChange接收索引;使用acceptEmpty时,空条目的索引会发生偏移。详情:{baseDir}/references/ui-components.md→ Input。Button的disabled只会将文本/背景透明度减半,并阻止onMouseDown/onMouseUp触发——onMouseEnter/onMouseLeave仍会触发。zIndex仅对同级元素组生效——它不会将元素提升到其他分支之上。要控制跨分支的堆叠顺序,请使用树顺序(或 coded UI 中的数组返回顺序);要堆叠整个渲染器,请使用渲染器的zIndex选项(ui-components.md→ Renderer zIndex;Admin Tools 智能项位于 1000)。opacity会沿树向下相乘。 根节点透明度为 0.5 时,内部透明度为 0.8 的子节点最终渲染透明度为 0.4。textureMode: 'stretch'会使非均匀图像变形;面板/按钮请使用'nine-slices'+textureSlices,原生尺寸请使用'center'。与texture并列的color会为其着色。⚠️ 九宫格切片在移动端仍未实现(截至 2026 年 10 月,尚无预计完成时间):移动客户端会平铺纹理,而不是拉伸中心区域,因此在桌面端显示正常的九宫格面板,在手机上会呈现重复图案。对于面向移动端的场景,面板背景建议使用纯色color,或使用经过设计、平铺后仍能呈现预期效果的图像。- Insets 不会裁剪内容。
ScreenInsetArea/InteractableArea不会设置overflow,因此定位在 inset 区域外的内容会渲染到预留区域中。内容越过安全区轮廓是需要修复的布局错误,不应通过overflow: 'hidden'隐藏。 - 在 7.26.0+ 版本中,手动放置的
ScreenInsetArea/InteractableArea会与渲染器自身的screenInset(默认值为'device')叠加——自行放置包装器时,请传入screenInset: 'none'。低于 7.26.0 的版本中,包装器是唯一的实现方式。自动生成的聚合器是无法更改的例外(参见 The aggregator)。 UiCanvasInformation.width/height是画布的原始像素尺寸,应用于决策(选择布局、纹理),而非用于计算尺寸。
故障排查
请先排查接线原因——绝大多数问题都由此引起。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| UI 未渲染/屏幕上什么也没有(最常见) | 未从 src/index.ts 中的 main() 调用 setupUi() | 添加调用。务必先检查这一项 |
已调用 setupUi(),但 UI 仍未渲染 | setupUi() 中缺少 ReactEcsRenderer.setUiRenderer(...) | 恢复聚合器结构(可编辑模式)或规范调用(代码模式) |
| UI 在最初几帧为空白,之后才显示 | 根组件在首次渲染时返回 null/假值 | 改为渲染占位内容或带条件门控的根组件 |
| 多个 UI 相互冲突/UI 缺失 | 存在多个 setUiRenderer——最后一个会静默生效 | 只保留一个 setUiRenderer;其他模块使用 addUiRenderer,并为其设置各自的所有者实体 |
| 绝对定位的子元素布局异常 | 根 <UiEntity> 缺少 width: '100%', height: '100%' | 添加该属性(经实际验证为必需——没有它,top-right 子元素会消失) |
| UI 元素重叠 | 缺少 flexDirection,或标签没有布局框 | 在父元素上设置 flexDirection: 'column';为 Label 明确设置布局框 |
| 左上角或左侧边缘的按钮可见但无法点击,或部分被遮挡 | 场景 UI 位于探索器 HUD(小地图、聊天、移动摇杆)下方,而这些控件会拦截点击 | 将锚点设在右侧或居中,或使用 interactable 内边距——参见 位置 |
| 移动端:点击大面板右下方无响应/触发跳跃或 E | 原生操作按钮绘制在可交互区域上方 | 避免将可交互元素放在右下角,或在打开全屏 UI 时隐藏触控控件 |
| 按钮点击未注册 | 缺少 onMouseDown | 将其添加到 Button/UiEntity(可编辑 UI 中使用 @ui-action thunk) |
| 屏幕上所有内容都无法点击了 | 100%×100% 的包装元素带有监听器、useInteraction 展开属性或 pointerFilter: 'block' | 从包装元素中移除这些属性;将门控逻辑/处理程序移到面板/按钮上 |
| 在一个探索器中看起来正常,但标签重叠/在 Unity 中面板被挤压 | Label 没有明确设置布局框,容器根据文本自动调整大小 | 为每个标签及每个堆叠标签的容器明确设置 width/height |
| 字符串部分缺失或显示为 □ | 表情符号/装饰性 Unicode 字符 | 移除;改用纹理图标 |
| 编译时出现 JSX 错误 | 文件是 .ts 而非 .tsx | 重命名 |
| 文本不可见 | 颜色与背景相同 | 使用对比鲜明的 color |
| 组件在编辑器画布上看起来被压扁/高度为 0,但在场景中正常 | 组件根元素或 ref 包装元素未明确设置 width/height | 同时声明两者 |
| 节点上的所有编辑操作都被拒绝(冻结) | uiTransform/uiBackground/Label 属性中包含计算值、标识符或函数调用 | 使用字面值或裸 state.x/props.x;将计算移到驱动程序中 |
| 子树未显示在画布上(不透明) | {cond && …}、三元表达式、.map()、JSX 注释、未知元素、非 useInteraction 展开属性 | 按 无法表达的内容 中所述替换 |
| 编辑器显示“UI Editor Unavailable” | @dcl/sdk 版本低于 7.26.0 | 更新场景的 SDK |
| 拖动滑块抖动/速度不对 | 通过过期闭包读取值,或 TRACK_PX/缩放因子不匹配 | {baseDir}/references/drag-slider.md |
自检清单
检查你编写或改编的 src/ui/ 下每个 .tsx 文件。违反任意一项都会导致编辑器静默失败。
- 文件路径为
src/ui/<PascalCaseName>.tsx,文件名与导出的组件名称一致,首行为/** @jsx ReactEcs.createElement */,且只导出一个组件。 - 每个 JSX 元素名称都是
UiEntity、Label、Input、Dropdown、Button,或其他src/ui/文件导出的组件。 - 不得使用
{cond && <X/>}或cond ? <A/> : <B/>——usePlatform()变体除外,但其每个分支都必须是单个元素或null。 - 不得使用
.map()、循环或通过数组构建子元素。 - 不得使用
{/* JSX comments */};注释必须是 JSX 外的//注释。 uiTransform/uiBackground中的所有值都必须是字面量或裸state.x/props.x。对这些对象进行 Grep,检查?、+、*、Math.、(和裸标识符——每一项都会导致节点被冻结。- 所有文本都必须放在
Label/Button的value上;不得在UiEntity上使用uiText属性包。 - 属性中的每个模板字符串只能插入裸引用。
- 任意元素上唯一允许的展开语法是单个
{...someUseInteractionConst}。 - 可见性必须通过
useInteraction的active层设置display: 'none';悬停 / 按下状态必须使用hover/press层。 - 任何
100%×100%元素(或任何填满屏幕的未定尺寸元素)都不得设置指针事件处理程序、{...useInteraction}展开语法或pointerFilter: 'block'。仅有两个例外,且两者在空闲时都必须禁用:模态背景和拖拽释放捕获器。 - 必须包含
export interface State和export const state: State;每个属性都必须是number、string、boolean、string[]或结构为{ r, g, b, a }的颜色。 - 声明的属性必须是内联对象类型,且只包含可选的
number/string/boolean/ 回调成员。 - 每个处理程序都必须是
/** @ui-action */函数,接收({ state, props, value }: UiAction),并通过 thunk 绑定。 - 所有绑定的尺寸 / 位置都必须是 px 数值;绑定值中不得使用百分比字符串。
- 组件根节点必须声明
width和height(px 或百分比字面量);每个包裹组件引用的UiEntity也必须声明这两个值,且与该组件的根节点尺寸一致。 src/ui/中任何位置都不得出现时钟、Date.now()、setTimeout、缓动、取整或字符串格式化——这些逻辑应放在驱动程序中。src/ui/index.tsx必须与生成的结构完全一致;除非明确要求使用全画布控件,否则每个根节点都必须包裹在ScreenInsetArea中。- 移动端检查:布局须适配
1600x720画布,触控目标 ≥ 约 48 px,不得有仅悬停时显示的操作,不得使用表情符号,不得锚定在左上角,不得在右下角放置交互元素。 - 在 UI-Designer 场景中,你编写的任何内容里都不得调用
TouchScreenControls——移动端控件属于 MobileHUD 面板,应通过src/mobile-hud.ts配置。对场景进行 Grep 并确认唯一匹配项是该生成文件。
参考资料
{baseDir}/references/component-template.md— 编辑器的种子文件、最简组件、功能完整的组件(绑定、混合文本、悬停、双变量退出门控)、对话框前后对比、由 props 驱动的填充条、展开的变体、组合屏幕、聚合器。{baseDir}/references/interaction-helper.md—src/ui/interaction.tsx和src/ui/platform.tsx的原文,以及解析器可识别的用法形式。{baseDir}/references/driver-pattern.md— 缓动开合、格式化计时器、双变量退出门控、操作→驱动握手、派生值、命名约定驱动器。{baseDir}/references/adapting-coded-ui.md— 移植编码 UI 的前后对照示例,以及无法移植的内容应如何告知用户。{baseDir}/references/drag-slider.md— 滑块为何需要PrimaryPointerInfo.screenDelta、经世界内验证的可编辑滑块(点击逐步调整 + 拖动、受门控的释放捕获器、驱动器)、编码版本、取决于 SDK 版本的缩放系数。{baseDir}/references/editor-behavior.md— Creator Hub UI 编辑器会写入和显示的内容:布局轴、组件预设、Scene Inset、画布工具、模式持久化、移动端预览及其限制、MobileHUD 面板的字段。{baseDir}/references/ui-components.md— 五种元素的完整属性参考、uiInputBinding、背景(纹理模式、九切片、UV、色调、头像)、元素和渲染器的zIndex、screenInset/ScreenInsetArea/InteractableArea、UiCanvasInformation,以及作为事实依据的引擎测试场景。{baseDir}/references/coded-ui.md— 自由形式的实现路径:虚拟屏幕默认规则和 7.26.0 版本门槛、addUiRenderer和混合场景、使用条件渲染的模块级状态、从零构建组件的模式,以及每种模式对应的可编辑版本。
交叉引用
- 自定义字体:在
Label、Button、Input、Dropdown和uiText上使用fontSrc(随场景附带的.ttf),并始终将font设为回退字体。需要首个晚于 7.30.1 的 SDK 版本;移动端始终不显示,桌面端则仅在资源包中显示(已部署,或预览时启用 Compress Assets)。参见 fonts。 - 平台检测:使用
@dcl/sdk/platform中的getPlatform()/isMobile()(在可编辑 UI 中由usePlatform()封装)。参见 advanced-input。 - 本地化 UI 文本:
getPlayerLanguage()(同一模块)会返回玩家客户端语言的 BCP-47 标签;会话中途切换语言时使用onPlayerLanguageChanged。在 explorer 响应之前,它会返回'en',因此应在系统(驱动器)中读取,而非在模块作用域读取。参见 advanced-input >“玩家语言”。SDK 7.30.0+。 - 将玩家发送到客户端面板:
openExplorerUi会打开地图 / 背包 / 地点 / 活动面板;当玩家关闭面板时,openExplorerUiAndWait(@dcl/sdk/explorer-ui)会完成。需要用户手势。参见 scene-runtime >“openExplorerUi -- 打开 Explorer 面板”。 - 替换原生移动端控制项:
TouchScreenControls(SDK 7.26.0+、advanced-input)会隐藏摇杆 / 准星 / 游戏手柄按钮,以便场景 UI 取而代之;使用uiInputBinding将替代按钮连接到 InputActions。在 UI-Designer 场景中,该配置属于 MobileHUD 面板。客户端自身的 HUD(聊天、个人资料、表情轮盘)无法隐藏。 - 规则 / 说明内容:game-design(“规则:展示,而非说教”)列出了文字密集的弹出窗口和难以阅读的世界内规则手册这两种反模式。
- 实时编辑已打开的场景(实体、智能物品、设置):参见 creator-hub-mcp。