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.
quelle
Antworten:
Sie haben im Wesentlichen ein statisches Objektsystem mit einem Pool-Allokator und mit dynamischen Klassen entworfen.
Ich habe ein Objektsystem geschrieben, das in meiner Schulzeit fast identisch mit Ihrem "Assemblages" -System funktioniert, obwohl ich in meinen eigenen Entwürfen immer eher "Assemblagen" als "Blaupausen" oder "Archetypen" bezeichne. Die Architektur war eher ein Problem als naive Objektsysteme und hatte keine messbaren Leistungsvorteile gegenüber einigen der flexibleren Designs, mit denen ich sie verglichen habe. Die Möglichkeit, ein Objekt dynamisch zu ändern, ohne es erneut ändern oder neu zuweisen zu müssen, ist äußerst wichtig, wenn Sie an einem Spieleditor arbeiten. Designer möchten Komponenten per Drag & Drop auf Ihre Objektdefinitionen ziehen. Möglicherweise muss der Laufzeitcode in einigen Designs sogar Komponenten effizient ändern, obwohl mir das persönlich nicht gefällt. Abhängig davon, wie Sie Objektreferenzen in Ihrem Editor verknüpfen,
In den meisten nicht trivialen Fällen wird die Cache-Kohärenz schlechter als gedacht. Ihr KI-System zum Beispiel kümmert sich nicht um
Render
Komponenten, sondern bleibt als Teil jeder Entität beim Durchlaufen dieser Komponenten hängen. Die Objekte, über die iteriert wird, sind größer, und Cacheline-Anforderungen ziehen unnötige Daten ein, und mit jeder Anforderung werden weniger ganze Objekte zurückgegeben. Es ist immer noch besser als die naive Methode, und die Objektzusammensetzung der naiven Methode wird sogar in großen AAA-Engines verwendet. Sie brauchen also wahrscheinlich keine bessere, aber denken Sie zumindest nicht, dass Sie sie nicht weiter verbessern können.Ihr Ansatz ist für einige am sinnvollstenKomponenten, aber nicht alle. Ich mag ECS nicht besonders, weil es befürwortet, jede Komponente immer in einem separaten Container abzulegen, was für Physik oder Grafik oder so weiter Sinn macht, aber überhaupt keinen Sinn, wenn Sie mehrere Skriptkomponenten oder zusammensetzbare KI zulassen. Wenn Sie das Komponentensystem nicht nur für integrierte Objekte verwenden lassen, sondern auch für Designer und Gameplay-Programmierer, um das Objektverhalten zu komponieren, kann es sinnvoll sein, alle KI-Komponenten (die häufig interagieren) oder alle Skripte zu gruppieren Komponenten (da Sie sie alle in einem Stapel aktualisieren möchten). Wenn Sie das leistungsstärkste System wünschen, benötigen Sie eine Mischung aus Komponentenzuordnungs- und Speicherschemata und nehmen sich Zeit, um endgültig herauszufinden, welches für den jeweiligen Komponententyp am besten geeignet ist.
quelle
Was Sie getan haben, sind überarbeitete C ++ - Objekte. Der Grund, warum dies offensichtlich ist, ist, dass wenn Sie das Wort "Entität" durch "Klasse" und "Komponente" durch "Mitglied" ersetzen, dies ein Standard-OOP-Design ist, das Mixins verwendet.
1) Sie verlieren die Dynamik steckbarer Komponenten, die speziell entwickelt wurden, um sich von der statischen Klassenkonstruktion zu lösen.
2) Die Speicherkohärenz ist innerhalb eines Datentyps am wichtigsten, nicht innerhalb eines Objekts, das mehrere Datentypen an einem Ort vereint. Dies ist einer der Gründe, warum Komponenten- + Systeme erstellt wurden, um die Fragmentierung des Klassen- + Objektspeichers zu vermeiden.
3) Dieses Design wird auch auf den C ++ - Klassenstil zurückgesetzt, da Sie die Entität als kohärentes Objekt betrachten, wenn in einem Komponenten- + Systemdesign die Entität lediglich ein Tag / eine ID ist, um das Innenleben für den Menschen verständlich zu machen.
4) Es ist für eine Komponente genauso einfach, sich selbst zu serialisieren wie für ein komplexes Objekt, mehrere Komponenten in sich selbst zu serialisieren, wenn nicht sogar einfacher, als Programmierer den Überblick zu behalten.
5) Der nächste logische Schritt auf diesem Weg besteht darin, Systeme zu entfernen und diesen Code direkt in die Entität einzufügen, wo sie alle Daten enthält, die sie zum Arbeiten benötigt. Wir können alle sehen, was das bedeutet =)
quelle
Es ist nicht so wichtig, wie Entitäten zusammenzuhalten, wie Sie vielleicht denken, weshalb es schwierig ist, sich einen anderen gültigen Grund als "weil es eine Einheit ist" vorzustellen. Da Sie dies jedoch aus Gründen der Cache-Kohärenz im Gegensatz zur logischen Kohärenz tun, ist dies möglicherweise sinnvoll.
Eine Schwierigkeit, die Sie haben könnten, ist die Interaktion zwischen Komponenten in verschiedenen Buckets. Es ist nicht besonders einfach, etwas zu finden , auf das Ihre KI schießen kann. Sie müssten beispielsweise alle Eimer durchgehen, die möglicherweise ein gültiges Ziel enthalten. Ohne eine externe Datenstruktur könnte die Kollisionserkennung ebenso schwierig sein.
Um Entitäten aus logischen Gründen zusammen zu organisieren, muss ich Entitäten möglicherweise nur zu Identifikationszwecken in meinen Missionen zusammenhalten. Ich muss wissen, ob Sie gerade Entitätstyp A oder Typ B erstellt haben, und ich komme darum herum, indem Sie ... Sie haben es erraten: Hinzufügen einer neuen Komponente, die die Assemblage identifiziert, die diese Entität zusammensetzt. Selbst dann sammle ich nicht alle Komponenten für eine große Aufgabe zusammen, ich muss nur wissen, was es ist. Ich denke nicht, dass dieser Teil schrecklich nützlich ist.
quelle