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 +----------->+------------->| | | | |
| | | | | '-------' | |
| '--------' | '------------' |
| |
'------------------------------------------------------'
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 SDK 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.
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ásicas), 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 abajo).
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 conflictos) 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.
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:
Una representación interna del estado de todas las entidades y componentes.
Una estrategia de resolución de conflictos para elegir entre estados en competencia.
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:
shouldCreatedeben añadir nuevas entidades, pero negarse a recrear entidades eliminadas.shouldReplacedeben 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:
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 Lamport (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:
Inicializar el contador local a
0.Antes de enviar un mensaje a partes remotas, incrementar el contador local.
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:
Si la entidad fue eliminada (y el ID nunca se reutilizó), ignorar el nuevo estado.
Si no había estado previo, conservar el nuevo estado.
Si el nuevo estado tiene una marca de tiempo mayor, conservar el nuevo estado.
Si el nuevo estado tiene una marca de tiempo menor, conservar el estado antiguo.
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ásicas 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 buffers para codificar mensajes como se especifica en el .proto archivos de el paquete del protocolo de Decentraland. 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