Engine API

El EngineApi módulo proporciona acceso al framework Entity-Component-System compartido entre el World Explorer (que ejecuta el bucle del juego) y las escenas individuales (que gestionan sus propias entidades), además de utilidades relacionadas.

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

Este módulo es el más complejo y rico en funcionalidad, y el que con mayor probabilidad recibirá actualizaciones y extensiones. Es donde reside la mayor parte del poder de Decentraland, ya que determina el tipo de experiencias que la gente puede crear.

Este módulo contiene los siguientes métodos:

Introducción

El EngineApi módulo está diseñado para sincronizar el estado del mundo entre el Explorer y la escena intercambiando actualizaciones, eventos y comandos. Implementa un protocolo de mensajes extensible que permite a las escenas, entre otras cosas:

  • Crear y destruir entidades.

  • Adjuntar, actualizar y eliminar componentes.

  • Lanzar rayos en el entorno 3D y detectar impactos.

  • Recibir eventos como la entrada del jugador.

.------------------------------------------------------.
| World Explorer                                       |
|                                                      |
|                   [Engine API]                       |
|                        |                             |
|  .--------.            |              .------------. |
|  |        |<-----------+<-------------+  Runtime   | |
|  |  Game  |            |   Commands   |  .------.  | |
|  |        |   Events   |              |  | Scene | | |
|  | Engine +----------->+------------->|  |       | | |
|  |        |            |              |  '-------' | |
|  '--------'            |              '------------' |
|                                                      |
'------------------------------------------------------'
circle-info

El EngineApi módulo está siendo renovado. Si vas a las definiciones de origen, encontrarás métodos heredados que ya no se usan o que pueden implementarse como no-ops en la versión más reciente (más sobre esto abajo).

Framework ECS

Con el EngineApi módulo viene una implementación genérica y extensible de un framework ECS compartido, que permite tanto a las escenas como al motor del juego crear entidades, adjuntar componentes y actualizar sus estados.

Los cambios realizados desde cualquiera de los lados se reflejan en el otro mediante el intercambio de mensajes. El mecanismo usado (ver más abajo) asegura que ambas partes estén de acuerdo sobre el orden de las actualizaciones y alcancen eventual consistencia sobre el estado del mundo.

El World Explorer implementa un conjunto conocido de componentes básicos (posiciones, formas, texturas, medios y más), y sabe cómo deserializar y aplicar cambios a su estado cuando recibe una actualización. Las escenas que usan el SDKarrow-up-right también tienen utilidades para crear componentes personalizados, pero esos son totalmente gestionados por la escena (no sincronizados) y por tanto están fuera del alcance del protocolo.

circle-info

La mayoría de las escenas usan el SDK de Decentraland, que encapsula el EngineApi módulo y ofrece una interfaz de nivel más alto y mucho más agradable para los desarrolladores de contenido. Las escenas que acceden directamente al protocolo de mensajería en este módulo son extremadamente raras.

Identificación de Entidades

Los IDs de entidad son enteros simples que empiezan con 0.

Por protocolo, los IDs por debajo de 512 están reservados para el World Explorer, y por tanto son inválidos para entidades creadas por la escena. Los números 0, 1 y 2 están realmente en uso (ver entidades básicasarrow-up-right), con el resto del rango disponible para futuras extensiones.

Cada escena tiene su propio rango privado de IDs, que el Explorer puede mapear de forma transparente a un identificador global entre todas las entidades en el motor del juego.

Con el tiempo, a medida que las entidades se crean y eliminan, es perfectamente válido reutilizar IDs que fueron liberados anteriormente -- de hecho, esto puede ser requerido por razones de rendimiento (ver más abajoarrow-up-right).

Sincronización

Tanto el World Explorer como la escena deben gestionar un conjunto de entidades y componentes compartidos, donde las actualizaciones provienen de ambos lados de forma asíncrona. Sin medidas adicionales, sus versiones del estado del mundo pueden (y acabarán) siendo diferentes.

