Vos identifiants doivent être un mélange d' index et de version . Cela vous permettra de réutiliser efficacement les ID, d'utiliser l'ID pour trouver rapidement des composants et de rendre votre "option 2" beaucoup plus facile à implémenter (bien que l'option 3 puisse être rendue beaucoup plus agréable avec du travail).
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;
}
Cela allouera un nouvel entier 32 bits qui est une combinaison d'un index unique (qui est unique parmi tous les objets actifs) et d'une balise de version (qui sera unique pour tous les objets qui ont déjà occupé cet index).
Lorsque vous supprimez une entité, vous incrémentez la version. Maintenant, si vous avez des références à cet identifiant flottant, il n'aura plus la même balise de version que l'entité occupant cet emplacement dans le pool. Toute tentative d'appel getEntity
(ou un isEntityValid
ou ce que vous préférez) échouera. Si vous allouez un nouvel objet à cette position, les anciens ID échoueront toujours.
Vous pouvez utiliser quelque chose comme ça pour votre "option 2" pour vous assurer que cela fonctionne sans vous soucier des anciennes références d'entité. Notez que vous ne devez jamais stocker un entity*
car ils pourraient se déplacer ( pool.push_back()
pourraient réaffecter et déplacer l'ensemble du pool!) Et ne les utiliser qu'à la place entity_id
pour des références à long terme. Utilisez getEntity
pour récupérer un objet d'accès plus rapide uniquement dans le code local. Vous pouvez également utiliser un std::deque
ou similaire pour éviter l'invalidation du pointeur si vous le souhaitez.
Votre "option 3" est un choix parfaitement valable. Il n'y a rien d'intrinsèquement mauvais à utiliser à la world.foo(e)
place de e.foo()
, d'autant plus que vous voulez probablement la référence de world
toute façon et qu'il n'est pas nécessairement préférable (mais pas nécessairement pire) de stocker cette référence dans l'entité elle-même.
Si vous voulez vraiment que la e.foo()
syntaxe reste, pensez à un "pointeur intelligent" qui gère cela pour vous. En vous basant sur l'exemple de code que j'ai abandonné ci-dessus, vous pourriez avoir quelque chose comme:
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
};
Vous avez maintenant un moyen de stocker une référence à une entité qui utilise un ID unique et qui peut utiliser l' ->
opérateur pour accéder à la entity
classe (et à toute méthode que vous créez dessus) tout naturellement. Le _world
membre peut également être un singleton ou un global, si vous préférez.
Votre code utilise simplement un entity_ptr
à la place de toute autre référence d'entité et disparaît. Vous pouvez même ajouter un comptage automatique des références à la classe si vous le souhaitez (un peu plus fiable si vous mettez à jour tout ce code en C ++ 11 et utilisez la sémantique de déplacement et les références rvalue) afin que vous puissiez simplement utiliser entity_ptr
partout et ne plus penser intensément sur les références et la propriété. Ou, et c'est ce que je préfère, créez un type distinct owning_entity
et weak_entity
uniquement avec les anciens comptages de référence de gestion afin que vous puissiez utiliser le système de type pour différencier les descripteurs qui maintiennent une entité en vie et ceux qui la référencent jusqu'à sa destruction.
Notez que les frais généraux sont très faibles. La manipulation des bits est bon marché. La recherche supplémentaire dans le pool n'est pas un coût réel si vous accédez à d'autres champs entity
peu de temps après. Si vos entités ne sont vraiment que des identifiants et rien d' autre, il peut y avoir un peu de surcharge supplémentaire. Personnellement, l'idée d'un ECS où les entités ne sont que des identifiants et rien d'autre me semble un peu ... académique. Il y a au moins quelques indicateurs que vous voudrez stocker sur l'entité générale, et les grands jeux voudront probablement une collection de composants de l'entité (liste liée en ligne si rien d'autre) pour les outils et la prise en charge de la sérialisation.
Pour terminer, je n'ai pas intentionnellement initialisé entity::version
. Ça n'a pas d'importance. Quelle que soit la version initiale, tant que nous l'incrémentons à chaque fois que nous allons bien. Si elle se rapproche, 2^16
elle s'enroulera. Si vous finissez par vous déplacer de manière à ce que les anciens ID restent valides, passez à des versions plus grandes (et à des ID 64 bits si vous en avez besoin). Pour être sûr, vous devez probablement effacer entity_ptr chaque fois que vous le vérifiez et qu'il est vide. Vous pouvez le faire empty()
pour vous avec un mutable _world_
et _id
, soyez juste prudent avec le filetage.
owning_entity
etweak_entity
?