实体与组件
了解 Decentraland 场景中实体与组件的基础知识
Decentraland 场景围绕……构建 实体, 组件 和 系统。这是多个游戏引擎架构中常用的一种模式,可实现良好的可组合性和可扩展性。

概述
实体 是在 Decentraland 场景中构建一切的基本单位。场景中的所有可见和不可见 3D 对象以及音频播放器都各自是一个实体。实体不过是一个 ID,可以被组件引用。实体本身没有任何属性或方法,只是用于将多个组件组合在一起。
组件 定义实体的特征。例如,一个 Transform 组件会存储实体的坐标、旋转和缩放。一个 MeshRenderer 组件会在场景中渲染时为实体赋予一个可见的形状(例如立方体或球体),而一个 Material 组件会为实体赋予颜色或纹理。你也可以创建自定义组件来承载场景所需的数据,例如一个自定义的 生命值 可以存储实体剩余的生命值,并将其添加到游戏中代表非玩家敌人的实体上。
如果你熟悉网页开发,可以把实体看作等同于 元素 中的 DOM 树中的元素,而组件则相当于 属性 。
在 Creator Hub 中的场景编辑器中,你可以通过选择某个实体来查看其所属的组件。

📔 注意:在 SDK 的早期版本中,实体是 对象 ,是实例化后的对象,并且可以通过扩展来添加函数。从 SDK 7.0 版本开始,实体仅仅是一个 ID。这种结构更符合 面向数据编程 的原则,并有助于提升场景性能。

像 Transform, Material 或任何 形状 组件都与场景渲染密切相关。如果这些组件中的值发生变化,仅凭这一点就足以让引擎在下一帧改变场景的渲染方式。
引擎是场景中处于中心位置并管理其他所有部分的组件。它决定哪些实体会被渲染,以及玩家如何与它们交互。它还会协调来自 系统 系统的哪些函数会在何时执行。
组件用于存储其所引用实体的数据。它们只能存储这些数据,不能自行修改这些数据。组件值的所有更改都由 系统完成。系统与组件和实体本身完全解耦。实体和组件并不关心 系统 是哪些系统在作用于它们。
实体和组件的语法
下面的示例展示了声明和配置基础实体与组件的一些基本操作。
📔 注意:在 SDK 的早期版本中,要开始渲染一个实体,必须手动将其添加到引擎中。从 SDK 7 版本开始,只要给实体分配了组件,它就会自动隐式地添加到引擎中。
当创建一个组件时,它总是被分配给一个父实体。随后该组件的值会影响这个实体。
移除实体
要从引擎中移除一个实体,请使用 engine.removeEntity()。此函数返回一个 布尔值: true ,如果实体已被移除, false 如果移除被拒绝,则返回 false。
如果一个被移除的实体还有任何子实体,这些子实体的父级会改回默认的 engine.RootEntity 实体,它位于场景基准位置,缩放为 1.
由渲染器保留的实体
有些实体 ID 被渲染器保留给远程玩家头像使用。你不能移除这些实体。如果你调用 engine.removeEntity() 针对一个由渲染器保留的实体,它会返回 false 并保持所有组件不变。
该 命名保留实体 (engine.RootEntity, engine.PlayerEntity, engine.CameraEntity)是一个特殊情况: engine.removeEntity() 仍然返回 false 这些对象(它们的 ID 永远不会释放),但它们的组件 会 被清除。这意味着你可以从中清除你自己的组件(例如,移除一个 InputModifier 从 engine.PlayerEntity),尽管实体 ID 本身永远不会被释放。
你可以在移除实体时检查返回值,以处理边缘情况:
移除实体及其子实体
要移除一个实体以及它的所有子实体(以及其子实体的所有子实体,递归地),请使用 removeEntityWithChildren() 辅助函数。
注意: 如果一个由渲染器保留的实体位于树中的任何位置, removeEntityWithChildren 会移除所有其他后代,但会保留该保留实体。该保留实体的 Transform.parent 会指向一个已移除的实体。只有当你的场景把一个保留实体挂到某个场景实体下面时才会发生这种情况,这并不常见。
在后台移除实体
实体不过是一个由其组件引用的 ID。因此,当你移除一个实体时,实际上是在移除所有引用该实体的组件。如果你手动移除某个实体的所有组件,在玩家看来效果与执行 engine.removeEntity()。不过, engine.removeEntity() 还会执行一些额外的内部记录,将实体 ID 标记为不再使用,因此始终推荐用这种方式移除实体。
嵌套实体
一个实体可以将其他实体作为子实体。借此,我们可以像网页的 HTML 一样,把实体组织成树结构。

