Conselhos sobre vinculação entre sistema de componentes de entidade em C ++

10

Depois de ler algumas documentações sobre o sistema de entidade-componente, decidi implementar o meu. Até agora, eu tenho uma classe World que contém as entidades e o gerente do sistema (sistemas), a classe Entity que contém os componentes como um std :: map e alguns sistemas. Estou mantendo entidades como um vetor std :: no mundo. Não há problema até agora. O que me confunde é a iteração de entidades, não posso ter uma mente clara sobre isso, então ainda não consigo implementar essa parte. Todo sistema deve conter uma lista local de entidades nas quais está interessado? Ou devo apenas percorrer as entidades da classe World e criar um loop aninhado para percorrer os sistemas e verificar se a entidade possui os componentes nos quais o sistema está interessado? Quero dizer :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

mas acho que um sistema de máscara de bits bloqueará a flexibilidade no caso de incorporar uma linguagem de script. Ou ter listas locais para cada sistema aumentará o uso de memória para as classes. Estou terrivelmente confuso.

deniz
fonte
Por que você espera que a abordagem de máscara de bit prejudique as ligações de script? Como um aparte, use referências (const, se possível) nos loops for-each para evitar a cópia de entidades e sistemas.
Benjamin Kloster
usando uma máscara de bits, por exemplo, um int, conterá apenas 32 componentes diferentes. Não estou sugerindo que haverá mais de 32 componentes, mas e se eu tiver? terei que criar outro int ou 64bit int, não será dinâmico.
Deniz
Você pode usar std :: bitset ou std :: vector <bool>, dependendo se deseja ou não que seja dinâmico em tempo de execução.
Benjamin Kloster

Respostas:

7

Ter listas locais para cada sistema aumentará o uso de memória para as classes.

É uma troca tradicional de espaço-tempo .

Embora a iteração por todas as entidades e a verificação de suas assinaturas sejam diretamente codificadas, pode se tornar ineficiente à medida que o número de sistemas cresce - imagine um sistema especializado (que seja uma entrada) que procure sua provavelmente única entidade de interesse entre milhares de entidades não relacionadas .

Dito isto, essa abordagem ainda pode ser boa o suficiente, dependendo de seus objetivos.

Embora, se você estiver preocupado com a velocidade, é claro que existem outras soluções a serem consideradas.

Todo sistema deve conter uma lista local de entidades nas quais está interessado?

Exatamente. Essa é uma abordagem padrão que deve oferecer desempenho decente e é razoavelmente fácil de implementar. A sobrecarga de memória é insignificante na minha opinião - estamos falando sobre o armazenamento de indicadores.

Agora, como manter essas "listas de interesse" pode não ser tão óbvio. Quanto ao contêiner de dados, std::vector<entity*> targetsa classe interna do sistema é perfeitamente suficiente. Agora o que faço é o seguinte:

  • A entidade está vazia na criação e não pertence a nenhum sistema.
  • Sempre que adiciono um componente a uma entidade:

    • obter sua assinatura de bit atual ,
    • mapeie o tamanho do componente para o pool mundial de tamanho adequado de chunk (pessoalmente eu uso o boost :: pool) e aloque o componente lá
    • obter a nova assinatura de bit da entidade (que é apenas "assinatura de bit atual" mais o novo componente)
    • percorrer todos os sistemas do mundo e se há um sistema cuja assinatura não corresponde à assinatura atual da entidade e não coincidir com a nova assinatura, torna-se óbvio que devemos push_back o ponteiro para a nossa entidade lá.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

A remoção de uma entidade é totalmente análoga, com a única diferença que removemos se um sistema corresponder à nossa assinatura atual (o que significa que a entidade estava lá) e não corresponder à nova assinatura (o que significa que a entidade não deve mais estar lá )

Agora você pode considerar o uso de std :: list porque remover do vetor é O (n), sem mencionar que você precisaria mudar grande parte dos dados toda vez que remover do meio. Na verdade, você não precisa - já que não nos preocupamos com o processamento da ordem nesse nível, podemos simplesmente chamar std :: remove e conviver com o fato de que, a cada exclusão, apenas precisamos realizar O (n) pesquisa por nossa entidade a ser removida.

