Quando / onde atualizar componentes

10

Em vez dos meus motores de jogo pesados ​​de herança usual, estou brincando com uma abordagem mais baseada em componentes. No entanto, tenho dificuldade em justificar onde deixar os componentes fazerem o que precisam.

Digamos que eu tenha uma entidade simples que tenha uma lista de componentes. É claro que a entidade não sabe quais são esses componentes. Pode haver um componente presente que dê à entidade uma posição na tela, outro pode estar lá para desenhar a entidade na tela.

Para que esses componentes funcionem, eles precisam atualizar todos os quadros, a maneira mais fácil de fazer isso é percorrer a árvore da cena e, em seguida, para cada entidade, atualizar cada componente. Mas alguns componentes podem precisar de um pouco mais de gerenciamento. Por exemplo, um componente que torna uma entidade colidível deve ser gerenciado por algo que possa supervisionar todos os componentes colidíveis. Um componente que torna uma entidade desenhável precisa de alguém para supervisionar todos os outros componentes desenháveis ​​para descobrir a ordem de desenho, etc.

Portanto, minha pergunta é: onde atualizo os componentes, qual é a maneira mais clara de levá-los aos gerentes?

Eu pensei em usar um objeto gerenciador de singleton para cada um dos tipos de componentes, mas que possui as desvantagens usuais de usar um singleton, uma maneira de aliviar isso é um pouco usando injeção de dependência, mas isso soa como um exagero para esse problema. Eu também poderia andar sobre a árvore da cena e, em seguida, reunir os diferentes componentes em listas usando algum tipo de padrão de observador, mas isso parece um pouco desperdício para todos os quadros.

Roy T.
fonte
1
Você está usando sistemas de alguma maneira?
Asakeron
Os sistemas de componentes são a maneira usual de fazer isso. Pessoalmente, chamo de atualização em todas as entidades, que chama atualização em todos os componentes, e tenho alguns casos "especiais" (como o gerenciador espacial para detecção de colisão, que é estático).
ashes999
Sistemas de componentes? Eu nunca ouvi falar disso antes. Vou começar a pesquisar no Google, mas gostaria de receber todos os links recomendados.
Roy T.
1
Sistemas de entidades são o futuro do desenvolvimento MMOG é um grande recurso. E, para ser sincero, estou sempre confuso com esses nomes de arquitetura. A diferença com a abordagem sugerida é que os componentes retêm apenas dados e os sistemas os processam. Esta resposta também é muito relevante.
Asakeron
1
Escrevi um post de blog meio sinuoso sobre esse assunto aqui: gamedevrubberduck.wordpress.com/2012/12/26/…
AlexFoxGill

Respostas:

15

Eu sugeriria começar lendo as três grandes mentiras de Mike Acton, porque você viola duas delas. Estou falando sério, isso mudará a maneira como você cria seu código: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

Então, o que você viola?

Mentira # 3 - Código é mais importante que dados

Você fala sobre injeção de dependência, que pode ser útil em algumas (e apenas algumas) instâncias, mas deve sempre tocar um grande sino de alarme se você a usar, especialmente no desenvolvimento de jogos! Por quê? Porque é uma abstração frequentemente desnecessária. E abstrações nos lugares errados são horríveis. Então você tem um jogo. O jogo tem gerentes para diferentes componentes. Os componentes estão todos definidos. Portanto, faça uma aula em algum lugar do código do loop do jogo principal que "tenha" os gerentes. Gostar:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

Dê a ele algumas funções getter para obter cada classe de gerente (getBulletManager ()). Talvez essa classe em si seja um Singleton ou seja acessível a partir de um (você provavelmente já tem um Singleton central do Game em algum lugar). Não há nada de errado com dados e comportamento codificados bem definidos.

Não crie um ManagerManager que permita registrar gerentes usando uma chave, que pode ser recuperada usando essa chave por outras classes que desejam usar o gerenciador. É um ótimo sistema e muito flexível, mas onde se fala de um jogo aqui. Você sabe exatamente quais sistemas estão no jogo. Por que fingir que não? Porque este é um sistema para pessoas que pensam que o código é mais importante que os dados. Eles dirão "O código é flexível, os dados apenas o preenchem". Mas código é apenas dados. O sistema que descrevi é muito mais fácil, mais confiável, mais fácil de manter e muito mais flexível (por exemplo, se o comportamento de um gerente for diferente de outros, você precisará alterar algumas linhas em vez de refazer o sistema inteiro)