Para evitar esto, se usa un CRDT (tipo de dato replicado sin conflictosarrow-up-right) para alcanzar un acuerdo entre ambas partes mediante el intercambio de mensajes con actualizaciones. Esto ofrece varias ventajas:

  • Ambas partes pueden actualizar el estado de forma independiente y concurrente.

  • Los conflictos se gestionan mediante una estrategia de resolución compartida aplicada localmente por ambas partes.

  • Se garantiza que ambas partes converjan a un estado compartido e idéntico.

  • Aparte de una pequeña sobrecarga en el tamaño de los mensajes, no se requieren coordinación adicional ni rondas de mensajes adicionales.

circle-info

Esta página explica el uso del CRDT para sincronizar el estado entre una escena que se ejecuta localmente y el motor del juego, pero el verdadero potencial de este mecanismo radica en escenarios multijugador. Usando este protocolo, los jugadores pueden coordinar su estado de juego y compartir la misma experiencia.

Para que esto funcione, todos los mensajes deben ser conmutativos e idempotentes. Los mensajes fuera de orden pueden aplicarse incluso si una supuesta actualización intermedia no se recibió aún, y mensajes iguales pueden re-procesarse sin romper la consistencia.

Revisemos el diagrama arquitectónico anterior, ahora enfocándonos en el CRDT:

La implementación del mecanismo de sincronización CRDT consta de tres partes:

  1. Una representación interna del estado de todas las entidades y componentes.

  2. Una estrategia de resolución de conflictos para elegir entre estados en competencia.

  3. Un protocolo de mensajería para comunicar y procesar actualizaciones al estado.

Estado CRDT

El CRDT mantiene el estado de todos los componentes para todas sus entidades adjuntas, y puede almacenarse en un mapeo de componentes a estados con marcas de tiempo para cada entidad. En pseudocódigo:

Las marcas de tiempo en realidad no son marcas de tiempo estilo Unix. Son un tipo de contador incremental para secuenciar actualizaciones, sin relación con la hora del reloj. Más sobre esto a continuación.

Dado que la capa CRDT no conoce (ni necesita conocer) todos los componentes disponibles, el estado inicial es un mapa vacío. Las entradas se crean sobre la marcha cuando las operaciones involucran un ComponentId o EntityId que no estaba ya mapeado. Cuando se elimina una entidad, el estado asociado se borra de todos los componentes.

Ilustremos esto con más pseudocódigo.

El pseudocódigo anterior (si se tradujera a código real) es obviamente incompleto y subóptimo. En particular, falta la definición de shouldCreate y shouldReplace, que encapsulan la estrategia de resolución de conflictos. Estos tienen requisitos:

  • shouldCreate deben añadir nuevas entidades, pero negarse a recrear entidades eliminadas.

  • shouldReplace deben preferir conservar estados que tengan marcas de tiempo mayores (con algunos casos borde).

Para soportar el shouldCreate requisito, querrás llevar un registro de los IDs de entidades eliminadas, con el fin de ignorar cualquier actualización de estado futura para esos. Veamos esto en más pseudocódigo:

circle-info

Nótese que, en la implementación ingenua anterior, el conjunto de entidades eliminadas solo crecerá. En escenas que reciclan rápidamente entidades, esto puede llevar a una explosión en el uso de memoria.

Para evitar esto, el explorer de la Foundation emplea un índice generacional para reutilizar IDs en un espacio numérico finito, limitando la cantidad de memoria requerida para llevar un registro tanto de entidades activas como eliminadas.

En cuanto al shouldReplace requisito, ver resolución de conflictos a continuación.

Marcas de tiempo

Como se mencionó arriba, al hablar de marcas de tiempo CRDT no nos referimos a marcas de tiempo Unix reales para un punto en el tiempo. En su lugar, se usa una marca de tiempo Lamportarrow-up-right (un tipo especial de contador incremental compartido) para llevar el registro de la secuencia de eventos por ambas partes.

