Alocando Entidades em um Sistema de Entidades

9

Não tenho certeza de como devo alocar / assemelhar-me às minhas entidades dentro do meu sistema de entidades. Eu tenho várias opções, mas a maioria delas parece ter contras associadas. Em todos os casos, as entidades são semelhantes a um ID (número inteiro) e, possivelmente, possuem uma classe de wrapper associada a ele. Essa classe de wrapper possui métodos para adicionar / remover componentes de / para a entidade.

Antes de mencionar as opções, aqui está a estrutura básica do meu sistema de entidades:

  • Entidade
    • Um objeto que descreve um objeto dentro do jogo
  • Componente
    • Usado para armazenar dados para a entidade
  • Sistema
    • Contém entidades com componentes específicos
    • Usado para atualizar entidades com componentes específicos
  • Mundo
    • Contém entidades e sistemas para o sistema de entidades
    • Pode criar / destruir entidades e ter sistemas adicionados / removidos de / para

Aqui estão as minhas opções, nas quais pensei:

Opção 1:

Não armazene as classes de wrapper de entidade e apenas armazene o próximo ID / IDs excluídos. Em outras palavras, as entidades serão retornadas por valor, assim:

Entity entity = world.createEntity();

Isso é muito parecido com o entityx, exceto que vejo algumas falhas nesse design.

Contras

  • Pode haver classes duplicadas de wrapper de entidade (como o copiador precisa ser implementado e os sistemas precisam conter entidades)
  • Se uma Entidade for destruída, as classes de wrapper de entidade duplicadas não terão um valor atualizado

Opção 2:

Armazene as classes de wrapper de entidade em um pool de objetos. ou seja, as entidades serão retornadas por ponteiro / referência, da seguinte forma:

Entity& e = world.createEntity();

Contras

  • Se houver entidades duplicadas, quando uma entidade for destruída, o mesmo objeto de entidade poderá ser reutilizado para alocar outra entidade.

Opção 3:

Use IDs brutos e esqueça as classes de entidade do wrapper. A queda nisso, eu acho, é a sintaxe que será necessária para isso. Estou pensando em fazer isso, pois parece o mais simples e fácil de implementá-lo. Não tenho muita certeza disso, devido à sintaxe.

ou seja, para adicionar um componente com esse design, seria semelhante a:

Entity e = world.createEntity();
world.addComponent<Position>(e, 0, 3);

Em oposição a isso:

Entity e = world.createEntity();
e.addComponent<Position>(0, 3);

Contras

  • Sintaxe
  • IDs duplicados
miguel.martin
fonte

Respostas:

12

Seus IDs devem ser uma mistura de índice e versão . Isso permitirá que você reutilize IDs com eficiência, use o ID para encontrar componentes rapidamente e torne sua "opção 2" muito mais fácil de implementar (embora a opção 3 possa ser muito mais agradável com algum trabalho).

struct entity {
  uint16 version;
  /* and other crap that doesn't belong in components */
};

std::vector<entity> pool;
std::vector<uint16> freelist;
typedef uint32 entity_id; /* this shoudl be a wrapper class */

entity_id createEntity()
{
  uint16 index;
  if (!freelist.empty())
  {
    pool.push_back(entity());
    freelist.push_back(pool.size() - 1);
  }
  index = freelist.pop_back();

  return (pool[id].version << 16) | index;
}

void deleteEntity(entity_id id)
{
   uint16 index = id & 0xFFFF;
   ++pool[index].version;
   freelist.push_back(index);
}

entity* getEntity(entity_id id)
{
  uint16 index = id & 0xFFFF;
  uint16 version = id >> 16;
  if (index < pool.size() && pool[index].version == version)
    return &pool[index];
  else
    return NULL;
 }

Isso alocará um novo inteiro de 32 bits, que é uma combinação de um índice exclusivo (que é único entre todos os objetos ativos) e uma tag de versão (que será exclusiva para todos os objetos que já ocuparam esse índice).

Ao excluir uma entidade, você incrementa a versão. Agora, se você tiver alguma referência a esse ID flutuando, ele não terá mais a mesma tag de versão que a entidade que ocupa esse ponto no pool. Qualquer tentativa de ligar getEntity(ou um isEntityValidou o que você preferir) falhará. Se você alocar um novo objeto nessa posição, os IDs antigos ainda falharão.

Você pode usar algo parecido com isso para a sua "opção 2" para garantir que funcione sem preocupações com referências a entidades antigas. Observe que você nunca deve armazenar um, entity*pois ele pode se mover ( pool.push_back()pode realocar e mover todo o pool!) E, entity_idem vez disso, use apenas referências de longo prazo. Use getEntitypara recuperar um objeto de acesso mais rápido apenas no código local. Você também pode usar um std::dequeou similar para evitar a invalidação do ponteiro, se desejar.

Sua "opção 3" é uma escolha perfeitamente válida. Não há nada de errado em usar em world.foo(e)vez de e.foo(), especialmente porque você provavelmente quer a referência de worldqualquer maneira e não é necessariamente melhor (embora não necessariamente pior) armazenar essa referência na própria entidade.

Se você realmente deseja que a e.foo()sintaxe permaneça, considere um "ponteiro inteligente" que lida com isso para você. Criando o código de exemplo que desisti acima, você pode ter algo como:

class entity_ptr {
  world* _world;
  entity_id _id;

public:
  entity_ptr() : _id(0) { }
  entity_ptr(world& world, entity_id id) : _world(&world), _id(id) { }

  bool empty() const { return _world != NULL && _world->getEntity(_id) != NULL; }
  void clear() { _world = NULL; _id = 0; }
  entity* get() { assert(!empty()); return _world->getEntity(_id); }
  entity* operator->() { return get(); }
  entity& operator*() { return *get(); }
  // add const method where appropriate
};

