> 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` 이 모듈은 개편 중입니다. 소스 정의로 가면 더 이상 사용되지 않거나 최신 버전에서는 no-op으로 구현할 수 있는 레거시 메서드들을 찾을 수 있습니다(자세한 내용은 아래에서 설명합니다).
{% 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`.

프로토콜상, 다음보다 작은 ID는 `512` World Explorer에 예약되어 있으므로, 씬에서 생성한 엔티티에는 사용할 수 없습니다. 숫자 `0`, `1` 및 `2` 는 실제로 사용 중이며( [기본 엔티티](https://github.com/decentraland/docs/blob/main/contributor/entities/README.md))을 참조), 나머지 범위는 향후 확장을 위해 남겨져 있습니다.

각 씬은 자체적인 비공개 ID 범위를 가지며, 익스플로러는 이를 게임 엔진 내의 모든 엔티티에 대한 전역 식별자로 투명하게 매핑할 수 있습니다.

시간이 지나 엔티티가 생성되고 삭제되면, 이전에 해제된 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>;
```

타임스탬프는 실제로 유닉스 스타일의 타임스탬프가 아닙니다. 이는 시계 시간과는 무관하게 업데이트 순서를 매기기 위한 증가 카운터의 한 종류입니다. 이에 대한 자세한 내용은 [아래에서](#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 타임스탬프를 말할 때 특정 시점의 실제 유닉스 타임스탬프를 가리키는 것은 아닙니다. 대신 [람포트 타임스탬프](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[]     |
'----------------'-------------------'-------------------------------------------'
╵                       공통 필드                    ╵  컴포넌트 종속  ╵

```

이 `상태를 업데이트합니다` field는 컴포넌트가 정의한 이진 직렬화입니다. 대부분의 컴포넌트는 [프로토콜 버퍼](https://protobuf.dev) 메시지를 다음에서 지정된 형식으로 인코딩하기 위해 `.proto` 파일을 [Decentraland 프로토콜 패키지의](https://github.com/decentraland/protocol)로 사용합니다. 이 규칙의 주목할 만한 예외는 `Transform` 컴포넌트인데, 이는 압도적으로 가장 흔하기 때문에 최적화된 직렬화 형식을 가지고 있습니다.

이 추가적인 직렬화 계층은 중요한 장점이 있습니다. CRDT 프로토콜이 현재 또는 미래의 어떤 컴포넌트 구현에도 구애받지 않는다는 점입니다.

이 메시지에 포함된 업데이트가 CRDT에 적용되면, `상태를 업데이트합니다` field는 구조체에 그대로 복사됩니다.

**`DeleteComponentMessage`**

의 상태를 제거합니다 `컴포넌트의` 에 대한 `엔티티를`. [충돌을 해소합니다](#crdtConflicts) 에 따라 `타임스탬프`.

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

```

**`DeleteEntityMessage`**

삭제합니다 `엔티티를` (즉, 모든 관련 컴포넌트 상태), 그리고 이 식별자가 다른 엔티티에 대해 다시 사용되지 않을 것으로 기대합니다.

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

```

다음이 없다는 점에 유의하세요 `타임스탬프` field. 이제 `엔티티를` 의 수명이 끝났으므로, 순서가 뒤바뀐 업데이트에 대한 충돌 해소 전략은 단순히 그것들을 무시하는 것입니다.

### 메서드

의 메서드 집합은 `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 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-ko/scene-runtime/runtime-modules/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.