El valor de este contador se actualiza de acuerdo con estas reglas:

  1. Inicializar el contador local a 0.

  2. Antes de enviar un mensaje a partes remotas, incrementar el contador local.

  3. Después de recibir un mensaje de una parte remota, establecer el contador local al máximo entre su valor y el valor recibido, más 1.

En pseudocódigo:

Resolución de conflictos

Cuando el CRDT encuentra dos estados en competencia, necesita una estrategia de resolución compartida que todas las partes apliquen de forma idéntica, asegurando que la misma decisión sea alcanzada por todos. El protocolo de Decentraland usa reglas muy simples:

  1. Si la entidad fue eliminada (y el ID nunca se reutilizó), ignorar el nuevo estado.

  2. Si no había estado previo, conservar el nuevo estado.

  3. Si el nuevo estado tiene una marca de tiempo mayor, conservar el nuevo estado.

  4. Si el nuevo estado tiene una marca de tiempo menor, conservar el estado antiguo.

  5. Si ambas marcas de tiempo son iguales, comparar los estados byte por byte y conservar el valor menor.

La mayoría de los casos se resolverán por las reglas 1, 2 y 3.

Sincronización inicial

Antes de que una escena comience a ejecutar su propio código, el runtime puebla el CRDT con el estado de todas las entidades básicasarrow-up-right y sus componentes.

Durante esta sincronización inicial, solo el runtime puede establecer el estado compartido. A la escena no se le permite hacer modificaciones hasta que el proceso esté completo.

Protocolo de mensajería

Los mensajes entre el World Explorer y el runtime de la escena son estructuras en una representación binaria serializada. Cada mensaje lleva un encabezado que indica el tipo y la longitud, seguido por una carga útil particular para cada tipo de mensaje.

Hay tres tipos de mensajes en el protocolo CRDT:

Nótese que no existen CreateComponent o CreateEntity mensajes. Las reglas CRDT auto-crean entidades y componentes que no se conocían previamente, como se explicó arriba.

PutComponentMessage

Actualizar el estado de un componente para un particular entity, creando componentes y entidades desconocidas en el CRDT si es necesario. Resolver conflictos de acuerdo con timestamp.

El estado field es una serialización binaria definida por el componente. La mayoría de los componentes usan protocol buffersarrow-up-right para codificar mensajes como se especifica en el .proto archivos de el paquete del protocolo de Decentralandarrow-up-right. La excepción notable a esta regla es el Transform componente, que (siendo con diferencia el más común) tiene un formato de serialización optimizado.

Esta capa adicional de serialización tiene una ventaja importante: el protocolo CRDT es agnóstico a cualquier implementación de componente presente o futura.

Si la actualización contenida en este mensaje se aplica al CRDT, el estado campo se copia tal cual en la estructura.

DeleteComponentMessage

Eliminar el estado de componente para un entity. Resolver conflictos de acuerdo con timestamp.

DeleteEntityMessage

Eliminar entity (es decir, todo el estado de componente asociado), esperando que el identificador nunca se reutilice para una entidad diferente.

Nótese que no existe un timestamp campo. Dado que la vida útil de entity ha terminado, la estrategia de resolución de conflictos para cualquier actualización fuera de orden es simplemente ignorarlas.

Métodos

El conjunto de métodos en EngineApi proporcionan la interfaz para intercambiar mensajes y reconstruir el estado del mundo desde cero.

crdtSendToRenderer

Enviar un mensaje serializado desde la escena al renderer, devolver un arreglo de mensajes serializados que el renderer tiene para la escena.

crdtGetState

Escenas heredadas

Las escenas de estilo antiguo (es decir, aquellas construidas usando el SDK versión 6 o anterior) no usan el mecanismo CRDT moderno, en su lugar llaman a métodos en EngineApi que ahora están obsoletos.

El soporte para estas escenas no es un requisito para una aplicación de Decentraland conforme al protocolo.

Última actualización