For the complete documentation index, see llms.txt. This page is also available as Markdown.

API do Engine

O EngineApi O módulo fornece acesso ao framework Entity-Component-System partilhado entre o World Explorer (que executa o ciclo de jogo) e as scenes individuais (que gerem as suas próprias entities), além de utilitários relacionados.

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

Este módulo é o mais complexo e rico em funcionalidades, e o mais provável de receber atualizações e extensões. É aqui que 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 concebido para sincronizar o estado do mundo entre o Explorer e a scene através da troca de atualizações, eventos e comandos. Implementa um protocolo de mensagens extensível que permite às scenes, entre outras coisas:

  • Criar e destruir entities.

  • Associar, atualizar e remover components.

  • Lançar rays no ambiente 3D e detetar impactos.

  • Receber events como input do player.

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

O EngineApi O módulo está a passar por renovação. Se fores às definições de origem, vais encontrar métodos legacy que já não são usados ou que podem ser implementados como no-ops na versão mais recente (mais sobre isto abaixo).

Framework ECS

Com o EngineApi módulo vem uma implementação genérica e extensível de um framework ECS partilhado, que permite tanto às scenes como ao próprio game engine criar entities, associar components e atualizar os seus estados.

As alterações feitas de qualquer dos lados refletem-se no outro através da troca de mensagens. O mecanismo utilizado (veja abaixo) garante que ambas as partes concordam com a ordem de quaisquer atualizações e alcançam uma consistência eventual quanto ao estado do mundo.

O World Explorer implementa um conjunto conhecido de components básicos (posições, formas, texturas, media e mais) e sabe desserializar e aplicar alterações ao seu estado quando recebe uma atualização. Scenes que usam o SDK também dispõem de utilitários para criar components personalizados, mas esses são inteiramente geridos pela scene (não sincronizados) e, portanto, estão fora do âmbito do protocolo.

A maioria das scenes usa o Decentraland SDK, que encapsula o EngineApi módulo e oferece uma interface muito mais agradável e de nível mais alto para os criadores de conteúdo. Scenes que acedem diretamente ao protocolo de mensagens neste módulo são extremamente raras.

Identificação de Entities

Os IDs das entities são inteiros simples que começam com 0.

Por protocolo, os IDs abaixo de 512 estão reservados ao World Explorer e, por isso, são inválidos para entities criadas pela scene. Os números 0, 1 e 2 estão realmente em uso (ver basic entities), com o restante do intervalo disponível para extensões futuras.

Cada scene tem o seu próprio intervalo privado de IDs, que o Explorer pode mapear de forma transparente para um identificador global entre todas as entities no game engine.

Com o tempo, à medida que as entities são criadas e eliminadas, é perfeitamente válido reutilizar IDs que tenham sido anteriormente libertados — na verdade, isso pode ser necessário por motivos de desempenho (veja abaixo).

Sincronização

Tanto o World Explorer como a scene têm de gerir um conjunto de entities e components partilhados, em que as atualizações vêm de ambos os lados de forma assíncrona. Sem medidas adicionais, as suas versões do estado do mundo podem (e vão) acabar por ser diferentes.

Para evitar isso, é utilizado um CRDT (tipo de dados replicado sem conflitos) para chegar a um acordo entre ambas as partes através da troca de mensagens com atualizações. Isto oferece várias vantagens:

  • Ambas as partes podem atualizar o estado de forma independente e concorrente.

  • Os conflitos são geridos por uma estratégia de resolução partilhada aplicada localmente por ambas as partes.

  • Ambas as partes têm garantia de convergir para um estado partilhado e idêntico.

  • Além de um pequeno acréscimo no tamanho das mensagens, não são necessárias coordenação adicional nem rondas extra de mensagens.

Esta página explica a utilização do CRDT para sincronizar o estado entre uma scene em execução local e o game engine, mas o verdadeiro potencial deste mecanismo reside em cenários multiplayer. Usando este protocolo, os players podem coordenar o seu estado de jogo e partilhar a mesma experiência.

Para que isto funcione, todas as mensagens têm de ser comutativas e idempotentes. Mensagens fora de ordem podem ser aplicadas mesmo que uma atualização intermédia alegada ainda não tenha sido recebida, e mensagens iguais podem ser reprocessadas sem quebrar a consistência.

Vamos revisitar o diagrama arquitetónico acima, agora com foco no CRDT:

A implementação do mecanismo de sincronização CRDT consiste em três partes:

  1. Uma representação interna do estado de todas as entities e components.

  2. Uma estratégia de resolução de conflitos para escolher entre estados concorrentes.

  3. Um protocolo de mensagens para comunicar e processar atualizações ao estado.

Estado do CRDT

O CRDT contém o estado de todos os components para todas as entities associadas, e pode ser armazenado num mapeamento de components para estados com timestamp de cada entity. Em pseudocódigo:

Os timestamps não são, na verdade, timestamps ao estilo Unix. São um tipo de contador incremental para sequenciar atualizações, sem relação com a hora do relógio. Mais sobre isto abaixo.

