For the complete documentation index, see llms.txt. This page is also available as Markdown.

屏幕 UI

了解如何为你场景中的玩家创建 UI。例如,这可用于显示与游戏相关的信息。

你可以为你的场景构建一个 UI,将其显示在屏幕的固定 2D 空间中,而不是 3D 世界空间中。

只有当玩家站在场景的 LAND 地块内时,UI 元素才可见,因为相邻场景可能有自己要显示的 UI。UI 的某些部分也可以在世界空间中发生特定事件时触发打开,例如玩家点击某个特定位置。

通过定义一个嵌套结构来构建 UI UiEntity 的 JSX 对象。UI 所使用的语法与以下内容非常相似: React (一个非常流行的基于 JavaScript 的 Web UI 构建库)。

带有静态元素的简单 UI 看起来很像 HTML,但当你加入会响应状态变化的动态元素时,就能实现强大得多的功能。

默认的 Decentraland 浏览器 UI 包括聊天组件、地图和其他元素。这些 UI 元素始终显示在最顶层,位于任何场景专属 UI 之上。因此,如果你的场景中有占据相同屏幕空间的 UI 元素,它们就会被遮挡。

参见 UX 指南 ,了解如何设计你的 UI 的外观和体验的建议。

📱 为移动端设计:设备会为刘海、状态栏、主页指示条和圆角预留屏幕空间,客户端还会在画布的一部分上绘制自己的控件。你无需自己测量——使用下面的 screenInset 选择 UI 放置的区域。它默认使用设备安全区域,因此无需额外工作就能避开硬件边距。在发布前,请查看 移动端安全区域 和 移动端 UI 最佳实践.

当玩家点击屏幕右下角的 关闭 UI 按钮时,所有 UI 元素都会隐藏。

渲染 UI

要在你的场景中显示 UI,请使用 ReactEcsRenderer.setUiRenderer() 函数,并传入一个有效的实体结构,该结构在一个 .tsx 文件中描述。

每个实体都定义为一个类似 HTML 的节点,并通过属性为其各个组件进行设置。

ui.tsx 文件:

import { UiEntity, ReactEcs } from '@dcl/sdk/react-ecs'
import { Color4 } from '@dcl/sdk/math'

export const uiMenu = () => (
  <UiEntity
    uiTransform={{
      width: 700,
      height: 400,
      margin: { top: '35px', left: '500px' },
    }}
    uiBackground={{ color: Color4.Red() }}
  />
)

index.ts 文件:

你也可以将实体结构的定义和渲染放在同一个命令里完成, .tsx 文件中描述。

ui.tsx 文件:

index.ts 文件:

UI 实体

UI 中的每个元素都必须定义为一个单独的 UiEntity,无论它是图像、文本、背景、不可见的对齐框等。就像场景的 3D 空间中一样,每个 UiEntity 都拥有自己的组件来为其提供位置、颜色等。

类似 React 的语法允许你将每个组件指定为内的一个属性,这使代码更简洁、更易读。 UiEntity,这使代码更简洁、更易读。

在一个 UiEntity 中使用的组件与常规实体使用的组件不同。你不能将 UI 组件应用到常规实体上,也不能将常规组件应用到 UI 实体上。

以下组件可在一个 UiEntity:

  • uiTransform

  • uiBackground

  • uiText

  • onMouseDown

  • onMouseUp

  • onMouseEnter

  • onMouseLeave

  • uiInputBinding —— 当元素被按住时保持输入动作,以构建自定义控件

像 HTML 标签一样,你可以将组件定义为自闭合,或者将一个组件嵌套在另一个组件中。

ui.tsx 文件:

index.ts 文件:

一个 UI 模块的定义只能有一个顶层父实体。你可以定义任意多的其他实体,但它们都必须位于一个顶层只有单一父实体的结构中。

屏幕虚拟缩放

为 UI 设置一个虚拟宽度和高度。这样可以确保你的 UI 在不同屏幕尺寸上看起来相同,而不受实际像素屏幕尺寸影响。

如果你将虚拟宽度设为 1920,虚拟高度设为 1080,UI 就会缩放以适配屏幕尺寸。如果屏幕是 1920x1080,UI 会以与虚拟尺寸相同的大小显示。如果屏幕更大或更小,任何像素值都会按比例缩放以适配虚拟尺寸。例如,如果屏幕是 3840x2160,那么一个定义为宽 100 像素的项目将显示为 200 个实际像素。

实际用于乘以像素值的 UI 缩放因子的计算公式是 Math.min(realWidth / virtualWidth, realHeight / virtualHeight).