Agora você tem uma maneira de armazenar uma referência a uma entidade que usa um ID exclusivo e que pode usar o ->operador para acessar a entityclasse (e qualquer método que você criar nela) naturalmente. O _worldmembro também pode ser único ou global, se você preferir.

Seu código usa apenas um entity_ptrlugar de qualquer outra referência de entidade e pronto. Você pode até adicionar contagem de referência automática à classe, se desejar (um pouco mais confiável se atualizar todo esse código para C ++ 11 e usar referências de semântica de movimentação e rvalue) para poder usar em entity_ptrqualquer lugar e não pensar mais muito sobre referências e propriedade. Ou, e é isso que eu prefiro, faça um separado owning_entitye weak_entitydigite apenas as contagens de referência de gerenciamento anteriores, para que você possa usar o sistema de tipos para diferenciar entre identificadores que mantêm uma entidade viva e aqueles que apenas fazem referência a ela até que ela seja destruída.

Observe que a sobrecarga é muito baixa. A manipulação de bits é barata. A pesquisa extra no pool não é um custo real se você acessar outros campos entitylogo em seguida. Se suas entidades são realmente apenas ids e nada mais, pode haver um pouco de sobrecarga extra. Pessoalmente, a ideia de um ECS onde entidades são apenas identificações e nada mais parece um pouco ... acadêmico para mim. Há pelo menos alguns sinalizadores que você deseja armazenar na entidade geral, e jogos maiores provavelmente desejam uma coleção de algum tipo de componente da entidade (lista vinculada inline, se nada mais) para ferramentas e suporte a serialização.

Como uma nota final, intencionalmente não inicializei entity::version. Não importa. Não importa qual seja a versão inicial, desde que a incrementemos sempre que estivermos bem. Se acabar perto 2^16, será apenas uma volta. Se você terminar de maneira que as IDs antigas permaneçam válidas, mude para versões maiores (e IDs de 64 bits, se necessário). Para estar seguro, você provavelmente deve limpar o entity_ptr sempre que o verificar e estiver vazio. Você pode empty()fazer isso por você com um mutável _world_e _id, apenas tome cuidado com a segmentação.

Sean Middleditch
fonte
Por que não conter o ID na estrutura da entidade? Estou bastante confuso. Também você poderia usar std :: shared_ptr / weak_ptr para owning_entitye weak_entity?
Miguel.martin
Você pode conter o ID, se desejar. O único ponto é que o valor do ID é alterado quando uma entidade no slot é destruída, enquanto o ID também contém o índice do slot para uma pesquisa eficiente. Você pode usar shared_ptre weak_ptrestar ciente de que eles são destinados a objetos alocados individualmente (embora possam ter deleters personalizados para alterar isso) e, portanto, não são os tipos mais eficientes de usar. weak_ptrem particular, pode não fazer o que você deseja; impede que uma entidade seja totalmente desalocada / reutilizada até que cada uma weak_ptrseja redefinida enquanto weak_entitynão o faria.
Sean Middleditch
Seria muito mais fácil explicar essa abordagem se eu tivesse um quadro branco ou não estivesse com preguiça de elaborar isso no Paint ou algo assim. :) Acho que visualizar a estrutura a torna extremamente clara.
6137 Sean Middleditch
gamesfromwithin.com/managing-data-relationships Este artigo parece apresentar um pouco da mesma coisa que você disse em sua resposta, é isso que você quer dizer?
Miguel.martin
11
Sou o autor do EntityX , e a reutilização de índices me incomoda há um tempo. Com base no seu comentário, atualizei o EntityX para também incluir uma versão. Obrigado @SeanMiddleditch!
Alec Thomas
0

Atualmente, estou trabalhando em algo parecido e estou usando uma solução mais próxima do seu número 1.

Tenho EntityHandleinstâncias retornadas do World. Cada EntityHandleum tem um ponteiro para World(no meu caso, eu apenas o chamo EntityManager), e os métodos de manipulação / recuperação de dados EntityHandlesão na verdade chamadas para World: por exemplo, para adicionar a Componenta uma entidade, você pode chamar EntityHandle.addComponent(component), que por sua vez chama World.addComponent(this, component).

Dessa forma, as Entityclasses de wrapper não são armazenadas e você evita a sobrecarga extra na sintaxe que obteria com a opção 3. Ele também evita o problema de "Se uma Entidade for destruída, as classes de wrapper duplicadas não terão um valor atualizado. ", porque todos apontam para os mesmos dados.

vijoc
fonte
O que acontece se você criar outro EntityHandle para se parecer com a mesma entidade e tentar excluir uma das alças? O outro identificador ainda terá o mesmo ID, o que significa que ele "manipula" uma entidade morta.
Miguel.martin 01/07/2017
Isso é verdade, os outros identificadores restantes apontarão para o ID que não "detém" mais uma entidade. Obviamente, situações em que você exclui uma entidade e tenta acessá-la de outro lugar devem ser evitadas. O Worldpoderia, por exemplo, lançar uma exceção ao tentar manipular / recuperar dados associados a uma entidade "morta".
vijoc
Embora seja melhor evitar, no mundo real isso acontecerá. Os scripts se apegam às referências, os objetos "inteligentes" do jogo (como a busca de mísseis) se apegam às referências, etc. Você realmente precisa de um sistema capaz de, em todos os casos, lidar adequadamente com referências obsoletas ou que rastreie e zere fraco referências.
Sean Middleditch
O mundo poderia, por exemplo, lançar uma exceção ao tentar manipular / recuperar dados associados a uma entidade "morta". Não se o ID antigo agora estiver alocado com uma nova entidade.
Miguel.martin