Como a camada CRDT não conhece (nem precisa de conhecer) todos os components disponíveis, o estado inicial é um mapa vazio. As entradas são criadas dinamicamente quando as operações envolvem um ComponentId ou EntityId que ainda não estava mapeado. Quando uma entity é removida, o estado associado é eliminado de todos os components.

Vamos ilustrar isto com mais pseudocódigo.

O pseudocódigo acima (se fosse traduzido para código real) está obviamente incompleto e abaixo do ideal. Em particular, falta a definição de shouldCreate e shouldReplace, que encapsulam a estratégia de resolução de conflitos. Têm estes requisitos:

  • shouldCreate deve adicionar novas entities, mas recusar recriar entities eliminadas.

  • shouldReplace deve dar preferência a manter estados com timestamps maiores (com alguns casos-limite).

Para dar suporte ao shouldCreate requisito, quererás manter o registo dos IDs das entities eliminadas, para ignorar quaisquer futuras atualizações de estado para essas entities. Vejamos isto em mais pseudocódigo:

Note-se que, na implementação ingénua acima, o conjunto de entities eliminadas só irá crescer. Em scenes que reciclem entities rapidamente, isto pode levar a uma explosão no uso de memória.

Para evitar isto, o Explorer da Foundation usa um índice geracional para reutilizar IDs num espaço numérico finito, limitando a quantidade de memória necessária para acompanhar tanto as entities ativas como as eliminadas.

Quanto ao shouldReplace requisito, vê resolução de conflitos abaixo.

Timestamps

Como referido acima, quando falamos de timestamps do CRDT não nos estamos a referir a timestamps Unix reais para um momento no tempo. Em vez disso, é utilizado um timestamp de Lamport (um tipo especial de contador incremental partilhado) para acompanhar a sequência de events por ambas as partes.

O valor deste contador é atualizado de acordo com estas regras:

  1. Inicializa o contador local para 0.

  2. Antes de enviar uma mensagem para partes remotas, incrementa o contador local.

  3. Depois de receber uma mensagem de uma parte remota, define o contador local como o máximo entre o seu valor e o valor recebido, mais 1.

Em pseudocódigo:

Resolução de conflitos

Quando o CRDT encontra dois estados concorrentes, precisa de uma estratégia de resolução partilhada que todas as partes apliquem de forma idêntica, garantindo que todos chegam à mesma decisão. O protocolo do Decentraland usa regras muito simples:

  1. Se a entity foi eliminada (e o ID nunca foi reutilizado), ignora o novo estado.

  2. Se não havia estado anterior, mantém o novo estado.

  3. Se o novo estado tiver um timestamp maior, mantém o novo estado.

  4. Se o novo estado tiver um timestamp menor, mantém o estado antigo.

  5. Se ambos os timestamps forem iguais, compara os estados byte a byte e mantém o valor mais baixo.

A maioria dos casos será resolvida pelas regras 1, 2 e 3.

Sincronização inicial

Antes de uma scene começar a executar o seu próprio código, o runtime preenche o CRDT com o estado de todas as basic entities e dos seus components.

Durante esta sincronização inicial, apenas o runtime pode definir o estado partilhado. A scene não tem permissão para fazer modificações até que o processo esteja concluído.

Protocolo de mensagens

As mensagens entre o World Explorer e o runtime da scene são estruturas numa representação binária serializada. Cada mensagem transporta um cabeçalho que indica o tipo e o comprimento, seguido de uma carga útil específica para cada tipo de mensagem.

Há três tipos de mensagem no protocolo CRDT:

Note-se que não existem CreateComponent ou CreateEntity mensagens. As regras do CRDT criam automaticamente entities e components que anteriormente não eram conhecidos, como explicado acima.

PutComponentMessage

Atualiza o estado de um component para uma determinada Entity, criando components e entities desconhecidos no CRDT se necessário. Resolve conflitos de acordo com timestamp.

O estado campo é uma serialização binária definida pelo component. A maioria dos components usa protocol buffers para codificar mensagens conforme especificado nos .proto ficheiros do pacote de protocolo do Decentraland. A exceção notável a esta regra é o Transform component, 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 component presentes ou futuras.

Se a atualização contida nesta mensagem for aplicada ao CRDT, o estado campo é copiado tal como está para a estrutura.

DeleteComponentMessage

Remove o estado de component para um Entity. Resolve conflitos de acordo com timestamp.

DeleteEntityMessage

Elimina Entity (isto é, todo o estado dos components associado), esperando que o identificador nunca seja reutilizado para uma entity diferente.

Note-se que não existe timestamp campo. Como o ciclo de vida de Entity terminou, 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

Envia uma mensagem serializada da scene para o renderer; devolve uma array de mensagens serializadas que o renderer tem para a scene.

crdtGetState

Scenes legacy

Scenes antigas (isto é, as construídas usando a versão 6 do SDK ou anterior) não usam o mecanismo CRDT moderno, chamando em vez disso métodos em EngineApi que agora estão obsoletos.

O suporte a estas scenes não é um requisito para uma aplicação Decentraland em conformidade com o protocolo.

Atualizado