Engine API
O EngineApi o módulo fornece acesso à estrutura Entity-Component-System compartilhada entre o World Explorer (que executa o loop do jogo) e cenas individuais (que gerenciam suas próprias entidades), além de utilitários relacionados.
const engine = require("~system/EngineApi");Este módulo é o mais complexo e rico em funcionalidades, e o que provavelmente receberá atualizações e extensões. É onde reside a maior parte do poder do Decentraland, já que determina o tipo de experiências que as pessoas podem criar.
Este módulo contém os seguintes métodos:
Introdução
O EngineApi o módulo foi projetado para sincronizar o estado do mundo entre o Explorer e a cena trocando atualizações, eventos e comandos. Implementa um protocolo de mensagens extensível que permite às cenas, entre outras coisas:
Criar e destruir entidades.
Anexar, atualizar e remover componentes.
Lançar raios no ambiente 3D e detectar colisões.
Receber eventos como entrada do jogador.
.------------------------------------------------------.
| World Explorer |
| |
| [Engine API] |
| | |
| .--------. | .------------. |
| | |<-----------+<-------------+ Runtime | |
| | Game | | Commands | .------. | |
| | | Events | | | Scene | | |
| | Engine +----------->+------------->| | | | |
| | | | | '-------' | |
| '--------' | '------------' |
| |
'------------------------------------------------------'
O EngineApi o módulo está passando por reformas. Se você for às definições de origem, encontrará métodos legados que não são mais usados ou que podem ser implementados como no-ops na versão mais recente (mais sobre isso abaixo).
Framework ECS
Com o EngineApi módulo vem uma implementação genérica e extensível de um framework ECS compartilhado, que permite tanto às cenas quanto ao próprio motor do jogo criar entidades, anexar componentes e atualizar seus estados.
As mudanças feitas de qualquer lado são refletidas no outro por meio da troca de mensagens. O mecanismo usado (veja abaixo) garante que ambas as partes concordem sobre a ordem de quaisquer atualizações e alcancem consistência eventual sobre o estado do mundo.
O World Explorer implementa um conjunto bem conhecido de componentes básicos (posições, formas, texturas, mídias e mais), e sabe desserializar e aplicar alterações ao seu estado quando recebe uma atualização. Cenas usando o SDK também têm utilitários para criar componentes customizados, mas esses são totalmente gerenciados pela cena (não sincronizados) e, portanto, estão fora do escopo do protocolo.
A maioria das cenas usa o Decentraland SDK, que encapsula o EngineApi módulo e oferece uma interface de nível mais alto e muito melhor para desenvolvedores de conteúdo. Cenas que acessam diretamente o protocolo de mensagens neste módulo são extremamente raras.
Identificando Entidades
IDs de entidade são inteiros simples começando com 0.
Por protocolo, IDs abaixo de 512 são reservados para o World Explorer, e portanto inválidos para entidades criadas pela cena. Números 0, 1 e 2 estão de fato em uso (veja entidades básicas), com o restante do intervalo disponível para futuras extensões.
Cada cena tem sua própria faixa privada de IDs, que o Explorer pode mapear de forma transparente para um identificador global entre todas as entidades no motor do jogo.
Com o tempo, à medida que entidades são criadas e excluídas, é perfeitamente válido reutilizar IDs que foram previamente liberados — de fato, isso pode ser necessário por razões de desempenho (veja abaixo).
Sincronização
Tanto o World Explorer quanto a cena devem gerenciar um conjunto de entidades e componentes compartilhados, onde atualizações vêm de ambos os lados de forma assíncrona. Sem medidas adicionais, suas versões do estado do mundo podem (e vão) acabar diferentes.
Para prevenir isso, um CRDT (tipo de dado replicado sem conflitos) é usado para chegar a um acordo entre ambas as partes trocando mensagens com atualizações. Isso oferece várias vantagens:
Ambas as partes podem atualizar o estado de forma independente e concorrente.
Conflitos são gerenciados por uma estratégia de resolução compartilhada aplicada localmente por ambas as partes.
Ambas as partes têm garantia de convergir para um estado compartilhado e idêntico.
Além de um pequeno overhead no tamanho das mensagens, nenhuma coordenação adicional ou ida-e-volta de mensagens é necessária.
Esta página explica o uso do CRDT para sincronizar estado entre uma cena executando localmente e o motor do jogo, mas o verdadeiro potencial deste mecanismo está em cenários multiplayer. Usando este protocolo, jogadores podem coordenar seu estado de jogo e compartilhar a mesma experiência.
Para que isso funcione, todas as mensagens devem ser comutativas e idempotentes. Mensagens fora de ordem podem ser aplicadas mesmo se uma suposta atualização intermediária ainda não tiver sido recebida, e mensagens iguais podem ser reprocessadas sem quebrar a consistência.
Vamos revisitar o diagrama arquitetônico acima, agora focando no CRDT:
A implementação do mecanismo de sincronização CRDT consiste em três partes:
Uma representação interna do estado de todas as entidades e componentes.
Uma estratégia de resolução de conflitos para escolher entre estados concorrentes.
Um protocolo de mensagens para comunicar e processar atualizações ao estado.
Estado do CRDT
O CRDT mantém o estado de todos os componentes para todas as suas entidades anexadas, e pode ser armazenado em um mapeamento de componentes para estados com carimbo de tempo (timestamp) para cada entidade. Em pseudocódigo:
Timestamps na verdade não são timestamps no estilo Unix. Eles são um tipo de contador incremental para sequenciar atualizações, sem relação com o tempo do relógio. Mais sobre isso abaixo.
Como a camada CRDT não conhece (nem precisa conhecer) todos os componentes disponíveis, o estado inicial é um mapa vazio. Entradas são criadas sob demanda quando operações envolvem um ComponentId ou EntityId que ainda não estava mapeado. Quando uma entidade é removida, o estado associado é excluído de todos os componentes.
Vamos ilustrar isso com mais pseudocódigo.
O pseudocódigo acima (se traduzido para código real) é obviamente incompleto e subótimo. Em particular, falta a definição de shouldCreate e shouldReplace , que encapsulam a estratégia de resolução de conflitos. Estes têm requisitos:
shouldCreatedeve adicionar novas entidades, mas recusar recriar entidades deletadas.shouldReplacedeve preferir manter estados que têm timestamps maiores (com alguns casos limite).
Para suportar o requisito de shouldCreate , você vai querer rastrear IDs de entidades deletadas, para ignorar quaisquer futuras atualizações de estado para elas. Vamos ver isto em mais pseudocódigo:
Note que, na implementação ingênua acima, o conjunto de entidades deletadas só crescerá. Em cenas que reciclam entidades rapidamente, isso pode levar a uma explosão no uso de memória.
Para evitar isso, o explorer da Foundation emprega um índice generacional para reutilizar IDs em um espaço numérico finito, limitando a quantidade de memória necessária para acompanhar tanto entidades ativas quanto deletadas.
Quanto ao requisito de shouldReplace , veja resolução de conflitos abaixo.
Timestamps
Como mencionado acima, ao falar sobre timestamps do CRDT não estamos nos referindo a timestamps Unix reais para um ponto no tempo. Em vez disso, um Lamport timestamp (um tipo especial de contador incremental compartilhado) é usado para acompanhar a sequência de eventos por ambas as partes.
O valor deste contador é atualizado de acordo com estas regras:
Inicializar o contador local em
0.Antes de enviar uma mensagem para partes remotas, incrementar o contador local.
Após receber uma mensagem de uma parte remota, definir o contador local para o máximo entre seu valor e o valor recebido, mais
1.
Em pseudocódigo:
Resolução de Conflitos
Quando o CRDT encontra dois estados concorrentes, ele precisa de uma estratégia de resolução compartilhada que todas as partes apliquem de forma idêntica, garantindo que a mesma decisão seja alcançada por todos. O protocolo Decentraland usa regras muito simples:
Se a entidade foi deletada (e o ID nunca reutilizado), ignorar novo estado.
Se não havia estado prévio, manter o novo estado.
Se o novo estado tem um timestamp maior, manter o novo estado.
Se o novo estado tem um timestamp menor, manter o estado antigo.
Se ambos os timestamps são iguais, comparar os estados byte-a-byte e manter o menor valor.
A maioria dos casos será resolvida pelas regras 1, 2 e 3.
Sincronização Inicial
Antes de uma cena começar a executar seu próprio código, o runtime popula o CRDT com o estado de todos entidades básicas e seus componentes.
Durante essa sincronização inicial, somente o runtime pode definir o estado compartilhado. A cena não tem permissão para fazer modificações até que o processo esteja completo.
Protocolo de Mensagens
Mensagens entre o World Explorer e o runtime da cena são estruturas em uma representação binária serializada. Cada mensagem carrega um cabeçalho indicando o tipo e comprimento, seguido por um payload particular para cada tipo de mensagem.
Existem três tipos de mensagens no protocolo CRDT:
Observe que não existem CreateComponent ou CreateEntity mensagens. As regras do CRDT auto-criam entidades e componentes que não eram previamente conhecidos, como explicado acima.
PutComponentMessage
Atualizar o estado de um componente para um determinado entity , criando componentes e entidades desconhecidos no CRDT se necessário. Resolver conflitos de acordo com timestamp.
O estado field é uma serialização binária definida pelo componente. A maioria dos componentes usa protocol buffers para codificar mensagens conforme especificado no .proto arquivos do pacote de protocolo Decentraland. A exceção notável a esta regra é o Transform componente, que (sendo de longe o mais comum) tem um formato de serialização otimizado.
Esta camada adicional de serialização tem uma vantagem importante: o protocolo CRDT é agnóstico a quaisquer implementações de componentes presentes ou futuras.
Se a atualização contida nesta mensagem for aplicada ao CRDT, o estado campo é copiado tal como está para dentro da estrutura.
DeleteComponentMessage
Remover o estado de componente para um entity. Resolver conflitos de acordo com timestamp.
DeleteEntityMessage
Deletar entity (i.e. todo o estado de componente associado), esperando que o identificador nunca seja reutilizado para uma entidade diferente.
Observe que não há campo timestamp . Como o tempo de vida de entity acabou, a estratégia de resolução de conflitos para quaisquer atualizações fora de ordem é simplesmente ignorá-las.
Métodos
O conjunto de métodos em EngineApi fornece a interface para trocar mensagens e reconstruir o estado do mundo do zero.
crdtSendToRenderer
Enviar uma mensagem serializada da cena para o renderer, retornar um array de mensagens serializadas que o renderer tem para a cena.
crdtGetState
Cenas Legadas
Cenas ao estilo antigo (i.e. aquelas construídas usando o SDK versão 6 ou anterior) não usam o mecanismo CRDT moderno, em vez disso chamando métodos em EngineApi que agora estão obsoletos.
O suporte a essas cenas não é um requisito para uma aplicação Decentraland compatível com o protocolo.
Atualizado