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
?shared_ptr
etweak_ptr
mais sachez qu'ils sont destinés aux objets alloués individuellement (bien qu'ils puissent avoir des suppresseurs personnalisés pour modifier cela) et ne sont donc pas les types les plus efficaces à utiliser.weak_ptr
en particulier, peut ne pas faire ce que vous voulez; il empêche une entité d'être entièrement désallouée / réutilisée jusqu'à ce que toutweak_ptr
soit réinitialisé alorsweak_entity
qu'il ne le serait pas.Je travaille actuellement sur quelque chose de similaire et utilise une solution la plus proche de votre numéro 1.
J'ai des
EntityHandle
instances de retour duWorld
. ChacunEntityHandle
a un pointeur sur leWorld
(dans mon cas, je l'appelle justeEntityManager
), et les méthodes de manipulation / récupération des données dans leEntityHandle
sont en fait des appels auWorld
: par exemple, pour ajouter unComponent
à une entité, vous pouvez appelerEntityHandle.addComponent(component)
, qui à son tour appelleraWorld.addComponent(this, component)
.De cette façon, les
Entity
classes wrapper ne sont pas stockées et vous évitez la surcharge supplémentaire de syntaxe que vous obtiendriez avec votre option 3. Cela évite également le problème de "Si une entité est détruite, les classes wrapper d'entité en double n'auront pas de valeur mise à jour ", car ils pointent tous vers les mêmes données.la source
World
pourrait par exemple lever une exception lors d'une tentative de manipulation / récupération de données associées à une entité "morte".