> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/creator/content-creator-zh/chang-jing-sdk7/jia-gou/entities-components.md).

# 实体与组件

了解 Decentraland 场景中实体和组件的基础知识

Decentraland 场景是围绕……构建的 [*实体*, *组件* 和 *系统*](https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system)。这是游戏引擎架构中常用的一种模式，可轻松实现可组合性和可扩展性。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-de62a18d51b28bebd55c86e8ef5a8a783467ae62%2Fecs-big-picture.png?alt=media\&token=0d7a02a2-955a-44ed-ae77-5df99c49e530)

## 概览

*实体* 是构建 Decentraland 场景中一切内容的基本单元。场景中的所有可见和不可见 3D 对象以及音频播放器都会各自成为一个实体。实体不过是一个可由组件引用的 id。实体本身没有任何属性或方法，只是用于将多个组件组合在一起。

*组件* 定义实体的特征。例如， `Transform` 组件会存储实体的坐标、旋转和缩放。 `MeshRenderer` 组件在场景中渲染时会赋予实体一个可见形状（如立方体或球体）， `材质` 组件会赋予实体颜色或纹理。你也可以创建自定义组件来满足场景所需的数据，例如自定义的 `生命值` 可以存储实体剩余的生命值，并将其添加到在游戏中代表非玩家敌人的实体上。

如果你熟悉 Web 开发，可以把实体理解为 *元素* 包裹在 *DOM* 树，而组件则类似于这些元素的 *属性* 。

在 [Creator Hub 中的场景编辑器](/creator/content-creator-zh/chang-jing-bian-ji-qi/ru-men/about-editor.md)，你可以通过选中它来查看属于某个实体的组件。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-2ffe3126d7a92824658497ed2664fcfcc28ce16a%2Fcomponents-example.png?alt=media\&token=543af76c-1c78-4a46-8238-d22a92fe16cc)

{% hint style="warning" %}
**📔 注意**：在 SDK 的早期版本中，实体是 *对象* ，它们会被实例化，并且可以通过扩展来添加函数。从 SDK 7.0 版本开始，实体仅仅是一个 ID。这种结构更符合 [面向数据编程](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/data-oriented-programming.md) 的原则，并有助于提升场景性能。
{% endhint %}

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-ee29a02358e859a30079072e8da958b899aea659%2Fecs-components-new.png?alt=media\&token=c9ecdd6f-77bf-4b98-80aa-2b806c3e8949)

像 `Transform`, `材质` 或任何 *形状* 组件与场景的渲染紧密相关。如果这些组件中的值发生变化，仅凭这一点就足以让引擎在下一帧改变场景的渲染方式。

引擎是位于场景中间、管理其他所有部分的组件。它决定哪些实体会被渲染，以及玩家如何与它们交互。它还协调来自 [系统](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/systems.md) 的哪些函数会在何时执行。

组件用于存储其所引用实体的数据。它们只能存储这些数据，不能自行修改这些数据。组件中的所有值变更都由 [系统](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/systems.md)完成。系统与组件和实体本身完全解耦。实体和组件并不关心 *系统* 在作用于它们的是什么。

## 实体和组件的语法

下面的示例展示了声明和配置基础实体与组件的一些基本操作。

```ts
export function main() {
	// 创建一个实体
	const door = engine.addEntity()

	// 通过 Transform 组件为实体赋予位置
	Transform.create(door, {
		position: Vector3.create(5, 1, 5),
	})

	// 通过 GltfContainer 组件为实体赋予可见形状
	GltfContainer.create(door, {
		src: 'assets/models/door.glb',
	})
}
```

{% hint style="warning" %}
**📔 注意**：在 SDK 的早期版本中，必须手动将实体添加到引擎中才能开始渲染。从 SDK 7 版本开始，只要为实体分配了组件，它就会被隐式添加到引擎中。
{% endhint %}

当组件被创建时，它总是分配给一个父实体。然后，该组件的值会影响这个实体。

{% hint style="info" %}
**💡 提示**：与其逐个创建实体，不如一次性从一个 [组合体](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/composites.md) 文件。
{% endhint %}

## 移除实体

要从引擎中移除一个实体，请使用 `engine.removeEntity()`。此函数返回一个 `布尔值`: `true` ，如果实体被移除， `false` ，如果移除被拒绝。

