Hinweise zum Verknüpfen zwischen Entitätskomponentensystemen in C ++

10

Nachdem ich einige Dokumentationen über Entity-Component-Systeme gelesen hatte, entschied ich mich, meine zu implementieren. Bisher habe ich eine Weltklasse, die die Entitäten und den Systemmanager (Systeme) enthält, eine Entitätsklasse, die die Komponenten als std :: map enthält, und einige Systeme. Ich halte Entitäten als std :: vector in World. Bisher kein Problem. Was mich verwirrt, ist die Iteration von Entitäten. Ich kann mir das nicht genau vorstellen, also kann ich diesen Teil immer noch nicht implementieren. Sollte jedes System eine lokale Liste von Entitäten enthalten, an denen sie interessiert sind? Oder sollte ich einfach die Entitäten in der Weltklasse durchlaufen und eine verschachtelte Schleife erstellen, um Systeme zu durchlaufen und zu überprüfen, ob die Entität die Komponenten enthält, an denen das System interessiert ist? Ich meine :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

Ich denke jedoch, dass ein Bitmaskensystem die Flexibilität beim Einbetten einer Skriptsprache blockieren wird. Oder lokale Listen für jedes System erhöhen die Speichernutzung für Klassen. Ich bin furchtbar verwirrt.

deniz
quelle
Warum erwarten Sie, dass der Bitmasken-Ansatz Skriptbindungen behindert? Verwenden Sie außerdem Verweise (wenn möglich const) in den for-each-Schleifen, um das Kopieren von Entitäten und Systemen zu vermeiden.
Benjamin Kloster
Wenn Sie beispielsweise eine Bitmaske verwenden, enthält ein Int nur 32 verschiedene Komponenten. Ich impliziere nicht, dass es mehr als 32 Komponenten geben wird, aber was ist, wenn ich habe? Ich muss ein anderes Int oder 64bit Int erstellen, es wird nicht dynamisch sein.
Deniz
Sie können std :: bitset oder std :: vector <bool> verwenden, je nachdem, ob es zur Laufzeit dynamisch sein soll oder nicht.
Benjamin Kloster

Antworten:

7

Lokale Listen für jedes System erhöhen die Speichernutzung für Klassen.

Es ist ein traditioneller Raum-Zeit-Kompromiss .

Während das Durchlaufen aller Entitäten und das Überprüfen ihrer Signaturen direkt zum Code führt, kann es mit zunehmender Anzahl von Systemen ineffizient werden. Stellen Sie sich ein spezialisiertes System vor (lassen Sie es eingeben), das unter Tausenden von nicht verwandten Entitäten nach seiner wahrscheinlich einzigen interessierenden Entität sucht .

Trotzdem kann dieser Ansatz abhängig von Ihren Zielen immer noch gut genug sein.

Wenn Sie sich Sorgen um die Geschwindigkeit machen, sollten Sie natürlich auch andere Lösungen in Betracht ziehen.

Sollte jedes System eine lokale Liste von Entitäten enthalten, an denen sie interessiert sind?

Genau. Dies ist ein Standardansatz, der Ihnen eine anständige Leistung bieten sollte und relativ einfach zu implementieren ist. Der Speicheraufwand ist meiner Meinung nach vernachlässigbar - wir sprechen über das Speichern von Zeigern.

Nun, wie diese "Listen von Interesse" zu pflegen sind, mag nicht so offensichtlich sein. Was den Datencontainer betrifft, std::vector<entity*> targetsist die Klasse innerhalb des Systems vollkommen ausreichend. Was ich jetzt mache, ist Folgendes:

  • Die Entität ist bei der Erstellung leer und gehört keinem System an.
  • Wann immer ich einer Entität eine Komponente hinzufüge:

    • seine aktuelle Bitsignatur erhalten ,
    • Ordnen Sie die Größe der Komponente dem Pool der Welt mit einer angemessenen Blockgröße zu (ich persönlich verwende boost :: pool) und weisen Sie die Komponente dort zu
    • Erhalten Sie die neue Bitsignatur der Entität (die nur "aktuelle Bitsignatur" plus die neue Komponente ist).
    • Iterierte durch alle Systeme der Welt und wenn es ein System ist , dessen Unterschrift nicht der Fall ist die aktuelle Signatur des Unternehmens übereinstimmen und nicht die neue Signatur übereinstimmen, wird es offensichtlich , dass wir den Zeiger auf unser Unternehmen dort push_back sollte.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

Das Entfernen einer Entität ist völlig analog, mit dem einzigen Unterschied, den wir entfernen, wenn ein System mit unserer aktuellen Signatur übereinstimmt (was bedeutet, dass die Entität vorhanden war) und nicht mit der neuen Signatur übereinstimmt (was bedeutet, dass die Entität nicht mehr vorhanden sein sollte ).

Jetzt können Sie die Verwendung von std :: list in Betracht ziehen, da das Entfernen aus dem Vektor O (n) ist, ganz zu schweigen davon, dass Sie jedes Mal, wenn Sie aus der Mitte entfernen, einen großen Datenblock verschieben müssten. Eigentlich müssen Sie das nicht - da uns die Verarbeitungsreihenfolge auf dieser Ebene egal ist, können wir einfach std :: remove aufrufen und damit leben, dass wir bei jedem Löschen nur eine O (n) -Suche nach unserer durchführen müssen zu entfernende Entität.