Mentira # 2 - O código deve ser projetado em torno de um modelo do mundo

Então você tem uma entidade no mundo do jogo. A entidade possui vários componentes que definem seu comportamento. Portanto, você cria uma classe Entity com uma lista de objetos Component e uma função Update () que chama a função Update () de cada componente. Direita?

Não. :) Isso é projetado em torno de um modelo do mundo: você tem bullet no seu jogo e adiciona uma classe Bullet. Então você atualiza cada marcador e passa para o próximo. Isso absolutamente prejudicará seu desempenho e fornecerá uma horrível base de código complicada, com código duplicado em todos os lugares e nenhuma estrutura lógica de código semelhante. (Confira minha resposta aqui para obter uma explicação mais detalhada sobre por que o design tradicional de OO é péssimo ou consulte Design orientado a dados)

Vamos dar uma olhada na situação sem nosso viés de OO. Queremos o seguinte, nem mais nem menos (observe que não há requisito para criar uma classe para entidade ou objeto):

  • Você tem um monte de entidades
  • As entidades são compostas por vários componentes que definem o comportamento da entidade
  • Você deseja atualizar cada componente do jogo em cada quadro, de preferência de forma controlada
  • Além de identificar os componentes como pertencentes, não há nada que a própria entidade precise fazer. É um link / ID para alguns componentes.

E vamos olhar para a situação. Seu sistema de componentes atualizará o comportamento de todos os objetos do jogo, todos os quadros. Este é definitivamente um sistema crítico do seu motor. O desempenho é importante aqui!

Se você conhece a arquitetura do computador ou o Data Oriented Design, sabe como obter o melhor desempenho: memória compactada e agrupando a execução do código. Se você executar trechos dos códigos A, B e C como este: ABCABCABC, não obterá o mesmo desempenho de quando é executado da seguinte maneira: AAABBBCCC. Isso não ocorre apenas porque o cache de instruções e dados será usado com mais eficiência, mas também porque se você executar todos os "A" s um após o outro, haverá muito espaço para otimização: remover código duplicado, pré-calcular dados usados ​​por todos os "A", etc.

Portanto, se queremos atualizar todos os componentes, não vamos torná-los classes / objetos com uma função de atualização. Não vamos chamar essa função de atualização para cada componente em cada entidade. Essa é a solução "ABCABCABC". Vamos agrupar todas as atualizações de componentes idênticas. Em seguida, podemos atualizar todos os componentes A, seguidos por B, etc. O que precisamos fazer?

Primeiro, precisamos de gerenciadores de componentes. Para cada tipo de componente no jogo, precisamos de uma classe de gerente. Possui uma função de atualização que atualiza todos os componentes desse tipo. Ele possui uma função de criação que adicionará um novo componente desse tipo e uma função de remoção que destruirá o componente especificado. Pode haver outras funções auxiliares para obter e definir dados específicos para esse componente (por exemplo: defina o modelo 3D para o Componente do modelo). Observe que, de certa forma, o gerente é uma caixa preta para o mundo exterior. Não sabemos como os dados de cada componente são armazenados. Não sabemos como cada componente é atualizado. Não nos importamos, desde que os componentes se comportem como deveriam.

Em seguida, precisamos de uma entidade. Você pode fazer disso uma aula, mas isso não é necessário. Uma entidade pode ser nada mais que um ID inteiro exclusivo ou uma cadeia de caracteres com hash (também um número inteiro). Ao criar um componente para a Entidade, você passa o ID como argumento para o Gerente. Quando você deseja remover o componente, passa o ID novamente. Pode haver algumas vantagens em adicionar um pouco mais de dados à Entidade, em vez de apenas torná-la um ID, mas essas serão apenas funções auxiliares, porque, como listei nos requisitos, todo o comportamento da entidade é definido pelos próprios componentes. É o seu motor, então faça o que faz sentido para você.

