> 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-ko/scene-runtime/runtime-modules/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` 모듈은 개편 중입니다. 소스 정의로 이동하면 더 이상 사용되지 않거나 최신 버전에서 아무 작업도 하지 않도록 구현할 수 있는 레거시 메서드를 찾을 수 있습니다(자세한 내용은 아래 참조).
{% endhint %}

### ECS 프레임워크

이 `EngineApi` 모듈에는 공유 ECS 프레임워크의 범용적이고 확장 가능한 구현이 포함되어 있어, 씬과 게임 엔진 자체 모두 엔티티를 생성하고 컴포넌트를 연결하며 상태를 업데이트할 수 있습니다.

양쪽에서 이루어진 변경 사항은 메시지를 교환하여 반대편에 반영됩니다. 사용되는 메커니즘([아래 참조](#synchronization))은 양측이 모든 업데이트의 순서에 합의하고 월드 상태에 대해 최종 일관성에 도달하도록 보장합니다.

월드 익스플로러는 잘 알려진 기본 컴포넌트 집합(위치, 형태, 텍스처, 미디어 등)을 구현하며, 업데이트를 수신하면 해당 상태 변경을 역직렬화하고 적용하는 방법을 알고 있습니다.  [SDK](https://docs.decentraland.org/creator) 를 사용하는 씬에는 사용자 지정 컴포넌트를 생성하는 유틸리티도 있지만, 이는 전적으로 씬에서 관리되며(동기화되지 않음) 따라서 프로토콜의 범위 밖에 있습니다.

{% hint style="info" %}
대부분의 씬은 Decentraland SDK를 사용하며, 이 SDK는  `EngineApi` 모듈을 캡슐화하고 콘텐츠 개발자에게 훨씬 더 편리한 고수준 인터페이스를 제공합니다. 이 모듈의 메시징 프로토콜에 직접 액세스하는 씬은 극히 드뭅니다.
{% endhint %}

#### 엔티티 식별 <a href="#identifying" id="identifying"></a>

엔티티 ID는 다음에서 시작하는 일반 정수입니다 `0`.

프로토콜상 다음보다 작은 ID는 `512` 월드 익스플로러용으로 예약되어 있으므로 씬에서 생성한 엔티티에는 유효하지 않습니다. 숫자 `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[]     |
'----------------'-------------------'-------------------------------------------'
╵                       공통 필드                    ╵  컴포넌트 의존적  ╵

```

그 `상태` 필드는 컴포넌트에서 정의한 바이너리 직렬화입니다. 대부분의 컴포넌트는  [프로토콜 버퍼](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[];
}

인터페이스 Response {
  // 렌더러에서 온 직렬화된 메시지 배열(있는 경우):
  data: byte[][];
}

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

**`crdtGetState`**

```ts
인터페이스 Request {}

인터페이스 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-ko/scene-runtime/runtime-modules/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.