std :: list würde Ihnen O (1) entfernen geben, aber auf der anderen Seite haben Sie ein bisschen zusätzlichen Speicheraufwand. Denken Sie auch daran, dass Sie die meiste Zeit Entitäten verarbeiten und nicht entfernen - und dies wird mit std :: vector sicherlich schneller erledigt.

Wenn Sie sehr leistungskritisch sind, können Sie auch ein anderes Datenzugriffsmuster in Betracht ziehen , aber in beiden Fällen führen Sie eine Art "Listen von Interesse". Denken Sie jedoch daran, dass es kein Problem sein sollte, die Entitätsverarbeitungsmethoden des Systems zu verbessern, wenn Ihre Framerate aufgrund dieser Abstraktionen so stark abstrahiert bleibt. Wählen Sie daher zunächst die Methode, die für Sie am einfachsten zu codieren ist dann profilieren und bei Bedarf verbessern.

Patryk Czachurski
quelle
5

Es gibt einen Ansatz, der erwägenswert ist, wo jedes System die mit sich selbst verbundenen Komponenten besitzt und die Entitäten nur auf sie verweisen. Grundsätzlich Entitysieht Ihre (vereinfachte) Klasse folgendermaßen aus:

class Entity {
  std::map<ComponentType, Component*> components;
};

Wenn Sie sagen, dass eine RigidBodyKomponente an eine angehängt ist Entity, fordern Sie sie von Ihrem PhysicsSystem an. Das System erstellt die Komponente und lässt die Entität einen Zeiger darauf behalten. Ihr System sieht dann so aus:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Dies mag zunächst etwas kontraintuitiv aussehen, aber der Vorteil liegt in der Art und Weise, wie Komponentenentitätssysteme ihren Status aktualisieren. Oft durchlaufen Sie Ihre Systeme und fordern sie auf, die zugehörigen Komponenten zu aktualisieren

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

Die Stärke, alle Komponenten des Systems im zusammenhängenden Speicher zu haben, besteht darin, dass Ihr System, wenn es über jede Komponente iteriert und diese aktualisiert, im Grunde nur noch etwas tun muss

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

Es muss nicht über alle Entitäten iterieren, die möglicherweise keine Komponente haben, die aktualisiert werden muss, und es kann auch zu einer sehr guten Cache-Leistung führen, da alle Komponenten zusammenhängend gespeichert werden. Dies ist einer, wenn nicht der größte Vorteil dieser Methode. Sie haben oft Hunderte und Tausende von Komponenten gleichzeitig und können genauso gut versuchen, so leistungsfähig wie möglich zu sein.

An diesem Punkt Worlddurchlaufen Sie nur die Systeme und rufen sie updateauf, ohne dass Sie auch Entitäten iterieren müssen. Es ist (imho) besseres Design, weil dann die Verantwortlichkeiten der Systeme viel klarer sind.

Natürlich gibt es eine Vielzahl solcher Designs, daher müssen Sie die Anforderungen Ihres Spiels sorgfältig abwägen und das am besten geeignete auswählen, aber wie wir hier sehen können, können manchmal die kleinen Designdetails einen Unterschied machen.

pwny
quelle
gute antwort, danke. Komponenten haben jedoch keine Funktionen (wie update ()), sondern nur Daten. und das System verarbeitet diese Daten. Nach Ihrem Beispiel sollte ich also ein virtuelles Update für die Komponentenklasse und einen Zeiger der Entität für jede Komponente hinzufügen. Stimmt das?
Deniz
@deniz Es hängt alles von Ihrem Design ab. Wenn Ihre Komponenten keine Methoden, sondern nur Daten haben, kann das System diese weiterhin durchlaufen und die erforderlichen Aktionen ausführen. Ja, Sie können einen Zeiger auf die Eigentümerentität in der Komponente selbst speichern oder Ihr System eine Zuordnung zwischen Komponentenhandles und Entitäten verwalten lassen. In der Regel möchten Sie jedoch, dass Ihre Komponenten so eigenständig wie möglich sind. Eine Komponente, die überhaupt nichts über die übergeordnete Entität weiß, ist ideal. Wenn Sie Kommunikation in diese Richtung benötigen, bevorzugen Sie Ereignisse und dergleichen.
Pwny
Wenn Sie sagen, dass es für die Effizienz besser ist, werde ich Ihr Muster verwenden.
Deniz
@deniz Stellen Sie sicher, dass Sie Ihren Code tatsächlich früh und häufig profilieren, um festzustellen, was für Ihren speziellen
Motor
Okay :) Ich werde ein bisschen Stresstest machen
Deniz
1

Meiner Meinung nach besteht eine gute Architektur darin, eine Komponentenschicht in den Entitäten zu erstellen und die Verwaltung jedes Systems in dieser Komponentenschicht zu trennen. Das Logiksystem verfügt beispielsweise über einige Logikkomponenten, die sich auf ihre Entität auswirken, und speichert die gemeinsamen Attribute, die für alle Komponenten in der Entität gemeinsam genutzt werden.

Wenn Sie danach die Objekte jedes Systems an verschiedenen Punkten oder in einer bestimmten Reihenfolge verwalten möchten, ist es besser, eine Liste der aktiven Komponenten in jedem System zu erstellen. Alle Listen von Zeigern, die Sie in den Systemen erstellen und verwalten können, sind weniger als eine geladene Ressource.

Superarce
quelle