Ihre IDs sollten eine Mischung aus Index und Version sein . Auf diese Weise können Sie IDs effizient wiederverwenden, die ID verwenden, um Komponenten schnell zu finden, und Ihre "Option 2" lässt sich viel einfacher implementieren (obwohl Option 3 mit etwas Arbeit viel schmackhafter gemacht werden kann).
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;
}
Dadurch wird eine neue 32-Bit-Ganzzahl zugewiesen, die eine Kombination aus einem eindeutigen Index (der für alle Live-Objekte eindeutig ist) und einem Versions-Tag (der für alle Objekte eindeutig ist, die diesen Index jemals belegt haben) ist.
Beim Löschen einer Entität erhöhen Sie die Version. Wenn Sie nun Verweise auf diese ID haben, hat diese nicht mehr das gleiche Versions-Tag wie die Entität, die diesen Platz im Pool belegt. Alle Anrufversuche getEntity
(oder ein isEntityValid
oder was auch immer Sie bevorzugen) schlagen fehl. Wenn Sie an dieser Position ein neues Objekt zuweisen, schlagen die alten IDs weiterhin fehl.
Sie können so etwas für Ihre "Option 2" verwenden, um sicherzustellen, dass es ohne Bedenken über alte Entitätsreferenzen funktioniert. Beachten Sie, dass Sie niemals einen speichern dürfen, entity*
da diese möglicherweise verschoben werden ( pool.push_back()
der gesamte Pool könnte neu zugewiesen und verschoben werden!) Und entity_id
stattdessen nur für langfristige Referenzen verwendet werden. Verwenden Sie getEntity
diese Option , um ein Objekt mit schnellerem Zugriff nur im lokalen Code abzurufen. Sie können auch ein std::deque
oder ähnliches verwenden, um eine Ungültigmachung des Zeigers zu vermeiden, wenn Sie dies wünschen.
Ihre "Option 3" ist eine absolut gültige Wahl. Es ist an sich nichts Falsches daran, world.foo(e)
stattdessen zu verwenden e.foo()
, zumal Sie wahrscheinlich world
trotzdem auf die Referenz verweisen möchten und es nicht unbedingt besser (wenn auch nicht unbedingt schlechter) ist, diese Referenz in der Entität selbst zu speichern.
Wenn Sie wirklich möchten, dass die e.foo()
Syntax erhalten bleibt, ziehen Sie einen "intelligenten Zeiger" in Betracht, der dies für Sie erledigt. Aufbauend auf dem Beispielcode, den ich oben aufgegeben habe, könnten Sie etwas haben wie:
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
};
Jetzt haben Sie die Möglichkeit, einen Verweis auf eine Entität zu speichern, die eine eindeutige ID verwendet und mit dem ->
Operator entity
ganz natürlich auf die Klasse (und jede darauf erstellte Methode) zugreifen kann. Das _world
Mitglied kann auch ein Singleton oder ein Global sein, wenn Sie es vorziehen.
Ihr Code verwendet nur eine entity_ptr
anstelle aller anderen Entitätsreferenzen und geht. Sie können der Klasse sogar eine automatische Referenzzählung hinzufügen, wenn Sie möchten (etwas zuverlässiger, wenn Sie den gesamten Code auf C ++ 11 aktualisieren und Verschiebungssemantik und rWert-Referenzen verwenden), sodass Sie sie einfach entity_ptr
überall verwenden können und nicht mehr lange nachdenken müssen über Referenzen und Eigentum. Oder, und das ist es, was ich bevorzuge, machen Sie eine separate owning_entity
und weak_entity
Typen mit nur den früheren verwaltenden Referenzzählern, damit Sie das Typsystem verwenden können, um zwischen Handles zu unterscheiden, die eine Entität am Leben erhalten, und solchen, die nur auf sie verweisen, bis sie zerstört wird.
Beachten Sie, dass der Overhead sehr gering ist. Die Bitmanipulation ist billig. Die zusätzliche Suche in den Pool ist keine echte Kosten, wenn Sie entity
ohnehin bald darauf auf andere Felder zugreifen . Wenn Ihre Entitäten wirklich nur IDs und nichts anderes sind, kann es zu einem zusätzlichen Aufwand kommen. Persönlich scheint mir die Idee eines ECS, bei dem Entitäten nur IDs sind und nichts anderes, ein bisschen ... akademisch zu sein. Es gibt mindestens ein paar Flags, die Sie in der allgemeinen Entität speichern möchten, und größere Spiele benötigen wahrscheinlich eine Sammlung der Komponenten der Entität (inline verknüpfte Liste, wenn nichts anderes) für Tools und Serialisierungsunterstützung.
Als ziemlich letzte Anmerkung habe ich absichtlich nicht initialisiert entity::version
. Es spielt keine Rolle. Egal wie die ursprüngliche Version lautet, solange wir sie jedes Mal erhöhen, wenn es uns gut geht. Wenn es nahe kommt, 2^16
wird es einfach herumlaufen. Wenn Sie am Ende so herumlaufen, dass alte IDs gültig bleiben, wechseln Sie zu größeren Versionen (und 64-Bit-IDs, falls erforderlich). Um sicher zu gehen, sollten Sie entity_ptr wahrscheinlich jedes Mal löschen, wenn Sie es überprüfen und es leer ist. Sie könnten empty()
dies für Sie mit einem veränderlichen tun _world_
und _id
nur vorsichtig mit dem Einfädeln sein.
owning_entity
und verwendenweak_entity
?shared_ptr
und sichweak_ptr
jedoch bewusst sein, dass sie für individuell zugewiesene Objekte bestimmt sind (obwohl sie benutzerdefinierte Löscher haben können, um dies zu ändern) und daher nicht die effizientesten Typen sind.weak_ptr
insbesondere kann nicht tun, was Sie wollen; Es verhindert, dass eine Entität vollständig freigegeben / wiederverwendet wird, bis alleweak_ptr
zurückgesetzt werden, währendweak_entity
dies nicht der Fall ist .Ich arbeite gerade an etwas Ähnlichem und habe eine Lösung verwendet, die Ihrer Nummer 1 am nächsten kommt.
Ich habe
EntityHandle
Instanzen von der zurückgegebenWorld
. JedesEntityHandle
hat einen Zeiger auf dasWorld
(in meinem Fall nenne ich es einfachEntityManager
), und die Datenmanipulations- / Abrufmethoden inEntityHandle
sind tatsächlich Aufrufe vonWorld
: z. B. umComponent
einer Entität ein hinzuzufügen , können Sie aufrufenEntityHandle.addComponent(component)
, was wiederum aufruftWorld.addComponent(this, component)
.Auf diese Weise werden die
Entity
Wrapper-Klassen nicht gespeichert, und Sie vermeiden den zusätzlichen Syntaxaufwand, den Sie mit Option 3 erhalten. Außerdem wird das Problem "Wenn eine Entität zerstört wird, haben die Wrapper-Klassen für doppelte Entitäten keinen aktualisierten Wert" vermieden ", weil sie alle auf die gleichen Daten verweisen.quelle
World
könnte beispielsweise eine Ausnahme auslösen, wenn versucht wird, Daten zu manipulieren / abzurufen, die einer "toten" Entität zugeordnet sind.