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 isEntityValid
ou 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_id
em vez disso, use apenas referências de longo prazo. Use getEntity
para recuperar um objeto de acesso mais rápido apenas no código local. Você também pode usar um std::deque
ou 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 world
qualquer 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 entity
classe (e qualquer método que você criar nela) naturalmente. O _world
membro também pode ser único ou global, se você preferir.
Seu código usa apenas um entity_ptr
lugar 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_ptr
qualquer lugar e não pensar mais muito sobre referências e propriedade. Ou, e é isso que eu prefiro, faça um separado owning_entity
e weak_entity
digite 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 entity
logo 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.
owning_entity
eweak_entity
?shared_ptr
eweak_ptr
estar 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_ptr
em particular, pode não fazer o que você deseja; impede que uma entidade seja totalmente desalocada / reutilizada até que cada umaweak_ptr
seja redefinida enquantoweak_entity
não o faria.Atualmente, estou trabalhando em algo parecido e estou usando uma solução mais próxima do seu número 1.
Tenho
EntityHandle
instâncias retornadas doWorld
. CadaEntityHandle
um tem um ponteiro paraWorld
(no meu caso, eu apenas o chamoEntityManager
), e os métodos de manipulação / recuperação de dadosEntityHandle
são na verdade chamadas paraWorld
: por exemplo, para adicionar aComponent
a uma entidade, você pode chamarEntityHandle.addComponent(component)
, que por sua vez chamaWorld.addComponent(this, component)
.Dessa forma, as
Entity
classes 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.fonte
World
poderia, por exemplo, lançar uma exceção ao tentar manipular / recuperar dados associados a uma entidade "morta".