> 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/contributor/contributor-zh/chang-jing-yun-xing-shi/yun-xing-shi-mo-kuai/engine-api.md).

# 引擎 API

该 `EngineApi` 该模块提供对 Entity-Component-System 框架的访问，该框架由 World Explorer（运行游戏循环）与各个场景（管理各自的实体）共享，并包含相关实用工具。

```ts
const engine = require("~system/EngineApi");
```

这个模块功能最复杂、最丰富，也最有可能接收更新和扩展。Decentraland 的大部分能力都蕴藏于此，因为它决定了人们可以创建何种体验。

本模块包含以下方法：

* [`函数 crdtSendToRenderer`](#crdtSendToRenderer)
* [`函数 crdtGetState`](#crdtGetState)

### 简介

该 `EngineApi` 该模块旨在通过交换更新、事件和命令，在 Explorer 与场景之间同步世界状态。它实现了一种可扩展的消息协议，使场景能够，除其他外：

* 创建和销毁实体。
* 附加、更新和移除组件。
* 在 3D 环境中发射射线并检测命中。
* 接收诸如玩家输入之类的事件。

```
.------------------------------------------------------.
| 世界浏览器                                       |
|                                                      |
|                   [引擎 API]                       |
|                        |                             |
|  .--------.            |              .------------. |
|  |        |<-----------+<-------------+  运行时   | |
|  |  游戏  |            |   命令      |  .------.  | |
|  |        |   事件     |              |  | 场景 | | |
|  | 引擎 +----------->+------------->|  |       | | |
|  |        |            |              |  '-------' | |
|  '--------'            |              '------------' |
|                                                      |
'------------------------------------------------------'

```

{% hint style="info" %}
该 `EngineApi` 该模块正在重构。如果你查看源定义，会发现一些遗留方法已不再使用，或者在最新版本中可以实现为空操作（下面会详细说明）。
{% endhint %}

### ECS 框架

随着 `EngineApi` 该模块带来了一个通用且可扩展的共享 ECS 框架实现，它允许场景和游戏引擎本身创建实体、附加组件并更新其状态。

任何一侧所做的更改都会通过交换消息反映到另一侧。所使用的机制（[见下文](#synchronization)）可确保双方就任何更新的顺序达成一致，并最终在世界状态上收敛到一致。

World Explorer 实现了一组众所周知的基础组件（位置、形状、纹理、媒体等），并且知道在接收到更新时如何反序列化并应用对其状态的更改。使用 [SDK](https://docs.decentraland.org/creator) 的场景也有工具来创建自定义组件，但这些组件完全由场景管理（不会同步），因此不在该协议的范围内。

{% hint style="info" %}
大多数场景使用 Decentraland SDK，它封装了 `EngineApi` 模块，并为内容开发者提供了更友好、更高层的接口。直接访问本模块中消息协议的场景极为罕见。
{% endhint %}

#### 识别实体 <a href="#identifying" id="identifying"></a>

实体 ID 是从 `0`.

按协议，低于 `512` 的 ID 预留给 World Explorer，因此对于场景创建的实体无效。数字 `0`, `1` 和 `2` 实际上正在使用中（见 [基础实体](https://github.com/decentraland/docs/blob/main/contributor/entities/README.md)），其余范围可供未来扩展使用。

每个场景都有自己的私有 ID 范围，Explorer 可以透明地将其映射到游戏引擎中所有实体的全局标识符。

随着时间推移，当实体被创建和删除时，重用先前释放的 ID 是完全合法的——事实上，这可能出于性能原因而是必要的（[见下文](https://github.com/decentraland/docs/blob/main/contributor/runtime-modules/crdtStateDeleteEntities/README.md)).

#### 同步 <a href="#synchronization" id="synchronization"></a>

World Explorer 和场景都必须管理一组共享实体和组件，更新会以异步方式从双方到来。如果没有额外措施，它们各自对世界状态的版本最终会（并且确实会）不同。

为防止这种情况，使用 CRDT（[无冲突复制数据类型](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)）通过交换带更新的消息来在双方之间达成一致。这带来若干优势：

* 双方都可以独立且并发地更新状态。
* 冲突由双方本地应用的共享解决策略来处理。
* 可保证双方最终收敛到共享且相同的状态。
* 除了消息大小略有开销外，不需要额外协调或消息往返。

{% hint style="info" %}
本页解释了如何使用 CRDT 在本地运行的场景与游戏引擎之间同步状态，但这种机制的真正潜力体现在多人场景中。使用此协议，玩家可以协调他们的游戏状态并共享相同的体验。
{% endhint %}

为使其工作，所有消息都必须满足可交换性和幂等性。即使某个假定的中间更新尚未收到，也可以应用乱序消息；相同的消息也可以重新处理而不会破坏一致性。

让我们重新看一下上面的架构图，现在聚焦于 CRDT：

```
.-------------------------------------------------------.
| 世界浏览器                                        |
|                                                       |
| .-------------.                       .------------.  |
| | 游戏引擎 |                       |   场景     | |
| |  .-------.  |                       |  .-------.  | |
| |  |       +--+-----------------------+->|       |  | |
| |  | CRDT  |  |     同步协议     |  | CRDT  |  | |
| |  |       |<-+-----------------------+--+       |  | |
| |  '-------'  |                       |  '-------'  | |
| |             |                       |             | |
| '-------------'                       '-------------' |
'-------------------------------------------------------'

```

CRDT 同步机制的实现由三部分组成：

1. 所有实体和组件状态的内部表示。
2. 用于在竞争状态之间选择的冲突解决策略。
3. 用于传达和处理状态更新的消息协议。

**CRDT 状态**

CRDT 保存着所有组件及其所附加到的所有实体的状态，并且可以将每个实体的组件映射为带时间戳的状态。伪代码如下：

```ts
// 某个组件在某个特定实体于某一时刻的（完全通用的）状态：
type EntityComponentState = { timestamp: number; state: byte[] };

// 一个组件对所有已附加实体的状态：
type ComponentState = Map<EntityId, EntityComponentState>;

// CRDT 的完整状态：
type CRDTState = Map<ComponentId, ComponentState>;
```

时间戳实际上并不是 Unix 风格的时间戳。它们是一种增量计数器，用于对更新进行排序，与时钟时间无关。关于这一点的更多内容 [如下](#timestamps).

由于 CRDT 层并不了解（也不需要了解）所有可用组件，初始状态是一个空映射。当操作涉及一个尚未映射的 `ComponentId` 或 `EntityId` 时，条目会即时创建。当实体被移除时，会从所有组件中删除关联状态。

让我们用更多伪代码来说明这一点。

```ts
const crdtState = new Map(); // 为空，我们会自动为新组件和新实体腾出空间
```

```ts
function putEntityComponentState(componentId, entityId, entityComponentState) {
  // 如果这个组件还未知，就添加它：
  if (!crdtState[componentId]) {
    crdtState[componentId] = new Map();
  }

  // 如果这个实体在此组件上没有状态，就添加并结束：
  if (!crdtState[componentId][entityId] && shouldCreate(entityId)) {
    crdtState[componentId][entityId] = entityComponentState;
    return;
  }

  // 到这里，我们有两个竞争状态。我们刚收到的那个不一定就是
  // 应当保留的那个。我们需要做出决定。
  const existingEntityComponentState = crdtState[componentId][entityId];

  // 只有当冲突解决规则如此规定时（稍后会详细说明），才替换状态：
  if (shouldReplace(entityComponentState, existingEntityComponentState)) {
    crdtState[componentId][entityId] = entityComponentState;
  }
}
```

上面的伪代码（如果翻译成实际代码）显然是不完整且次优的。尤其是，它缺少 `shouldCreate` 和 `shouldReplace`的定义，而这两个函数封装了冲突解决策略。这些函数有如下要求：

* `shouldCreate` 必须添加新实体，但要拒绝重新创建已删除的实体。
* `shouldReplace` 必须优先保留时间戳更大的状态（有一些边缘情况）。

为了支持 `shouldCreate` 这一要求，你需要跟踪已删除的实体 ID，以便忽略这些实体未来的任何状态更新。让我们再看一段伪代码：

```ts
const crdtDeletedEntities = new Set();

function shouldCreate(entityId) {
  return !crdtDeletedEntities.contains(entityId);
}

function deleteEntity(entityId) {
  // 将此实体标记为已删除：
  crdtDeletedEntities.add(entityId);

  // 删除该实体在每个组件中的状态：
  for (componentState of crdtState) {
    delete componentState[entityId];
  }
}
```

{% hint style="info" %}
请注意，在上面的朴素实现中，已删除实体的集合只会不断增长。在快速回收实体的场景中，这可能导致内存使用量激增。

为避免这种情况，Foundation 的 explorer 采用代际索引，在有限的数值空间内重用 ID，从而限制跟踪活动和已删除实体所需的内存量。
{% endhint %}

至于 `shouldReplace` 要求，见 [冲突解决](#crdtConflicts) 如下。

**时间戳**

如上所述，在谈到 CRDT 时间戳时，我们指的并不是某一时刻的实际 Unix 时间戳。相反，使用的是 [Lamport 时间戳](https://en.wikipedia.org/wiki/Lamport_timestamp) （一种特殊的共享增量计数器）来跟踪双方事件的顺序。

该计数器的值按以下规则更新：

1. 将本地计数器初始化为 `0`.
2. 在向远端方发送消息之前，将本地计数器加一。
3. 在从远端方接收消息后，将本地计数器设为其当前值与收到值中的最大值，再加 `1`.

伪代码如下：

```ts
const localTimestamp = 0;

function sendState(state) {
  send({ state, timestamp: ++localTimestamp });
}

function receiveState() {
  const { state, timestamp } = receive();
  localTimestamp = max(localTimestamp, remoteTimestamp) + 1;
}
```

**冲突解决**

当 CRDT 遇到两个竞争状态时，它需要一种所有参与方都以相同方式应用的共享解决策略，以确保每个人都得出相同的决定。Decentraland 协议使用非常简单的规则：

1. 如果实体已被删除（且 ID 从未重用），则忽略新状态。
2. 如果之前没有状态，则保留新状态。
3. 如果新状态的时间戳更大，则保留新状态。
4. 如果新状态的时间戳更小，则保留旧状态。
5. 如果两个时间戳相等，则逐字节比较状态并保留较小的值。

大多数情况将由规则 `1`, `2` 和 `3`.

**初始同步**

在场景开始运行自己的代码之前，运行时会用所有 [基础实体](https://github.com/decentraland/docs/blob/main/contributor/entities/README.md) 及其组件的状态填充 CRDT。

在此初始同步期间，只有运行时可以设置共享状态。在该过程完成之前，不允许场景进行修改。

**消息协议**

World Explorer 与场景运行时之间的消息是以序列化二进制表示形式构成的。每条消息都携带一个标头，标明类型和长度，后面跟着针对每种消息类型的特定有效载荷。

```
.----------------.--------------.---------------------------------.
| length: uint32 | type: uint32 |     payload: byte[length]       |
'----------------'--------------'---------------------------------'
╵         通用字段         ╵         依赖类型          ╵

```

CRDT 协议中有三种消息类型：

* [`PutComponent`](#PutComponent)
* [`DeleteComponent`](#DeleteComponent)
* [`DeleteEntity`](#DeleteEntity)

请注意，没有 `CreateComponent` 或 `CreateEntity` 消息。CRDT 规则会自动创建之前未知的实体和组件，正如 [上文所述](#crdtStateAutoCreate).

**`PutComponentMessage`**

更新 `的` 状态 `组件` 针对特定的 `实体`，必要时在 CRDT 中创建未知的组件和实体。 [按](#crdtConflicts) 如下： `时间戳`.

```
.----------------.-------------------.-------------------------------------------.
| entity: uint32 | component: uint32 | timestamp: uint32 |     state: byte[]     |
'----------------'-------------------'-------------------------------------------'
╵                       通用字段                    ╵  依赖组件  ╵

```

该 `的` 该字段是由组件定义的二进制序列化。大多数组件使用 [协议缓冲区](https://protobuf.dev) 来按照 `.proto` 文件中的 [Decentraland 协议包](https://github.com/decentraland/protocol)进行消息编码。该规则的一个显著例外是 `变换` 组件，它（远为最常见）拥有一种优化的序列化格式。

这一额外的序列化层具有一个重要优势：CRDT 协议对当前或未来的任何组件实现都保持中立。

如果本消息中包含的更新被应用到 CRDT， `的` 字段将按原样复制到结构中。

**`DeleteComponentMessage`**

移除 `组件` 的状态，针对一个 `实体`. [按](#crdtConflicts) 如下： `时间戳`.

```
.----------------.-------------------.-------------------.
| entity: uint32 | component: uint32 | timestamp: uint32 |
'----------------'-------------------'-------------------'

```

**`DeleteEntityMessage`**

删除 `实体` （即所有关联的组件状态），并期望该标识符永远不会被用于其他实体。

```
.----------------.
| entity: uint32 |
'----------------'

```

请注意，没有 `时间戳` 字段。由于 `实体` 的生命周期已经结束，对于任何乱序更新的冲突解决策略就是直接忽略它们。

### 方法

中的方法集 `EngineApi` 提供了交换消息并从零重建世界状态的接口。

**`crdtSendToRenderer`**

将一条序列化消息从场景发送到渲染器，并返回渲染器为场景持有的序列化消息数组。

```ts
接口 Request {
  // 此请求的序列化消息：
  data: byte[];
}

interface Response {
  // 来自渲染器的序列化消息数组（如果有）：
  data: byte[][];
}

function crdtSendToRenderer(Request): Promise<Response>;
```

**`crdtGetState`**

```ts
interface Request {}

interface Response {
  // 状态是否包含场景创建的实体：
  hasEntities: boolean;

  // 可重建 CRDT 状态的消息数组：
  data: byte[][];
}

function crdtGetState(Request): Promise<Response>;
```

### 旧版场景

旧式场景（即使用 SDK 6 或更早版本构建的场景）不使用现代 CRDT 机制，而是调用 `EngineApi` 中现已弃用的方法。

协议兼容的 Decentraland 应用并不要求支持这些场景。


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/contributor/contributor-zh/chang-jing-yun-xing-shi/yun-xing-shi-mo-kuai/engine-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