```ts
export function main() {
	// 创建一个实体
	const door = engine.addEntity()

	// 通过 GltfContainer 组件为实体赋予可见形状
	GltfContainer.create(door, {
		src: 'assets/models/door.glb',
	})

	// 移除实体
	const removed = engine.removeEntity(door)
	console.log('实体已移除：', removed) // true
}
```

如果被移除的实体有任何子实体，这些子实体的父级会改回默认的 `engine.RootEntity` 实体，它位于场景基准位置，缩放为 *1*.

### 渲染器保留实体

某些实体 id 会被渲染器保留，用于远程玩家头像。你不能移除这些实体。如果你对一个渲染器保留实体调用 `engine.removeEntity()` ，它会返回 `false` ，并保持所有组件不变。

该 [命名的保留实体](#reserved-entities) (`engine.RootEntity`, `engine.PlayerEntity`, `engine.CameraEntity`）是一个特殊情况： `engine.removeEntity()` 仍然返回 `false` ，对于这些实体（它们的 id 永远不会释放），但它们的组件会被 **是** 清除。这意味着你可以移除你自己的组件（例如删除一个 `InputModifier` 来自 `engine.PlayerEntity`），尽管实体 id 本身永远不会被释放。

```ts
// 尝试移除一个保留实体
const result = engine.removeEntity(engine.PlayerEntity)
console.log(result) // false — 实体 id 不会被释放
// 但 PlayerEntity 上场景自身的组件会被清除
```

在移除实体时，你可以检查返回值来处理边缘情况：

```ts
if (!engine.removeEntity(someEntity)) {
	console.log('实体无法移除（保留）')
}
```

### 移除实体及其子实体

要移除一个实体以及它的所有子实体（以及其子实体的子实体，递归地），请使用 `removeEntityWithChildren()` 辅助函数。

```ts
export function main() {
	// 创建父实体
	const door = engine.addEntity()

	// 创建子实体
	const doorKnob = engine.addEntity()

	// 让这些实体具有可见形状
	GltfContainer.create(door, {
		src: 'models/door.glb',
	})
	GltfContainer.create(doorKnob, {
		src: 'models/doorKnob.glb',
	})

	// 父级
	Transform.create(doorKnob, {
		parent: door,
	})

	// 同时移除父级和子级
	removeEntityWithChildren(engine, door)
}
```

{% hint style="warning" %}
**注意：** 如果一个渲染器保留实体位于树中的任何位置， `removeEntityWithChildren` 会移除所有其他后代，但保留该保留实体。该保留实体的 `Transform.parent` 会指向一个已移除的实体。只有当你的场景把一个保留实体作为某个场景实体的子级时才会发生这种情况，这并不常见。
{% endhint %}

{% hint style="info" %}
**💡 提示**：在某些情况下，与其从引擎中移除实体，不如把它设为不可见，这样你就可以在不产生延迟的情况下再次加载它。参见 [设为不可见](/creator/content-creator-zh/chang-jing-sdk7/3d-nei-rong-ji-chu/shape-components.md#make-invisible)
{% endhint %}

### 在后台移除实体

实体只是一个由其组件引用的 id。因此，当你移除实体时，实际上是在移除引用该实体的每一个组件。如果你手动移除某个实体的所有组件，玩家看到的效果会与执行 `engine.removeEntity()`。不过， `engine.removeEntity()` 还会执行一些额外的内部记账工作，将实体 id 标记为不再使用，因此它始终是移除实体时推荐的方式。

## 嵌套实体

一个实体可以拥有其他实体作为子级。借助这一点，我们可以像网页的 HTML 一样，把实体组织成树状结构。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-8b6c8cd679f648a41400eb6f29889cd09ede8947%2Fecs-nested-entities-new.png?alt=media\&token=847cb6ef-9cee-49ea-9894-b294e72341d2)

要将一个实体设为另一个实体的父级，子实体必须具有一个 `Transform` 组件。然后你可以设置 `parent` 字段，并将父实体作为引用传入。

```ts
export function main() {
	// 创建实体
	const parentEntity = engine.addEntity()

	const childEntity = engine.addEntity()

	// 设置父级
	Transform.create(childEntity, {
		parent: parentEntity,
	})
}
```

一旦分配了父级，就可以在子实体的 `parent` 字段中读取它的 `Transform` 组件。

```ts
// 从实体获取父级
const parent = Transform.get(childEntity).parent
```

如果父实体具有一个 `Transform` 会影响其位置、缩放或旋转的组件，那么它的子实体也会受到影响。位置或旋转值会相加，缩放值会相乘。

如果父实体或子实体没有 `Transform` 组件，则使用以下默认值。

* 对于 **位置**，父级的中心是 *0, 0, 0*
* 对于 **旋转** ，父级的旋转四元数是 *0, 0, 0, 1* （等同于欧拉角 *0, 0, 0*)
* 对于 **缩放**，父级被视为大小为 *1*。对父级的任何缩放都会按比例影响其缩放和位置。