O que precisamos é de um Entity Manager. Essa classe gerará IDs exclusivos se você usar a solução somente de ID ou poderá ser usada para criar / gerenciar objetos de Entidade. Também pode manter uma lista de todas as entidades do jogo, se você precisar. O Entity Manager pode ser a classe central do seu sistema de componentes, armazenando as referências a todos os ComponentManagers no seu jogo e chamando suas funções de atualização na ordem correta. Dessa forma, tudo o que o loop do jogo precisa fazer é chamar EntityManager.update () e todo o sistema é bem separado do restante do seu mecanismo.

Essa é a visão geral, vamos dar uma olhada em como os gerentes de componentes funcionam. Aqui está o que você precisa:

  • Crie dados do componente quando create (entityID) for chamado
  • Excluir dados do componente quando remover (entityID) for chamado
  • Atualizar todos os dados do componente (aplicável) quando update () é chamado (ou seja, nem todos os componentes precisam atualizar cada quadro)

O último é onde você define o comportamento / lógica dos componentes e depende completamente do tipo de componente que está escrevendo. O AnimationComponent atualizará os dados da animação com base no quadro em que está. O DragableComponent atualizará apenas um componente que está sendo arrastado pelo mouse. O PhysicsComponent atualizará os dados no sistema de física. Ainda assim, como você atualiza todos os componentes do mesmo tipo de uma só vez, é possível fazer algumas otimizações que não são possíveis quando cada componente é um objeto separado com uma função de atualização que pode ser chamada a qualquer momento.

Observe que eu nunca pedi a criação de uma classe XxxComponent para armazenar dados de componentes. Isso é contigo. Você gosta de design orientado a dados? Em seguida, estruture os dados em matrizes separadas para cada variável. Você gosta de Design Orientado a Objetos? (Eu não recomendaria, ele ainda prejudicará seu desempenho em muitos lugares). Em seguida, crie um objeto XxxComponent que reterá os dados de cada componente.

A grande coisa sobre os gerentes é o encapsulamento. Agora, o encapsulamento é uma das filosofias mais terrivelmente mal utilizadas no mundo da programação. É assim que deve ser usado. Somente o gerente sabe quais dados do componente são armazenados onde, como a lógica de um componente funciona. Existem algumas funções para obter / definir dados, mas é isso. Você pode reescrever o gerente inteiro e suas classes subjacentes e, se não alterar a interface pública, ninguém percebe. Mudou o mecanismo de física? Apenas reescreva PhysicsComponentManager e pronto.

Depois, há uma coisa final: comunicação e compartilhamento de dados entre componentes. Agora isso é complicado e não existe uma solução única para todos. Você pode criar funções get / set nos gerenciadores para permitir, por exemplo, que o componente de colisão obtenha a posição do componente de posição (por exemplo, PositionManager.getPosition (entityID)). Você poderia usar um sistema de eventos. Você pode armazenar alguns dados compartilhados na entidade (a solução mais feia na minha opinião). Você pode usar (geralmente usado) um sistema de mensagens. Ou use uma combinação de vários sistemas! Não tenho tempo nem experiência para entrar em cada um desses sistemas, mas a pesquisa no google e no stackoverflow são seus amigos.

Mart
fonte
Acho esta resposta muito interessante. Apenas uma pergunta (espero que você ou alguém possa me responder). Como você consegue eliminar a entidade em um sistema baseado em componentes do DOD? Mesmo Artemis usa Entity como uma classe, não tenho certeza de que seja muito esquivo.
Wolfrevo Kcats
1
O que você quer dizer com eliminá-lo? Você quer dizer um sistema de entidades sem uma classe de entidade? A razão pela qual Artemis tem uma Entidade é porque, na Artemis, a classe Entity gerencia seus próprios componentes. No sistema que propus, as classes ComponentManager gerenciam os componentes. Portanto, em vez de precisar de uma classe Entity, você pode apenas ter um ID inteiro exclusivo. Então, digamos que você tenha a entidade 254, que possui um componente de posição. Quando você deseja alterar a posição, pode chamar PositionCompMgr.setPosition (ID int, Vector3 newPos), com 254 como o parâmetro id.
Mart
Mas como você gerencia identificações? E se você deseja remover um componente de uma entidade para atribuí-lo posteriormente a outra? E se você deseja remover uma entidade e adicionar uma nova? E se você quiser que um componente seja compartilhado entre duas ou mais entidades? Estou realmente interessado nisso.
Wolfrevo Kcats 24/03
1
O EntityManager pode ser usado para fornecer novos IDs. Também poderia ser usado para criar entidades completas com base em modelos predefinidos (por exemplo, criar "EnemyNinja", que gera um novo ID e cria todos os componentes que compõem um ninja inimigo, como renderização, colisão, IA, talvez algum componente para combate corpo a corpo etc). Também pode ter uma função removeEntity que chama automaticamente todas as funções de remoção do ComponentManager. O ComponentManager pode verificar se possui dados de componente para a Entidade especificada e, se houver, excluir esses dados.
Mart
1
Mover um componente de uma entidade para outra? Basta adicionar uma função swapComponentOwner (int oldEntity, int newEntity) a cada ComponentManager. Os dados estão todos lá no ComponentManager, tudo o que você precisa é de uma função para alterar a qual proprietário pertence. Cada ComponentManager terá algo como um índice ou mapa para armazenar quais dados pertencem a qual ID de entidade. Basta alterar o ID da entidade do antigo para o novo ID. Não tenho certeza se o compartilhamento de componentes é fácil no sistema que pensei, mas quão difícil pode ser? Em vez de um link de ID de entidade <-> Dados do componente na tabela de índices, existem vários.
Mart
3

