Sobre DOTS: Sistema de Componentes de Entidade

Esta é uma das várias postagens sobre nosso novo Data-Oriented Tech Stack (DOTS), compartilhando alguns insights sobre como e por que chegamos onde estamos hoje e para onde queremos ir.
No meu último post, falei sobre HPC# e Burst como tecnologias fundamentais de baixo nível para o Unity daqui para frente. Gosto de me referir a esse nível da nossa pilha como “mecanismo de jogo”. Qualquer um pode usar essa pilha para escrever um mecanismo de jogo. Pudermos. Vamos. Você também pode. Não gosta do nosso? Escreva o seu próprio ou modifique o nosso como preferir.
A próxima camada que estamos construindo é um novo sistema de componentes. O Unity sempre foi centrado nos conceitos de componentes. Adicione um componente Rigidbody a um GameObject e ele começará a cair. Adicione um componente Light a um GameObject e ele começará a emitir luz. Adicione um componente AudioEmitter e o GameObject começará a produzir som.
É um conceito muito natural para programadores e não programadores, e é fácil criar interfaces de usuário intuitivas para ele. Na verdade, estou bastante surpreso com o quão bem esse conceito envelheceu. Tão bem que queremos mantê-lo.
O que não envelheceu bem foi a forma como implementamos nosso sistema de componentes. Foi escrito com uma mentalidade orientada a objetos. Componentes e GameObjects são objetos “c++ pesados”. Criá-los/destruí-los requer um bloqueio de mutex para modificar a lista global de id->objectpointers. Todos os GameObjects têm um nome. Cada um recebe um objeto wrapper C# que aponta para o C++. Esse objeto C# pode estar em qualquer lugar na memória. O objeto C++ também pode estar em qualquer lugar na memória. Erros de cache são muitos. Tentamos amenizar os sintomas da melhor forma possível, mas não há muito que você possa fazer.
Com uma mentalidade orientada a dados, podemos fazer muito melhor. Podemos manter as mesmas propriedades interessantes do ponto de vista do usuário (adicione um componente Rigidbody e a coisa cairá), mas também obter desempenho e paralelismo incríveis com nosso novo sistema de componentes.
Este novo sistema de componentes é o nosso Sistema de Componentes de Entidade (ECS). Em termos gerais, o que você faz com um GameObject hoje, você faz com uma Entidade no novo sistema. Os componentes ainda são chamados de componentes. Então o que é diferente? O layout dos dados.
Vejamos alguns padrões comuns de acesso a dados
Um componente típico que você escreveria no Unity da maneira tradicional poderia ser assim:
classe Órbita: MonoComportamento
{
público Transformar _objectToOrbitAround;
Atualização vazia()
{
//por favor ignore essa matemática, ela está toda errada, esse não é o ponto aqui :)
var currentPos = GetComponent<Transform>().position;
var targetPos = _objectToOrbitAround.position;
GetComponent<RigidBody>().velocity += De alguma forma, orientar para (currentPos, targetPos)
}
}
Esse padrão se repete repetidamente. Um componente precisa encontrar um ou mais outros componentes no mesmo GameObject e ler/escrever alguns valores nele.
Há muitas coisas erradas nisso:
- O método Update() é chamado para um único componente de órbita. A próxima chamada Update() pode ser para um componente completamente diferente, provavelmente fazendo com que esse código seja removido do cache na próxima vez que for necessário executar esse quadro para outro componente do Orbit.
- Update() precisa usar GetComponent() para encontrar seu Rigidbody. (Ele poderia ser armazenado em cache, mas você precisa ter cuidado para que o componente Rigidbody não seja destruído).
- Os outros componentes nos quais estamos operando estão em lugares completamente diferentes na memória.
O layout de dados usado pelo ECS reconhece que esse é um padrão muito comum e otimiza o layout da memória para tornar operações como essa rápidas.
O ECS agrupa todas as entidades que têm exatamente o mesmo conjunto de componentes na memória. Ele chama esse conjunto de arquétipo. Um exemplo de um arquétipo é: “Posição e Velocidade e Corpo Rígido e Colisor”. O ECS aloca memória em blocos de 16k. Cada bloco conterá apenas os dados do componente para entidades de um único arquétipo.
Em vez de fazer com que o método Update do usuário procure outros componentes para operar em tempo de execução, por instância do Orbit, no ECS Land você tem que declarar estaticamente "Quero executar algumas operações em todas as entidades que têm um componente Velocity, um Rigidbody e um Orbit. Para encontrar todas essas entidades, simplesmente encontramos todos os arquétipos que correspondem a uma “consulta de pesquisa de componente” específica. Cada arquétipo tem uma lista de Chunks onde as entidades daquele arquétipo são armazenadas. Fazemos um loop em todos esses blocos e, dentro de cada um deles, fazemos um loop linear de memória compactada para ler e gravar os dados dos componentes. Esse loop linear que executa o mesmo código em cada entidade também cria uma provável oportunidade de vetorização para o Burst.
Em muitos casos, esse processo pode ser dividido trivialmente em várias tarefas, fazendo com que o código que opera o componente ECS seja executado com quase 100% de utilização do núcleo.
O ECS faz todo esse trabalho para você, você só precisa fornecer o código que deseja executar em cada entidade. (Você pode fazer a iteração do bloco manualmente se quiser.)
Quando você adiciona/remove um componente de uma Entidade, o arquétipo dela muda. Nós o movemos de seu pedaço atual para um pedaço do novo arquétipo e trocamos de volta a última entidade do pedaço anterior para “preencher o buraco”.
No ECS, você também declara estaticamente o que pretende fazer com os dados do componente. Somente leitura ou Leitura/Gravação. Ao prometer (a promessa é verificada) ler somente o componente Posição, o ECS pode obter um agendamento mais eficiente de seus trabalhos. Outros trabalhos que também desejam ler o componente Posição não precisarão esperar.
Esse layout de dados também nos permite lidar com uma frustração antiga que temos, que são os tempos de carregamento e o desempenho da serialização. Carregar/transmitir dados ECS para uma cena grande não é muito mais do que apenas carregar bytes brutos do disco e usá-los como estão.
É por isso que a demonstração do Megacity carrega em poucos segundos em um telefone.
Embora as entidades possam fazer o que os objetos de jogo fazem hoje, elas podem fazer mais porque são muito leves. Na verdade, o que realmente é uma Entidade? Em um rascunho anterior deste post, escrevi “nós armazenamos entidades em blocos” e depois alterei para “nós armazenamos dados de componentes para entidades em blocos”. É uma distinção importante a ser feita: perceber que uma Entidade é apenas um inteiro de 32 bits. Não há nada para armazenar ou alocar para ele, além dos dados de seus componentes. Como são tão baratos, você pode usá-los para cenários em que os objetos do jogo não são adequados. Como usar uma entidade para cada partícula individual em um sistema de partículas.
A próxima camada que precisamos construir é muito grande. É a camada do “mecanismo de jogo” composta de recursos como “renderizador”, “física”, “rede”, “entrada”, “animação”, etc. É mais ou menos onde estamos hoje. Começamos a trabalhar nessas peças, mas elas não ficarão prontas da noite para o dia.
Isso pode parecer uma chatice. De certa forma sim, mas de outra não. Como o ECS e tudo criado sobre ele são escritos em C#, ele pode ser executado dentro do Unity tradicional. Como ele é executado dentro do Unity, você pode escrever componentes ECS que usam funcionalidades pré-ECS. Não existe um sistema de desenho de malha ECS puro no momento. No entanto, você pode escrever um ECS MeshRenderSystem que use a API Graphics.DrawMeshIndirect pré-ECS como uma implementação, enquanto aguarda o lançamento de uma versão pura do ECS. Essa é exatamente a técnica que nossa demonstração do Megacity usa. Carregamento/Streaming/Selecção/LODding/Animação são feitos com sistemas ECS puros, mas o desenho final não.
Assim você pode misturar e combinar. O melhor de tudo é que você já pode aproveitar os benefícios do Burst codegen e do desempenho do ECS para o código do seu jogo, em vez de ter que esperar que enviemos versões ECS puras de todos os subsistemas. O que não é bom nisso é que nessa fase de transição, você pode ver e sentir esse atrito de que está "usando dois mundos diferentes que estão colados".
Enviaremos todo o código-fonte para nossos subsistemas ECS HPC# em pacotes. Você pode inspecionar, depurar e modificar cada subsistema, além de ter um controle mais preciso sobre quando deseja atualizar cada subsistema. Você poderia, por exemplo, atualizar o pacote do subsistema de Física sem atualizar mais nada.
Os objetos do jogo não vão a lugar nenhum. As pessoas têm lançado com sucesso jogos incríveis nele há mais de uma década. Essa fundação não vai a lugar nenhum.
O que mudará é que, com o tempo, você verá nossa energia para fazer melhorias se inclinar de focar exclusivamente no mundo dos objetos do jogo para o mundo do ECS.
Um ponto comum e muito válido que as pessoas levantam ao analisar o ECS é que há muita digitação. Muito código clichê que fica entre você e o que você está tentando alcançar.
Há muitas melhorias no horizonte que visam eliminar a necessidade de termos padronizados e tornar mais simples expressar sua intenção. Ainda não implementamos muitos deles, pois estamos nos concentrando no desempenho fundamental, mas acreditamos que não há nenhuma boa razão para que o código do jogo ECS tenha muito código clichê ou seja particularmente mais trabalhoso de escrever do que escrever um MonoBehaviour.
O Projeto Tiny já implementou algumas dessas melhorias (como uma API de iteração baseada em lambda). Falando nisso...
O Projeto Tiny será lançado sobre o mesmo C# ECS sobre o qual este post do blog está falando. O Projeto Tiny será um grande marco do ECS para nós de várias maneiras:
- Ele poderá ser executado em um ambiente completo somente ECS. Um novo jogador sem bagagem do passado.
- Isso significa que ele também é ECS puro e precisa ser fornecido com todos os subsistemas ECS que um jogo real (pequeno) precisa.
- Adotaremos o suporte do Editor do Project Tiny para edição de entidades em todos os cenários do ECS, não apenas no tiny.
Temos vagas de emprego para todas as diferentes áreas do DOTS, especialmente em Burbank e Copenhague. Confira careers.unity.com.
Além disso, junte-se a nós no fórum Unity Entity Component System e C# Job System para dar feedback e obter informações sobre recursos experimentais e de pré-visualização.