没有形状组件的实体在场景中是不可见的。它们可用作包装器，以组的形式处理和定位多个实体。

要将子实体与其父级分离，你可以把该实体的父级设为 `engine.RootEntity`.

```ts
const mutableChildTransform = Transform.getMutable(childEntity)
mutableChildTransform.parent = engine.RootEntity
```

{% hint style="warning" %}
**📔 注意**：当处理与其他玩家同步的嵌套实体时，请使用 `parentEntity()` 函数，而不是 `parent` Transform 中的实体。参见 [父子实体](/creator/content-creator-zh/chang-jing-sdk7/lian-wang/serverless-multiplayer.md#parented-entities)
{% endhint %}

在场景编辑器中，你可以在左侧面板看到场景中所有嵌套实体的完整层级结构。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-60929069a225f48ce0d6c94d9dce285ff875af10%2Fentity-tree-example.png?alt=media\&token=b2baf1b6-e035-4ae5-92fc-509628408f8d)

## 通过 ID 获取实体

场景中的每个实体都有一个唯一编号 *id*。你可以根据这个 ID 从引擎中检索引用特定实体的组件。

```typescript
// 获取一个 Transform 组件
Transform.get(1000 as Entity)
```

{% hint style="warning" %}
**📔 注意**：介于 *0* 和 *511* 之间的实体 id 被引擎保留给固定实体，例如玩家头像、基础场景等。
{% endhint %}

例如，如果玩家的点击或一个 [射线投射](/creator/content-creator-zh/chang-jing-sdk7/jiao-hu-xing/raycasting.md) 击中某个实体，它将返回被击中实体的 id，你可以使用上面的命令获取与该 id 匹配的实体的 Transform 组件。你也可以用同样的方式获取该实体的任何其他组件。

## 通过名称获取实体

在场景编辑器中通过拖放添加实体时，每个实体都有一个唯一名称。使用 `engine.getEntityOrNullByName()` 函数即可在代码中引用这些实体中的一个。请将实体名称作为字符串传入，名称应与场景编辑器界面左侧树状视图中显示的一致。

```ts
function main() {
	const door = engine.getEntityOrNullByName('door3')
}
```

{% hint style="warning" %}
**📔 注意**: 请确保你只在 `engine.getEntityOrNullByName()` 内使用 `main()` 函数中、在 `main()`，或者在系统中使用。如果在这些上下文之外使用，场景编辑器 UI 中创建的实体可能尚未实例化。
{% endhint %}

你可以自由地对通过此方法获取的实体执行任何操作，例如添加或移除组件、修改现有组件的值，或将实体从引擎中移除。

```ts
function main() {
	// 获取实体
	const door = engine.getEntityOrNullByName('door-3')
	// 验证实体存在
	if (door) {
		// 添加指针事件回调
		pointerEventsSystem.onPointerDown(
			{
				entity: door,
				opts: { button: InputAction.IA_PRIMARY, hoverText: '打开' },
			},
			function () {
				// 打开门
			}
		)
	}
}
```

通过场景编辑器 UI 添加的所有实体都有一个 `名称` 组件，你可以像这样遍历它们：

```ts
function main() {
	for (const [entity, name] of engine.getEntitiesWith(Name)) {
		console.log({ entity, name })
	}
}
```

## 添加或替换组件

每个实体只能拥有某一类型的一个组件。例如，如果你尝试向一个已经有 Transform 的实体再次分配 Transform，就会引发错误。

为避免此错误，你可以使用 `.createOrReplace` 而不是 `.create`。如果存在同类型的现有组件，此命令会覆盖它；否则，它会像 `.create`.

```ts
Transform.createOrReplace(door, {
	position: Vector3.create(5, 1, 5),
})
```

{% hint style="warning" %}
**📔 注意**：由于 `.createOrReplace` 在创建组件之前会执行额外检查，因此使用 `.create`总是更高效。如果你确定该实体尚未拥有你要添加的那类组件，请使用 `.create`.
{% endhint %}

