Começamos com a abordagem básica de sistemas-componentes-entidades .
Vamos criar assemblages (termo derivado deste artigo) apenas com informações sobre os tipos de componentes . Isso é feito dinamicamente no tempo de execução, assim como adicionaríamos ou removeríamos componentes de uma entidade, um por um, mas vamos dar um nome mais preciso, pois trata-se apenas de informações de tipo.
Em seguida, construímos entidades especificando assemblage para cada uma delas. Depois de criar a entidade, seu conjunto é imutável, o que significa que não podemos modificá-lo diretamente no local, mas ainda podemos obter a assinatura da entidade existente para uma cópia local (junto com o conteúdo), fazer as devidas alterações nela e criar uma nova entidade. disso.
Agora, o conceito-chave: sempre que uma entidade é criada, ela é atribuída a um objeto chamado bucket de assemblage , o que significa que todas as entidades da mesma assinatura estarão no mesmo contêiner (por exemplo, em std :: vector).
Agora, os sistemas interagem com todos os grupos de interesse e realizam seu trabalho.
Essa abordagem tem algumas vantagens:
- os componentes são armazenados em alguns (precisamente: número de buckets) blocos de memória contíguos - isso melhora a facilidade de memória e é mais fácil despejar todo o estado do jogo
- sistemas processam componentes de maneira linear, o que significa melhor coerência do cache - adeus dicionários e saltos aleatórios de memória
- criar uma nova entidade é tão fácil quanto mapear um conjunto para o balde e empurrar os componentes necessários para seu vetor
- excluir uma entidade é tão fácil quanto uma chamada para std :: move para trocar o último elemento pelo excluído, porque a ordem não importa no momento
Se tivermos muitas entidades com assinaturas completamente diferentes, os benefícios da coerência do cache diminuem, mas acho que isso não aconteceria na maioria dos aplicativos.
Também existe um problema com a invalidação do ponteiro depois que os vetores são realocados - isso pode ser resolvido com a introdução de uma estrutura como:
struct assemblage_bucket {
struct entity_watcher {
assemblage_bucket* owner;
entity_id real_index_in_vector;
};
std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;
//...
};
Portanto, sempre que, por algum motivo em nossa lógica do jogo, desejamos acompanhar uma entidade recém-criada, dentro do bucket, registramos um entity_watcher e, uma vez que a entidade precisa ser std :: move'd durante a remoção, procuramos seus observadores e atualizamos seus real_index_in_vector
para novos valores. Na maioria das vezes, isso impõe apenas uma pesquisa de dicionário para cada exclusão de entidade.
Existem mais desvantagens nessa abordagem?
Por que a solução não foi mencionada em nenhum lugar, apesar de ser bastante óbvia?
EDIT : Estou editando a pergunta para "responder as respostas", pois os comentários são insuficientes.
você perde a natureza dinâmica dos componentes conectáveis, que foram criados especificamente para evitar a construção de classe estática.
Eu não. Talvez eu não tenha explicado com clareza suficiente:
auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket
É tão simples quanto pegar a assinatura da entidade existente, modificá-la e fazer o upload novamente como uma nova entidade. Natureza dinâmica e conectável ? Claro. Aqui, gostaria de enfatizar que existe apenas uma classe "assemblage" e uma "bucket". As caçambas são orientadas por dados e criadas em tempo de execução em uma quantidade ideal.
você precisaria passar por todos os buckets que possam conter um destino válido. Sem uma estrutura de dados externa, a detecção de colisões pode ser igualmente difícil.
Bem, é por isso que temos as estruturas de dados externas acima mencionadas . A solução alternativa é tão simples quanto introduzir um iterador na classe System que detecta quando saltar para o próximo bucket. O salto seria puramente transparente para a lógica.
fonte
Respostas:
Você projetou essencialmente um sistema de objetos estáticos com um alocador de pool e com classes dinâmicas.
Eu escrevi um sistema de objetos que funciona quase de forma idêntica ao seu sistema de "assemblages" nos meus tempos de escola, embora eu sempre costumo chamar "assemblages" de "projetos" ou "arquétipos" em meus próprios projetos. A arquitetura era mais problemática do que os sistemas de objetos ingênuos e não apresentava vantagens mensuráveis de desempenho em relação a alguns dos projetos mais flexíveis com os quais eu a comparava. A capacidade de modificar dinamicamente um objeto sem a necessidade de reificá-lo ou realocá-lo é extremamente importante quando você estiver trabalhando em um editor de jogos. Os designers desejam arrastar e soltar componentes nas definições de seu objeto. O código de tempo de execução pode até precisar modificar componentes eficientemente em alguns designs, embora eu pessoalmente não goste. Dependendo de como você vincula as referências de objeto no seu editor,
Você terá uma coerência pior do cache do que pensa na maioria dos casos não triviais. Seu sistema de IA, por exemplo, não se preocupa com os
Render
componentes, mas acaba ficando preso repetindo-os como parte de cada entidade. Os objetos que estão sendo iterados são maiores e as solicitações de cacheline acabam atraindo dados desnecessários e menos objetos inteiros são retornados a cada solicitação). Ainda será melhor que o método ingênuo, e a composição de objetos do método ingênuo é usada mesmo em grandes mecanismos AAA, então você provavelmente não precisa de melhor, mas pelo menos não pense que não pode melhorar ainda mais.Sua abordagem faz mais sentido para algunscomponentes, mas não todos. Eu não gosto muito do ECS, porque defende sempre colocar cada componente em um contêiner separado, o que faz sentido para física ou gráficos ou outros enfeites, mas não faz sentido algum se você permitir vários componentes de script ou IA de composição. Se você permitir que o sistema de componentes seja usado para mais do que apenas objetos internos, mas também como uma maneira de designers e programadores de jogo comporem o comportamento dos objetos, pode fazer sentido agrupar todos os componentes de IA (que geralmente interagem) ou todos os scripts componentes (desde que você deseja atualizá-los todos em um lote). Se você deseja o sistema com melhor desempenho, precisará de uma combinação de esquemas de alocação e armazenamento de componentes e dedicar tempo para descobrir de maneira conclusiva qual é o melhor para cada tipo específico de componente.
fonte
O que você fez foi reprojetar objetos C ++. A razão pela qual isso parece óbvio é que, se você substituir a palavra "entidade" por "classe" e "componente" por "membro", esse é um projeto OOP padrão usando mixins.
1) você perde a natureza dinâmica dos componentes conectáveis, criados especificamente para evitar a construção de classe estática.
2) a coerência da memória é mais importante dentro de um tipo de dados, não dentro de um objeto que unifica vários tipos de dados em um único local. Esse é um dos motivos pelos quais os componentes + foram criados, para evitar a fragmentação da memória da classe + do objeto.
3) esse design também reverte para o estilo de classe C ++ porque você está pensando na entidade como um objeto coerente quando, em um design de componente + sistema, a entidade é apenas uma tag / ID para tornar o funcionamento interno compreensível para os seres humanos.
4) é tão fácil para um componente serializar a si mesmo quanto um objeto complexo serializar vários componentes dentro de si, se não for realmente mais fácil acompanhar como programador.
5) o próximo passo lógico nesse caminho é remover Systems e colocar esse código diretamente na entidade, onde ele possui todos os dados necessários para trabalhar. Todos nós podemos ver o que isso significa =)
fonte
Manter entidades unidas não é tão importante quanto você imagina, e é por isso que é difícil pensar em um motivo válido que não seja "porque é uma unidade". Mas como você está realmente fazendo isso por coerência de cache, em oposição à coerência lógica, pode fazer sentido.
Uma dificuldade que você pode ter é a interação entre componentes em diferentes buckets. Não é muito fácil encontrar algo em que sua IA possa disparar, por exemplo, você precisaria passar por todos os baldes que possam conter um alvo válido. Sem uma estrutura de dados externa, a detecção de colisões pode ser igualmente difícil.
Para continuar organizando entidades em conjunto por coerência lógica, a única razão pela qual eu poderia ter para manter entidades unidas é para fins de identificação em minhas missões. Eu preciso saber se você acabou de criar o tipo de entidade A ou tipo B, e eu posso contornar isso ... adivinhou: adicionando um novo componente que identifica o conjunto que une essa entidade. Mesmo assim, não estou reunindo todos os componentes para uma grande tarefa, só preciso saber o que é. Portanto, não acho que essa parte seja terrivelmente útil.
fonte