std :: list daria a você remover O (1), mas por outro lado você tem um pouco de sobrecarga de memória adicional. Lembre-se também de que na maioria das vezes você processará entidades e não as removerá - e isso certamente é feito mais rapidamente usando std :: vector.

Se você é muito crítico em termos de desempenho, pode considerar ainda outro padrão de acesso a dados , mas de qualquer forma mantém algum tipo de "lista de interesse". Lembre-se, porém, de que se você mantiver sua API do sistema de entidades abstrata o suficiente, não deverá ser um problema para melhorar os métodos de processamento de entidades dos sistemas se a taxa de quadros cair por causa deles - portanto, por enquanto, escolha o método mais fácil para você codificar - apenas perfil e aprimore, se necessário.

Patryk Czachurski
fonte
5

Vale a pena considerar uma abordagem em que cada sistema possui os componentes associados a si próprio e as entidades apenas se referem a eles. Basicamente, sua Entityclasse (simplificada) fica assim:

class Entity {
  std::map<ComponentType, Component*> components;
};

Quando você diz que um RigidBodycomponente está anexado a um Entity, solicita-o ao seu Physicssistema. O sistema cria o componente e permite que a entidade mantenha um ponteiro para ele. Seu sistema se parece com:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Agora, isso pode parecer um pouco contra-intuitivo no começo, mas a vantagem está na maneira como os sistemas de entidades componentes atualizam seu estado. Frequentemente, você percorre seus sistemas e solicita que eles atualizem os componentes associados

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

A força de ter todos os componentes pertencentes ao sistema na memória contígua é que, quando o sistema itera sobre cada componente e o atualiza, basicamente é necessário apenas

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

Ele não precisa iterar sobre todas as entidades que potencialmente não possuem um componente que elas precisam atualizar e também tem um potencial para um desempenho muito bom do cache, porque todos os componentes serão armazenados de forma contígua. Essa é uma, se não a maior vantagem desse método. Muitas vezes, você terá centenas e milhares de componentes em um determinado momento, e pode tentar ser o mais eficiente possível.

Nesse ponto, você Worldapenas percorre os sistemas e os chama updatesem precisar iterar as entidades também. É (imho) melhor design, porque as responsabilidades dos sistemas são muito mais claras.

É claro que existem inúmeros projetos assim, então você deve avaliar cuidadosamente as necessidades do seu jogo e escolher o mais apropriado, mas como podemos ver aqui, às vezes são os pequenos detalhes do projeto que podem fazer a diferença.

pwny
fonte
boa resposta, obrigado. mas os componentes não têm funções (como update ()), apenas dados. e o sistema processa esses dados. Então, de acordo com o seu exemplo, devo adicionar uma atualização virtual para a classe de componente e um ponteiro de entidade para cada componente, estou certo?
Deniz
@deniz Tudo depende do seu design. Se seus componentes não tiverem métodos, mas apenas dados, o sistema ainda poderá iterá-los e executar as ações necessárias. Quanto à vinculação de volta às entidades, sim, você pode armazenar um ponteiro para a entidade proprietária no próprio componente ou fazer com que seu sistema mantenha um mapa entre identificadores e entidades de componentes. Normalmente, porém, você deseja que seus componentes sejam o mais independentes possível. Um componente que não sabe nada sobre sua entidade-mãe é ideal. Se você precisar de comunicação nessa direção, prefira eventos e similares.
Pwny
Se você diz que será melhor em termos de eficiência, usarei seu padrão.
Deniz19
@deniz Certifique-se de que você realmente o perfil de seu código cedo e muitas vezes para identificar o que funciona e não para o seu engin especial :)
pwny
okay :) i fará um bocado teste de estresse
deniz
1

Na minha opinião, uma boa arquitetura é criar uma camada de componentes nas entidades e separar o gerenciamento de cada sistema nessa camada de componentes. Por exemplo, o sistema lógico possui alguns componentes lógicos que afetam sua entidade e armazena os atributos comuns que são compartilhados com todos os componentes na entidade.

Depois disso, se você deseja gerenciar os objetos de cada sistema em pontos diferentes ou em uma ordem específica, é melhor criar uma lista de componentes ativos em cada sistema. Todas as listas de ponteiros que você pode criar e gerenciar nos sistemas são menos de um recurso carregado.

superarce
fonte