## 从实体访问组件

你可以使用实体的 `.get()` 或者 `getMutable()` 函数来创建颜色。

```ts
export function main() {
	// 创建实体
	const box = engine.addEntity()

	// 创建并将组件添加到该实体
	Transform.create(box)

	// 获取组件的只读版本
	let transform = Transform.get(box)

	// 获取组件的可变版本
	let transform = Transform.getMutable(box)
}
```

该 `get()` 函数会获取该组件的只读引用。你无法通过这个组件引用修改任何值。

如果你希望修改组件的值，请使用 `getMutable()` 函数。若你修改组件的可变版本中的值，就会直接影响该组件所属的实体。

参见 [可变数据](/creator/content-creator-zh/chang-jing-sdk7/bian-cheng-mo-shi/mutable-data.md) 了解更多详情。

{% hint style="warning" %}
**📔 注意**：仅在 `getMutable()` 你确实要修改组件值时才使用。否则，请始终使用 `get()`。这种做法遵循 [面向数据编程](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/data-oriented-programming.md)，并能显著提升场景性能。
{% endhint %}

```ts
// 获取组件的可变版本
let transform = Transform.getMutable(box)

// 修改组件的一个值
transform.scale.x = 5
```

上面的示例直接修改了 *x* Transform 组件上的 scale 值。

如果你不完全确定该实体是否具有你要检索的组件，请使用 `getOrNull()` 或 `getMutableOrNull()`.

{% hint style="warning" %}
**📔 注意**：尽量避免使用 `getOrNull()` 或 `getMutableOrNull()` ，因为这些函数会涉及额外检查，因此效率低于 `.get()` 和 `getMutable()`.
{% endhint %}

```ts
//  getOrNull
const transformOrNull = Transform.getOrNull(myEntity)

//  getMutableOrNull
const mutableTransformOrNull = Transform.getMutableOrNull(myEntity)
```

如果你要检索的组件在该实体中不存在：

* `get()` 和 `getMutable()` 返回错误。
* `getOrNull()` 和 `getMutableOrNull()` 返回 `Null`.

## 从实体中移除组件

要从实体中移除一个组件，请使用该实体的 `deleteFrom()` 组件类型的方法。

```ts
Transform.deleteFrom(myEntity)
```

如果你尝试移除实体中不存在的组件，此操作不会抛出任何错误。