默认虚拟尺寸

虚拟尺寸是可选的,但它的两个维度必须一起传入:要么同时传入两个,要么都不传。当你没有传入时,会应用平台默认值:

平台
默认虚拟尺寸

移动端

1600x720

桌面端和网页端

1920x1080

这意味着你的 UI 中的像素值始终会相对于一个参考分辨率进行缩放,即使你完全不传任何选项也是如此。即使场景只使用 addUiRenderer() 而从不调用 setUiRenderer().

三个特殊情况:

  • 取消缩放:传入一个无效尺寸——任何值,只要它是 0 0 或更小——即可完全禁用虚拟屏幕。此时像素值会被直接当作原始画布像素使用,不进行缩放。这是关闭虚拟屏幕的文档化方式,因此不会记录任何内容。

  • 不完整的尺寸:只提供两个维度中的一个也是无效的,因此它会像上面的情况一样禁用虚拟屏幕——平台默认值 不会 介入。与上面的主动取消不同,这种情况会被报告:只给出一半的尺寸是错误,而不是有意的选择,因此 SDK 会记录 无效的虚拟屏幕尺寸(…):两个维度都必需,因此虚拟屏幕已被禁用,并且不会应用 UI 缩放。 每种尺寸只会记录一次。请同时传入两个维度,或者都不传。

    在主渲染器上尤其值得再次检查,因为那里的不完整尺寸也是一个场景级开关:一个 setUiRenderer() 提到 任一 维度的调用优先于任何 addUiRenderer(),因此上面的示例会为整个场景禁用虚拟屏幕,并丢弃传递给附加渲染器的有效尺寸。

  • 移动端上的 16:9 尺寸:手机屏幕比 16:9 宽得多,因此 16:9 的虚拟画布会给 UI 加上上下黑边。如果你传入一个 16:9 尺寸(1920x1080, 1280x720,…),并且场景在移动端运行,那么它会被覆盖为 1600x720,并且会向控制台记录一条消息。移动端上的非 16:9 尺寸,以及桌面端或网页端上的所有有效尺寸,都会原样保留。

📔 注:vw 和 vh 单位不受虚拟屏幕影响: vw 以及 vh 单位不受虚拟屏幕影响: 1vw 是画布宽度的 1%,而 1vh 是画布高度的 1%,与 CSS 完全一致。它们也会忽略 screenInset,因此在默认的 'device' 内嵌下, width: '100vw' 比 width: '100%' 更宽——前者覆盖整个屏幕,后者填满内嵌区域。

📱 注意:平台检测是异步完成的。在最初几帧中,SDK 还不知道自己是在移动端运行,因此虚拟屏幕最初会以 1920x1080 开始,并在 1600x720 检测完成后切换到——你可能会在加载时看到 UI 短暂重新缩放,而且 16:9 覆盖消息会在场景启动后不久出现在控制台中。如果你希望从第一帧开始就有一个稳定的单一参考分辨率,请传入一个明确的非 16:9 虚拟尺寸。

屏幕内嵌区域

屏幕并非完全可用:手机会为刘海、状态栏、主页指示条和圆角预留空间,而每个浏览器都会在画布的一部分上绘制自己的 HUD(小地图、聊天等)。渲染器选项中的可选 screenInset 属性用于选择你的 UI 所定位的屏幕区域:

值
UI 放置的区域

'device' (默认)

设备安全区域,不包括刘海、状态栏和圆角。读取自 UiCanvasInformation.screenInsetArea.

'interactable'

浏览器报告为不受其自身 HUD(小地图、聊天等)占用的区域。读取自 UiCanvasInformation.interactableArea。它覆盖的范围由各个浏览器决定——请在你目标的平台上进行验证。

'none'

整个屏幕,且 0,0 位于其左上角。

在桌面端,设备内边距为零,因此 'device' 会将 UI 放置在整个屏幕上——效果与……相同 'none'。该区域会在每个 tick 重新读取,因此当内边距变化时 UI 会随之调整,例如屏幕旋转或系统栏出现与隐藏时。

真实设备上的三个区域

下面的截图是在同一台手机上的同一场景,仅通过更改 screenInset 值后分别渲染三次。洋红色矩形是 UI 本身;轮廓线是 explorer 报告的区域——青色是完整画布,琥珀色是 screenInsetArea,绿色是 interactableArea.