Para que esses componentes funcionem, eles precisam atualizar todos os quadros, a maneira mais fácil de fazer isso é percorrer a árvore da cena e, em seguida, para cada entidade, atualizar cada componente.

Essa é a abordagem ingênua típica para atualizações de componentes (e não há nada necessariamente errado em ser ingênua, se funcionar para você). Um dos grandes problemas em que você realmente abordou - você está operando através da interface do componente (por exemplo IComponent) para não saber nada sobre o que acabou de atualizar. Você provavelmente também não sabe nada sobre a ordem dos componentes dentro da entidade; portanto,

  1. é provável que você atualize componentes de tipos diferentes com frequência (localidade de referência de código ruim, essencialmente)
  2. esse sistema não se presta bem a atualizações simultâneas porque você não está em condições de identificar dependências de dados e, assim, dividir as atualizações em grupos locais de objetos não relacionados.

Eu pensei em usar um objeto gerenciador de singleton para cada um dos tipos de componentes, mas que possui as desvantagens usuais de usar um singleton, uma maneira de aliviar isso é um pouco usando injeção de dependência, mas isso soa como um exagero para esse problema.

Um singleton não é realmente necessário aqui e, portanto, você deve evitá-lo, pois possui as desvantagens mencionadas. A injeção de dependência não é um exagero - o cerne do conceito é que você passa coisas que um objeto precisa para ele, idealmente no construtor. Você não precisa de uma estrutura DI pesada (como o Ninject ) para isso - basta passar um parâmetro extra para um construtor em algum lugar.

Um renderizador é um sistema fundamental e provavelmente suporta a criação e o gerenciamento da vida útil de vários objetos renderizáveis ​​que correspondem a coisas visuais no seu jogo (provavelmente sprites ou modelos). Da mesma forma, um mecanismo de física provavelmente tem controle vitalício sobre coisas que representam as entidades que podem se mover na simulação de física (corpos rígidos). Cada um desses sistemas relevantes deve possuir, de alguma forma, esses objetos e ser responsável por atualizá-los.

Os componentes que você usa no sistema de composição de entidades do jogo devem ser apenas invólucros em torno das instâncias desses sistemas de nível inferior - o componente de posição pode envolver um corpo rígido, o componente visual envolve um sprite ou modelo renderizável etc.

Em seguida, o próprio sistema que possui os objetos de nível inferior é responsável por atualizá-los e pode fazê-lo em massa e de uma maneira que permita multithread dessa atualização, se apropriado. Seu loop principal do jogo controla a ordem bruta em que esses sistemas são atualizados (primeiro a física, depois o renderizador ou o que for). Se você tiver um subsistema que não tenha vida útil ou controle de atualização sobre as instâncias distribuídas, poderá criar um wrapper simples para lidar com a atualização de todos os componentes relevantes para esse sistema em massa e decidir onde colocá-lo. atualização relativa ao restante das atualizações do sistema (acho que isso acontece com os componentes "script").

Essa abordagem é ocasionalmente conhecida como abordagem de componente externo , se você estiver procurando mais detalhes.


fonte