{% hint style="warning" %}
**📔 注意**：要一次性移除某个实体的所有组件，请参见 [本节](#remove-entities)
{% endhint %}

## 检查组件是否存在

你可以使用 `has()` 函数来检查某个实体是否拥有某个组件的实例。该函数返回 *true* ，如果组件存在则返回 *false* ，如果不存在则返回。这个功能在场景中的条件逻辑里非常实用。

```ts
const hasTransform = Transform.has(myEntity)
```

{% hint style="info" %}
**💡 提示**：你也可以 [查询组件](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/querying-components.md) 来获取持有某个特定组件或某组特定组件的完整实体列表。不要手动遍历场景中的所有实体并逐个使用 `has()`进行检查，这种方式要低效得多。
{% endhint %}

## 检查组件变化

使用 `onChange` 函数，在某个实体的组件值发生变化时运行回调函数。它适用于任何组件，是保持代码可读性的绝佳捷径。

回调函数可以包含一个输入参数，其中包含该组件的新状态。

```ts
Transform.onChange(cubeEntity, (newTransform) => {
	if (!newTransform) return
	console.log(
		'立方体位置已更改：',
		newTransform.position,
		newTransform.rotation
	)
})

VisibilityComponent.onChange(cubeEntity, (newVisibilityComponent) => {
	if (!newVisibilityComponent) return
	console.log('立方体可见性已更改：', newVisibilityComponent.visible)
})
```

如果组件从实体中移除，则函数会以 `undefined`.

{% hint style="info" %}
**💡 提示**：该 `.onChange()` 函数既适用于 SDK 的原生组件，也适用于 [自定义组件](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/custom-components.md) ，由创建者定义。
{% endhint %}

## 获取子实体

要访问某个父实体的所有直接子实体，请使用 `getEntitiesWithParent`。它接受 `引擎` 和 `parent` 中的实体，并返回所有以该特定实体为父级的实体列表。请注意，它只返回直接子实体，不返回子实体的子实体。

```ts
import { getEntitiesWithParent } from '@dcl/sdk/ecs'

const children = getEntitiesWithParent(engine, myEntity)
for (const child of children) {
   // 处理每个子实体
}
```

如果你想访问某个实体的所有后代，无论它们嵌套得多深，请使用函数 `getComponentEntityTree()`。与其手动逐级遍历层级结构，这个函数会返回一个扁平的后代列表，便于遍历。它还会筛选出仅包含具有指定组件或组件列表的实体。

```ts
import { getComponentEntityTree } from '@dcl/sdk/ecs'

export function main() {
	// 创建一个带有嵌套子实体的父实体
	const parentEntity = engine.addEntity()
	Transform.create(parentEntity, {
		position: Vector3.create(8, 0, 8),
	})

	// ……假设父实体有多个子实体和孙实体

	// 遍历父实体的所有后代
	for (const descendantEntity of getComponentEntityTree(
		engine,
		parentEntity,
		Transform
	)) {
		// 访问每个后代实体
		const transform = Transform.get(descendantEntity)
		console.log('后代位置：', transform.position)
	}
}
```

该 `getComponentEntityTree` 函数接受三个参数：

* `引擎`：运行这些实体的引擎实例
* `实体`：要从哪个根实体开始
* `组件`：要按其筛选的组件（通常是 `Transform` 用于空间层级）

该函数返回一个生成器，它会在树结构中逐个产出每个后代实体。只有具有指定组件的实体才会包含在结果中。

你可以将它与其他组件检查结合起来，在层级结构中查找特定实体：

```ts
// 查找具有特定名称的所有后代
for (const descendantEntity of getComponentEntityTree(
	engine,
	parentEntity,
	Transform
)) {
	const name = Name.getOrNull(descendantEntity)
	if (name && name.value === 'targetEntity') {
		console.log('找到目标实体：', descendantEntity)
	}
}
```

## 保留实体

某些实体 id 会为每个场景中都存在的特殊实体保留。你可以通过以下别名访问它们：

* `engine.RootEntity`
* `engine.PlayerEntity`
* `engine.CameraEntity`

{% hint style="warning" %}
**📔 注意**：避免在这些实体初始化之前引用它们。为避免这个问题，请在 `main()` 初始化函数中，或在系统中引用这些实体。
{% endhint %}

## 根实体

场景中的所有实体都是 `engine.RootEntity`的子级，直接或间接。

这个实体没有 Transform 组件，但它用于处理一些代表更全局设置的组件，例如 [天空盒控制](/creator/content-creator-zh/chang-jing-sdk7/jiao-hu-xing/skybox-control.md), [光标位置](/creator/content-creator-zh/chang-jing-sdk7/jiao-hu-xing/user-data.md#check-the-players-cursor-position)，或 [屏幕尺寸](/creator/content-creator-zh/chang-jing-sdk7/2d-ui/ui-positioning.md#responsive-ui-size).

## 玩家实体

该 `engine.PlayerEntity` 实体代表玩家的头像。

获取玩家的 `Transform` 组件以获取玩家当前的位置和旋转，参见 [用户数据](/creator/content-creator-zh/chang-jing-sdk7/jiao-hu-xing/user-data.md)。玩家的 Transform 是只读的，要修改它请使用 `movePlayerTo()` 函数， [了解更多](/creator/content-creator-zh/chang-jing-sdk7/jiao-hu-xing/tou-xiang/move-player.md).

你也可以通过将对象设为该实体的子级来附加到玩家身上，不过 [附加到玩家](/creator/content-creator-zh/chang-jing-sdk7/3d-nei-rong-ji-chu/entity-positioning.md#attach-an-entity-to-an-avatar) 通常是更好的选择。

## 摄像机实体

该 `engine.CameraEntity` 实体代表玩家的摄像机。

获取摄像机的 `Transform` 组件以获取摄像机的位置和旋转。该实体的 Transform 也是只读的。要修改摄像机角度或位置，请使用 [虚拟摄像机](/creator/content-creator-zh/chang-jing-sdk7/3d-nei-rong-ji-chu/camera.md#using-virtual-cameras).

你还可以获取摄像机的 `CameraMode` 组件来了解玩家正在使用第一人称还是第三人称摄像机模式，参见 [摄像机模式](/creator/content-creator-zh/chang-jing-sdk7/jiao-hu-xing/user-data.md#check-the-players-camera-mode).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.decentraland.org/creator/content-creator-zh/chang-jing-sdk7/jia-gou/entities-components.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