要将一个实体设为另一个实体的父级,子实体必须拥有一个 Transform 组件。然后你可以设置 parent 字段,填入对父实体的引用。
一旦分配了父级,就可以从子实体的 parent 其上的 Transform 组件找到。
如果父实体拥有一个 Transform 会影响其位置、缩放或旋转的组件,它的子实体也会受到影响。位置和旋转值会相加,缩放值会相乘。
如果父实体或子实体没有 Transform 组件,则使用以下默认值。
对于 位置,父实体的中心是 0, 0, 0
对于 旋转 ,父实体的旋转是四元数 0, 0, 0, 1 (等同于欧拉角 0, 0, 0)
对于 scale,父实体被视为大小为 1。父实体的任何缩放都会按比例影响子实体的缩放和位置。
没有形状组件的实体在场景中是不可见的。它们可用作包装器,将多个实体作为一个组来处理和定位。
要将子实体与其父实体分离,你可以把该实体的父级设为 engine.RootEntity.
📔 注意:在处理与其他玩家同步的嵌套实体时,请使用 parentEntity() 函数,而不是 parent Transform 中的 entity。参见 已设父级的实体
在场景编辑器中,你可以在左侧面板看到场景里嵌套实体的完整层级结构。

按 ID 获取实体
场景中的每个实体都有一个唯一编号 id。你可以根据这个 ID 从引擎中检索引用该特定实体的组件。
📔 注意:介于 0 和 511 之间的实体 ID 会被引擎保留,用于固定实体,例如玩家头像、基础场景等。
例如,如果玩家的点击或一个 raycast 命中一个实体,那么这里会返回被命中实体的 ID,你可以使用上面的命令来获取与该 ID 对应实体的 Transform 组件。你也可以用同样的方式获取该实体的其他任何组件。
按名称获取实体
在场景编辑器中通过拖放添加实体时,每个实体都有一个唯一名称。请使用 engine.getEntityOrNullByName() 函数在代码中引用这些实体之一。请将实体名称作为字符串传入,名称需与场景编辑器界面左侧树状视图中显示的一致。
📔 注意:请确保你只在 engine.getEntityOrNullByName() 内使用 main() 函数中,或在 main()之后运行的函数中,或在系统中使用。如果在这些上下文之外使用,场景编辑器 UI 中创建的实体可能尚未实例化。
你可以自由地对通过此方法获取的实体执行任何操作,例如添加或移除组件、修改现有组件的值,或将该实体从引擎中移除。
所有通过场景编辑器 UI 添加的实体都有一个 Name 组件,你可以像这样遍历所有这些实体:
添加或替换组件
每个实体对于同一种组件只能拥有一个。例如,如果你尝试给一个已经拥有 Transform 的实体再分配一个 Transform,就会报错。
为避免此错误,你可以使用 .createOrReplace 而不是 .create。如果同类组件已存在,此命令会覆盖它们;否则就像 .create.
📔 注意:由于 .createOrReplace 在创建组件前会额外执行一次检查,因此始终使用 .create。如果你确定该实体还没有你要添加的这种组件,请使用 .create.
从实体访问组件
你可以通过实体的 .get() 或 getMutable() 函数。
该 get() 函数会获取组件的只读引用。你不能通过这个引用更改组件的任何值。
如果你希望更改组件的值,请使用 getMutable() 函数。如果你更改组件的可变版本中的值,就等于直接影响该组件所属的实体。
参见 可变数据 了解更多细节。
📔 注意:仅在 getMutable() 你确实要修改组件值时才使用。否则,请始终使用 get()。这种做法遵循 面向数据编程,并能显著提升场景性能。
上面的示例直接修改了 x Transform 组件上的 scale 值。
如果你不完全确定该实体是否拥有你要获取的组件,请使用 getOrNull() 或 getMutableOrNull().
📔 注意:尽量不要使用 getOrNull() 或 getMutableOrNull() (如果可能),因为这些函数包含额外检查,因此比 .get() 和 getMutable().
如果你要检索的组件在该实体中不存在:
get()和getMutable()会返回错误。getOrNull()和getMutableOrNull()返回空值.
从实体中移除一个组件
要从实体中移除一个组件,请使用实体的 deleteFrom() 方法。
如果你尝试移除一个在实体中不存在的组件,这个操作不会引发任何错误。
📔 注意:要一次性移除一个实体的所有组件,请参见 本节
检查组件是否存在
你可以使用 has() 函数来检查某个实体是否拥有某个组件的实例。此函数返回 true 如果组件存在,则返回 false ;如果不存在,则返回 false。这在场景中的条件逻辑里非常实用。
检查组件的变化
使用 onChange 函数,可以在给定实体的组件值发生变化时运行回调函数。它适用于任何组件,是帮助保持代码可读性的绝佳捷径。
回调函数可以包含一个输入参数,其中保存组件的新状态。
如果该组件从实体中移除,那么函数会以 undefined.
获取子实体
要访问某个父实体的所有直接子实体,请使用 getEntitiesWithParent。它将 engine 和 parent 实体作为参数,并返回所有以该特定实体为父级的实体列表。请注意,它只返回直接子实体,不返回子实体的子实体。
如果想改为访问某个实体的所有后代,无论它们嵌套有多深,请使用函数 getComponentEntityTree()。它不会让你按层级逐级手动遍历,而是返回一个易于迭代的后代扁平列表。它还会进行过滤,只保留具有给定组件或组件列表的实体。
该 getComponentEntityTree 该函数接受三个参数:
engine:运行这些实体的引擎实例实体:作为起点的根实体组件:用于筛选的组件(通常Transform用于空间层级)
该函数返回一个生成器,用于在树结构中逐个产出每个后代实体。结果中只会包含具有指定组件的实体。
你可以将其与其他组件检查结合使用,以在层级中查找特定实体:
保留实体
某些实体 ID 被保留给每个场景中都存在的特殊实体。可以通过以下别名访问它们:
engine.RootEntityengine.PlayerEntityengine.CameraEntity
📔 注意:在这些实体初始化之前,避免引用它们。为避免此问题,请在 main() 函数中或在系统中引用这些实体。
根实体
场景中的所有实体都是 engine.RootEntity的子实体,直接或间接皆是。
这个实体缺少 Transform 组件,但它用于处理几个表示更全局设置的组件,例如 天空盒控制, 光标位置,或 屏幕尺寸.
玩家实体
该 engine.PlayerEntity 实体代表玩家的化身。
获取玩家的 Transform 组件,以获取玩家当前的位置和旋转,参见 用户数据。玩家的 Transform 仅可读,如需修改,请使用 movePlayerTo() 函数, 了解更多.
你也可以通过将对象设为此实体的子对象来把它们附加到玩家身上,不过 附加到玩家 通常是更好的选择。
摄像机实体
该 engine.CameraEntity 实体代表玩家的摄像机。
获取摄像机的 Transform 组件,以获取摄像机的位置和旋转。该实体的 Transform 也仅可读。如需修改摄像机角度或位置,请使用 虚拟摄像机.
你也可以获取摄像机的 CameraMode 组件,以了解玩家是否正在使用第一人称或第三人称摄像机模式,参见 摄像机模式.
最后更新于