Scene UI covering the whole phone screen, including the notch strip and the areas under the client controls
screenInset: 'none' ——UI 覆盖整个画布。它的角落位于刘海下方并被客户端自身的控件挡在后面。
Scene UI inset to the device safe area, clear of the notch
screenInset: 'device' (默认)——UI 会被收进设备安全区域,避开刘海和圆角。它仍然会与客户端的控件重叠,因为这些控件不属于该区域。
Scene UI inset to the area the client reports as free of its own HUD
screenInset: 'interactable' ——UI 被放置在客户端为场景 UI 指定的矩形内。在移动客户端 1.12.1,此处捕获的画面中,这不包括左侧列(个人资料、聊天、摇杆);右下角的操作按钮则是按设计绘制在该区域之上的。

💡 提示: 'interactable' 会为你提供每个 explorer 为场景 UI 指定的区域。这刻意并不等同于“每个客户端控件都在其外面”——正如第三张截图所示,移动端的操作按钮是故意绘制在该区域之上的。放在它们下面的任何内容你仍然可以触及,但会与它们争抢同样的点击,因此请留意 拥挤的角落 ,并检查你所针对的平台:该区域覆盖哪些内容由各个 explorer 自行决定,而且可能在不同版本之间发生变化。

每个渲染器都会遵循自己的 screenInset,因此主 UI 以及通过 addUiRenderer() 添加的任何 UI 可以同时使用不同的区域。与虚拟尺寸不同,这个值不会在渲染器之间共享。

多个 UI 模块

如果你的场景包含多个各自定义了 UI 的系统或模块,你可以使用 ReactEcsRenderer.addUiRenderer()进行渲染。这在处理包含多个 UI 组件的复杂场景时尤其有用,或者在为 智能物品定义 UI 时也很有用,因为它应当能够独立于场景其余代码中的内容而使用。

该 ReactEcsRenderer.addUiRenderer() 函数要求你提供一个实体作为 UI 的所有者。这个实体可以是任何实体,甚至可以是仅为充当 UI 所有者而创建的虚拟实体。

这段代码片段可以独立于场景中的任何其他 UI 代码而存在。场景的其余部分可能包含一个 ReactEcsRenderer.setUiRenderer(),也可能完全没有,而 UI 仍然会被渲染。

一个 addUiRenderer() 调用也可以包含虚拟宽度和高度,就像 setUiRenderer()。不过,如果场景中有一个 setUiRenderer() 调用也定义了虚拟宽度和高度,那么 addUiRenderer() 调用中的虚拟宽度和高度将被忽略。

虚拟尺寸是一个整个场景范围内的单一值,其解析方式如下: setUiRenderer() 上的值优先;否则,最先的 addUiRenderer() 提供该值的调用获胜;如果没有任何渲染器提供该值,则 平台默认值 生效。仅携带 screenInset 的选项不算作已提供尺寸。

该 screenInset 则相反——它是按渲染器分别设置的,因此每个 UI 模块都可以位于不同的屏幕区域:

可以使用以下方式移除该 UI: ReactEcsRenderer.removeUiRenderer(dummyEntity) ,另外如果拥有 UI 的实体被销毁,UI 也会被移除。如果 ReactEcsRenderer.addUiRenderer() 再次针对同一实体调用,但使用了不同的 UiRenderer,之前的那个会被清理,新的是替换它,并保持其在堆叠顺序中的位置。再次调用它也是更改已渲染 UI 模块选项的方式,例如它的 zIndex.

UI 模块之间的堆叠顺序

默认情况下,UI 模块按它们首次出现的顺序堆叠,后出现的在前。首次出现在同一帧中的模块——通常是场景第一帧之前注册的所有内容——会与主 UI 一起绘制,其中 setUiRenderer() 位于最底层,其后按照添加顺序绘制其他模块。要显式控制这一点,请在渲染器选项中传入一个 zIndex :具有更高 zIndex 的模块会渲染在较低值模块前面,而不管它们出现的顺序如何。保留为 0 的模块会保持默认顺序。该选项在 setUiRenderer()上也是同样生效的,因此主 UI 也可以放到其他模块前面。

这与……的设置不同 zIndex 中的 UiEntity,它只会对……排序 同一 UI 模块内的兄弟元素。渲染器选项是按整个模块彼此排序,而每个模块内部的元素则保留各自的 zIndex.

共享一条 setUiRenderer 语句

而不是调用 ReactEcsRenderer.addUiRenderer() 对于每个 UI 模块,你可以调用 ReactEcsRenderer.setUiRenderer() 一次,传入一个 UI 模块数组,它们可以位于不同的文件中。

下面是一个更完整的示例:

ui.tsx 文件:

index.ts 文件:

最后更新于