Wir gehen vom grundlegenden System-Komponenten-Entitäten-Ansatz aus .
Lassen Sie uns Assemblagen (Begriff aus diesem Artikel abgeleitet) nur aus Informationen über Komponententypen erstellen . Dies geschieht dynamisch zur Laufzeit, genau wie wir einer Entität nacheinander Komponenten hinzufügen / entfernen würden, aber nennen wir es einfach genauer, da es sich nur um Typinformationen handelt.
Dann konstruieren wir Entitäten , die für jede von ihnen eine Assemblage angeben . Sobald wir die Entität erstellt haben, ist ihre Assemblage unveränderlich, was bedeutet, dass wir sie nicht direkt an Ort und Stelle ändern können, aber dennoch die Signatur der vorhandenen Entität für eine lokale Kopie (zusammen mit dem Inhalt) erhalten, die richtigen Änderungen daran vornehmen und eine neue Entität erstellen können davon.
Nun zum Schlüsselkonzept: Wenn eine Entität erstellt wird, wird sie einem Objekt namens Assemblage Bucket zugewiesen. Dies bedeutet, dass sich alle Entitäten derselben Signatur im selben Container befinden (z. B. in std :: vector).
Jetzt durchlaufen die Systeme einfach jeden Eimer ihres Interesses und erledigen ihre Arbeit.
Dieser Ansatz hat einige Vorteile:
- Komponenten werden in wenigen (genau: Anzahl der Buckets) zusammenhängenden Speicherblöcken gespeichert - dies verbessert die Speicherfreundlichkeit und es ist einfacher, den gesamten Spielstatus zu sichern
- Systeme verarbeiten Komponenten linear, was eine verbesserte Cache-Kohärenz bedeutet - Tschüss-Wörterbücher und zufällige Speichersprünge
- Das Erstellen einer neuen Entität ist so einfach wie das Zuordnen einer Assemblage zum Bucket und das Zurückschieben der erforderlichen Komponenten auf ihren Vektor
- Das Löschen einer Entität ist so einfach wie ein Aufruf von std :: move, um das letzte Element gegen das gelöschte auszutauschen, da die Reihenfolge in diesem Moment keine Rolle spielt
Wenn wir viele Entitäten mit völlig unterschiedlichen Signaturen haben, verringern sich die Vorteile der Cache-Kohärenz, aber ich denke nicht, dass dies in den meisten Anwendungen passieren würde.
Es gibt auch ein Problem mit der Ungültigmachung von Zeigern, sobald Vektoren neu zugewiesen werden - dies könnte durch Einführung einer Struktur wie der folgenden gelöst werden:
struct assemblage_bucket {
struct entity_watcher {
assemblage_bucket* owner;
entity_id real_index_in_vector;
};
std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;
//...
};
Wenn wir also aus irgendeinem Grund in unserer Spielelogik eine neu erstellte Entität verfolgen möchten, registrieren wir im Bucket einen entity_watcher . Sobald die Entität beim Entfernen std :: move'd sein muss, suchen wir nach ihren Beobachtern und aktualisieren sie ihre real_index_in_vector
zu neuen Werten. In den meisten Fällen wird für jede Entitätslöschung nur eine einzige Wörterbuchsuche durchgeführt.
Gibt es weitere Nachteile bei diesem Ansatz?
Warum wird die Lösung nirgends erwähnt, obwohl sie ziemlich offensichtlich ist?
BEARBEITEN : Ich bearbeite die Frage, um "die Antworten zu beantworten", da die Kommentare nicht ausreichen.
Sie verlieren die Dynamik steckbarer Komponenten, die speziell entwickelt wurden, um sich von der statischen Klassenkonstruktion zu lösen.
Ich nicht. Vielleicht habe ich es nicht klar genug erklärt:
auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket
Es ist so einfach, nur die Signatur einer vorhandenen Entität zu übernehmen, sie zu ändern und erneut als neue Entität hochzuladen. Steckbare, dynamische Natur ? Natürlich. Hier möchte ich betonen, dass es nur eine "Assemblage" - und eine "Bucket" -Klasse gibt. Buckets werden datengesteuert und zur Laufzeit in einer optimalen Menge erstellt.
Sie müssten alle Buckets durchgehen, die möglicherweise ein gültiges Ziel enthalten. Ohne eine externe Datenstruktur könnte die Kollisionserkennung ebenso schwierig sein.
Nun, deshalb haben wir die oben genannten externen Datenstrukturen . Die Problemumgehung ist so einfach wie die Einführung eines Iterators in der Systemklasse, der erkennt, wann zum nächsten Bucket gesprungen werden muss. Das Springen wäre für die Logik rein transparent.