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.