> 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` 该模块提供对世界探索器（负责运行游戏循环）与各个场景（负责管理各自实体）之间共享的实体-组件-系统框架的访问，以及相关工具。

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

该模块是功能最复杂、最丰富的模块，也是最有可能接收更新和扩展的模块。Decentraland 的大部分能力都体现在这里，因为它决定了人们能够创建什么样的体验。

此模块包含以下方法：

* [`function crdtSendToRenderer`](#crdtSendToRenderer)
* [`function crdtGetState`](#crdtGetState)

### 简介

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

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

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

```

{% hint style="info" %}
该 `EngineApi` 该模块正在进行改造。如果你查看源定义，会发现一些已不再使用的旧方法，或者它们可以在最新版本中实现为 no-op（关于这一点，下文会进一步说明）。
{% endhint %}

### ECS 框架

借助 `EngineApi` 模块，提供了一个共享 ECS 框架的通用且可扩展的实现，使场景和游戏引擎本身都能够创建实体、附加组件并更新其状态。

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

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

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

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

实体 ID 是从 `0`.

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

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

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

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

世界探索器和场景都必须管理一组共享实体和组件，其中更新会以异步方式来自双方。若没有额外措施，世界状态的各自版本最终可能（并且会）变得不同。

为防止这种情况，会使用 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 的探索器采用代际索引，在有限的数字空间中重用 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/tree/main/contributor/entities/README.md) 及其组件的状态填充 CRDT。

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

**消息协议**

世界探索器与场景运行时之间的消息是以序列化二进制表示的结构。每条消息都携带一个指示类型和长度的头部，后跟每种消息类型对应的特定有效载荷。

```
.----------------.--------------.---------------------------------.
| 长度: uint32 | 类型: uint32 |     有效载荷: byte[length]       |
'----------------'--------------'---------------------------------'
╵         公共字段         ╵         依赖类型          ╵

```

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

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

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

**`PutComponentMessage`**

更新 `状态` 的 `组件` 用于某个特定 `实体`，必要时在 CRDT 中创建未知组件和实体。 [根据](#crdtConflicts) 解决冲突 `时间戳`.

```
.----------------.-------------------.-------------------------------------------.
| 实体: uint32 | 组件: uint32 | 时间戳: uint32 |     状态: byte[]     |
'----------------'-------------------'-------------------------------------------'
╵                       公共字段                    ╵  依赖组件                ╵

```

该 `状态` 字段是由组件定义的二进制序列化。大多数组件使用 [Protocol Buffers](https://protobuf.dev) 来按照 `.proto` 文件中的 [Decentraland 协议包](https://github.com/decentraland/protocol)。这个规则的一个显著例外是 `变换` 组件，它（迄今为止最常见）拥有优化的序列化格式。

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

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

**`DeleteComponentMessage`**

移除 `组件` 的状态，用于 `实体`. [根据](#crdtConflicts) 解决冲突 `时间戳`.

```
.----------------.-------------------.-------------------.
| 实体: uint32 | 组件: uint32 | 时间戳: uint32 |
'----------------'-------------------'-------------------'

```

**`DeleteEntityMessage`**

删除 `实体` （即所有相关组件状态），并假定该标识符永远不会再次用于其他实体。

```
.----------------.
| 实体: uint32 |
'----------------'

```

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

### 方法

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

**`crdtSendToRenderer`**

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

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