Wann / wo Komponenten aktualisiert werden sollen

10

Anstelle meiner üblichen vererbungslastigen Game-Engines spiele ich mit einem komponentenbasierten Ansatz. Es fällt mir jedoch schwer zu rechtfertigen, wo die Komponenten ihr Ding machen sollen.

Angenommen, ich habe eine einfache Entität mit einer Liste von Komponenten. Natürlich weiß die Entität nicht, was diese Komponenten sind. Möglicherweise ist eine Komponente vorhanden, die der Entität eine Position auf dem Bildschirm gibt. Eine andere Komponente ist möglicherweise vorhanden, um die Entität auf dem Bildschirm zu zeichnen.

Damit diese Komponenten funktionieren, müssen sie jeden Frame aktualisieren. Der einfachste Weg, dies zu tun, besteht darin, über den Szenenbaum zu gehen und dann für jede Entität jede Komponente zu aktualisieren. Einige Komponenten benötigen jedoch möglicherweise etwas mehr Verwaltung. Beispielsweise muss eine Komponente, die eine Entität kollidierbar macht, von etwas verwaltet werden, das alle kollidierbaren Komponenten überwachen kann. Eine Komponente, die eine Entität zeichnbar macht, benötigt jemanden, der alle anderen zeichnbaren Komponenten überwacht, um die Zeichenreihenfolge usw. herauszufinden.

Meine Frage ist also, wo ich die Komponenten aktualisiere und wie ich sie auf saubere Weise zu den Managern bringen kann.

Ich habe darüber nachgedacht, ein Singleton-Manager-Objekt für jeden der Komponententypen zu verwenden, aber das hat die üblichen Nachteile der Verwendung eines Singletons. Eine Möglichkeit, dies zu mildern, ist die Verwendung der Abhängigkeitsinjektion, aber das klingt nach einem Overkill für dieses Problem. Ich könnte auch über den Szenenbaum gehen und dann die verschiedenen Komponenten unter Verwendung eines Beobachtermusters in Listen zusammenfassen, aber das scheint ein bisschen verschwenderisch zu sein, um jedes Bild zu machen.

Roy T.
quelle
1
Verwenden Sie Systeme in irgendeiner Weise?
Asakeron
Komponentensysteme sind der übliche Weg, dies zu tun. Ich persönlich rufe nur update für alle Entitäten auf, wodurch update für alle Komponenten aufgerufen wird, und habe einige "Sonderfälle" (wie den räumlichen Manager für die Kollisionserkennung, der statisch ist).
Asche999
Komponentensysteme? Davon habe ich noch nie gehört. Ich werde mit dem Googeln beginnen, aber ich würde alle empfohlenen Links begrüßen.
Roy T.
1
Entity Systems sind die Zukunft der MMOG-Entwicklung ist eine große Ressource. Und um ehrlich zu sein, bin ich immer verwirrt von diesen Architekturnamen. Der Unterschied zum vorgeschlagenen Ansatz besteht darin, dass Komponenten nur Daten enthalten und Systeme darauf zugreifen. Diese Antwort ist ebenfalls sehr relevant.
Asakeron
1
Ich habe hier eine Art mäanderförmigen Blog-Beitrag zu diesem Thema geschrieben: gamedevrubberduck.wordpress.com/2012/12/26/…
AlexFoxGill

Antworten:

15

Ich würde vorschlagen, zunächst die drei großen Lügen von Mike Acton zu lesen, da Sie zwei davon verletzen. Ich meine es ernst, dies wird die Art und Weise ändern, wie Sie Ihren Code entwerfen: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

Also, gegen was verstößt du?

Lüge Nr. 3 - Code ist wichtiger als Daten

Sie sprechen von Abhängigkeitsinjektion, die in einigen (und nur in einigen) Fällen nützlich sein kann, aber immer eine große Alarmglocke läuten sollte, wenn Sie sie verwenden, insbesondere in der Spieleentwicklung! Warum? Weil es eine oft unnötige Abstraktion ist. Und Abstraktionen an den falschen Stellen sind schrecklich. Du hast also ein Spiel. Das Spiel hat Manager für verschiedene Komponenten. Die Komponenten sind alle definiert. Machen Sie also irgendwo in Ihrem Hauptspiel-Loop-Code eine Klasse, die die Manager "hat". Mögen:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

Geben Sie ihm einige Getter-Funktionen, um jede Manager-Klasse abzurufen (getBulletManager ()). Vielleicht ist diese Klasse selbst ein Singleton oder von einem aus erreichbar (wahrscheinlich haben Sie sowieso irgendwo einen zentralen Game-Singleton). An gut definierten, fest codierten Daten und Verhaltensweisen ist nichts auszusetzen.

Erstellen Sie keinen ManagerManager, mit dem Sie Manager mithilfe eines Schlüssels registrieren können, der von anderen Klassen, die den Manager verwenden möchten, mit diesem Schlüssel abgerufen werden kann. Es ist ein großartiges System und sehr flexibel, aber hier wird über ein Spiel gesprochen. Sie wissen genau, welche Systeme im Spiel sind. Warum so tun, als ob du es nicht tust? Weil dies ein System für Leute ist, die denken, dass Code wichtiger ist als Daten. Sie werden sagen "Der Code ist flexibel, die Daten füllen ihn nur". Aber Code ist nur Daten. Das von mir beschriebene System ist viel einfacher, zuverlässiger, einfacher zu warten und viel flexibler (wenn sich beispielsweise das Verhalten eines Managers von anderen Managern unterscheidet, müssen Sie nur einige Zeilen ändern, anstatt das gesamte System zu überarbeiten).

Lüge Nr. 2 - Code sollte um ein Modell der Welt herum entworfen werden

Sie haben also eine Einheit in der Spielwelt. Die Entität verfügt über eine Reihe von Komponenten, die ihr Verhalten definieren. Sie erstellen also eine Entitätsklasse mit einer Liste von Komponentenobjekten und einer Update () - Funktion, die die Update () - Funktion jeder Komponente aufruft. Recht?

Nein :) Das ist ein Entwurf für ein Modell der Welt: Sie haben eine Kugel in Ihrem Spiel, also fügen Sie eine Kugelklasse hinzu. Dann aktualisieren Sie jedes Bullet und fahren mit dem nächsten fort. Dies wird Ihre Leistung absolut beeinträchtigen und Ihnen eine schrecklich verschlungene Codebasis mit doppeltem Code überall und ohne logische Strukturierung ähnlichen Codes geben. (Schauen Sie sich meine Antwort hier an, um eine detailliertere Erklärung zu erhalten, warum traditionelles OO-Design scheiße ist, oder lesen Sie datenorientiertes Design nach.)

Werfen wir einen Blick auf die Situation ohne unsere OO-Tendenz. Wir wollen Folgendes, nicht mehr und nicht weniger (bitte beachten Sie, dass keine Klasse für Entität oder Objekt erstellt werden muss):

  • Sie haben eine Reihe von Entitäten
  • Entitäten bestehen aus einer Reihe von Komponenten, die das Verhalten der Entität definieren
  • Sie möchten jede Komponente im Spiel in jedem Frame aktualisieren, vorzugsweise auf kontrollierte Weise
  • Abgesehen davon, dass Komponenten als zusammengehörig identifiziert werden, muss die Entität selbst nichts tun. Es ist ein Link / eine ID für einige Komponenten.

Und schauen wir uns die Situation an. Ihr Komponentensystem aktualisiert das Verhalten jedes Objekts im Spiel in jedem Frame. Dies ist definitiv ein kritisches System Ihres Motors. Leistung ist hier wichtig!

Wenn Sie mit Computerarchitektur oder datenorientiertem Design vertraut sind, wissen Sie, wie die beste Leistung erzielt wird: dicht gepackter Speicher und Gruppierung der Codeausführung. Wenn Sie Codefragmente von Code A, B und C wie folgt ausführen: ABCABCABC, erhalten Sie nicht die gleiche Leistung wie bei der Ausführung wie folgt: AAABBBCCC. Dies liegt nicht nur daran, dass der Befehls- und Datencache effizienter verwendet wird, sondern auch daran, dass, wenn Sie alle "A" nacheinander ausführen, viel Raum für Optimierungen besteht: Entfernen von doppeltem Code, Vorberechnung von Daten, die von verwendet werden alle "A" usw.

Wenn wir also alle Komponenten aktualisieren möchten, sollten wir sie nicht zu Klassen / Objekten mit einer Aktualisierungsfunktion machen. Rufen wir diese Aktualisierungsfunktion nicht für jede Komponente in jeder Entität auf. Das ist die "ABCABCABC" -Lösung. Lassen Sie uns alle identischen Komponentenaktualisierungen zusammenfassen. Dann können wir alle A-Komponenten aktualisieren, gefolgt von B usw. Was brauchen wir, um dies zu machen?

Erstens brauchen wir Komponentenmanager. Für jede Art von Komponente im Spiel benötigen wir eine Managerklasse. Es verfügt über eine Aktualisierungsfunktion, mit der alle Komponenten dieses Typs aktualisiert werden. Es verfügt über eine Erstellungsfunktion, die eine neue Komponente dieses Typs hinzufügt, und eine Entfernungsfunktion, die die angegebene Komponente zerstört. Möglicherweise gibt es andere Hilfsfunktionen zum Abrufen und Festlegen von Daten, die für diese Komponente spezifisch sind (z. B. Festlegen des 3D-Modells für die Modellkomponente). Beachten Sie, dass der Manager in gewisser Weise eine Black Box für die Außenwelt ist. Wir wissen nicht, wie die Daten der einzelnen Komponenten gespeichert werden. Wir wissen nicht, wie jede Komponente aktualisiert wird. Es ist uns egal, solange sich die Komponenten so verhalten, wie sie sollten.

Als nächstes brauchen wir eine Entität. Sie könnten dies zu einer Klasse machen, aber das ist kaum notwendig. Eine Entität kann nichts anderes als eine eindeutige Ganzzahl-ID oder eine Hash-Zeichenfolge (also auch eine Ganzzahl) sein. Wenn Sie eine Komponente für die Entität erstellen, übergeben Sie die ID als Argument an den Manager. Wenn Sie die Komponente entfernen möchten, übergeben Sie die ID erneut. Das Hinzufügen von etwas mehr Daten zur Entität kann einige Vorteile haben, anstatt sie nur zu einer ID zu machen. Dies sind jedoch nur Hilfsfunktionen, da, wie in den Anforderungen aufgeführt, das gesamte Verhalten der Entität von den Komponenten selbst definiert wird. Es ist jedoch Ihr Motor, also tun Sie, was für Sie Sinn macht.

Was wir brauchen, ist ein Entity Manager. Diese Klasse generiert entweder eindeutige IDs, wenn Sie die Nur-ID-Lösung verwenden, oder sie kann zum Erstellen / Verwalten von Entitätsobjekten verwendet werden. Es kann auch eine Liste aller Entitäten im Spiel führen, wenn Sie das brauchen. Der Entity Manager kann die zentrale Klasse Ihres Komponentensystems sein. Er speichert die Verweise auf alle ComponentManager in Ihrem Spiel und ruft deren Aktualisierungsfunktionen in der richtigen Reihenfolge auf. Auf diese Weise muss die Spieleschleife nur noch EntityManager.update () aufrufen, und das gesamte System ist gut vom Rest Ihrer Engine getrennt.

Das ist die Vogelperspektive. Schauen wir uns an, wie die Komponentenmanager arbeiten. Folgendes benötigen Sie:

  • Erstellen Sie Komponentendaten, wenn create (entityID) aufgerufen wird
  • Löschen Sie Komponentendaten, wenn remove (entityID) aufgerufen wird
  • Aktualisieren Sie alle (zutreffenden) Komponentendaten, wenn update () aufgerufen wird (dh nicht alle Komponenten müssen jeden Frame aktualisieren).

In der letzten definieren Sie das Verhalten / die Logik der Komponenten und hängen vollständig von der Art der Komponente ab, die Sie schreiben. Die AnimationComponent aktualisiert die Animationsdaten basierend auf dem Frame, in dem sie sich befinden. Die DragableComponent aktualisiert nur eine Komponente, die mit der Maus gezogen wird. Die PhysicsComponent aktualisiert die Daten im Physiksystem. Da Sie jedoch alle Komponenten desselben Typs auf einmal aktualisieren, können Sie einige Optimierungen vornehmen, die nicht möglich sind, wenn jede Komponente ein separates Objekt mit einer Aktualisierungsfunktion ist, die jederzeit aufgerufen werden kann.

Beachten Sie, dass ich noch nie die Erstellung einer XxxComponent-Klasse für die Speicherung von Komponentendaten gefordert habe. Das liegt an dir. Gefällt dir datenorientiertes Design? Strukturieren Sie dann die Daten in separaten Arrays für jede Variable. Magst du objektorientiertes Design? (Ich würde es nicht empfehlen, es wird Ihre Leistung an vielen Stellen immer noch beeinträchtigen.) Erstellen Sie dann ein XxxComponent-Objekt, das die Daten jeder Komponente enthält.

Das Tolle an den Managern ist die Kapselung. Jetzt ist die Kapselung eine der am schrecklichsten missbrauchten Philosophien in der Welt des Programmierens. So sollte es verwendet werden. Nur der Manager weiß, welche Komponentendaten wo gespeichert sind und wie die Logik einer Komponente funktioniert. Es gibt einige Funktionen zum Abrufen / Einstellen von Daten, aber das war's. Sie können den gesamten Manager und die zugrunde liegenden Klassen neu schreiben. Wenn Sie die öffentliche Schnittstelle nicht ändern, bemerkt dies niemand. Geänderte Physik-Engine? Schreiben Sie einfach PhysicsComponentManager neu und Sie sind fertig.

Dann gibt es noch eine letzte Sache: Kommunikation und Datenaustausch zwischen Komponenten. Das ist schwierig und es gibt keine einheitliche Lösung. Sie können in den Managern Funktionen zum Abrufen / Festlegen erstellen, damit beispielsweise die Kollisionskomponente die Position von der Positionskomponente abrufen kann (z. B. PositionManager.getPosition (entityID)). Sie könnten ein Ereignissystem verwenden. Sie könnten einige gemeinsam genutzte Daten in einer Entität speichern (meiner Meinung nach die hässlichste Lösung). Sie können ein Nachrichtensystem verwenden (dies wird häufig verwendet). Oder verwenden Sie eine Kombination aus mehreren Systemen! Ich habe nicht die Zeit oder Erfahrung, um in jedes dieser Systeme einzusteigen, aber Google und Stackoverflow Search sind Ihre Freunde.

Mart
quelle
Ich finde diese Antwort sehr interessant. Nur eine Frage (hoffe, Sie oder jemand kann mir antworten). Wie schaffen Sie es, die Entität auf einem DOD-komponentenbasierten System zu eliminieren? Selbst Artemis benutzt Entity als Klasse, ich bin mir nicht sicher, ob das sehr dumm ist.
Wolfrevo Kcats
1
Was meinst du damit, es zu beseitigen? Meinen Sie ein Entitätssystem ohne Entitätsklasse? Der Grund, warum Artemis eine Entität hat, liegt darin, dass in Artemis die Entitätsklasse ihre eigenen Komponenten verwaltet. In dem von mir vorgeschlagenen System verwalten die ComponentManager-Klassen die Komponenten. Anstatt eine Entity-Klasse zu benötigen, können Sie einfach eine eindeutige Ganzzahl-ID verwenden. Angenommen, Sie haben die Entität 254, die eine Positionskomponente hat. Wenn Sie die Position ändern möchten, können Sie PositionCompMgr.setPosition (int id, Vector3 newPos) mit 254 als ID-Parameter aufrufen.
Mart
Aber wie verwalten Sie IDs? Was ist, wenn Sie eine Komponente aus einer Entität entfernen möchten, um sie später einer anderen zuzuweisen? Was ist, wenn Sie eine Entität entfernen und eine neue hinzufügen möchten? Was ist, wenn eine Komponente von zwei oder mehr Entitäten gemeinsam genutzt werden soll? Das interessiert mich wirklich.
Wolfrevo Kcats
1
Mit EntityManager können neue IDs ausgegeben werden. Es kann auch verwendet werden, um vollständige Entitäten basierend auf vordefinierten Vorlagen zu erstellen (z. B. "EnemyNinja" erstellen, das eine neue ID generiert und alle Komponenten erstellt, aus denen ein feindlicher Ninja besteht, wie z. B. Renderable, Collision, AI, möglicherweise eine Komponente für den Nahkampf , usw). Es kann auch eine removeEntity-Funktion haben, die automatisch alle ComponentManager-Entfernungsfunktionen aufruft. Der ComponentManager kann prüfen, ob Komponentendaten für die angegebene Entität vorhanden sind, und diese Daten in diesem Fall löschen.
Mart
1
Eine Komponente von einer Entität in eine andere verschieben? Fügen Sie einfach jedem ComponentManager eine Funktion swapComponentOwner (int oldEntity, int newEntity) hinzu. Die Daten sind alle in ComponentManager vorhanden. Sie benötigen lediglich eine Funktion, um zu ändern, zu welchem ​​Eigentümer sie gehören. Jeder ComponentManager verfügt über einen Index oder eine Zuordnung, um zu speichern, welche Daten zu welcher Entitäts-ID gehören. Ändern Sie einfach die Entitäts-ID von der alten in die neue ID. Ich bin mir nicht sicher, ob das Teilen von Komponenten in dem System, das ich mir ausgedacht habe, einfach ist, aber wie schwer kann es sein? Anstelle einer Entitäts-ID <-> Komponentendaten-Verknüpfung in der Indextabelle gibt es mehrere.
Mart
3

Damit diese Komponenten funktionieren, müssen sie jeden Frame aktualisieren. Der einfachste Weg, dies zu tun, besteht darin, über den Szenenbaum zu gehen und dann für jede Entität jede Komponente zu aktualisieren.

Dies ist der typische naive Ansatz für Komponentenaktualisierungen (und es ist nicht unbedingt falsch daran, dass er naiv ist, wenn er für Sie funktioniert). Eines der großen Probleme, die Sie tatsächlich angesprochen haben - Sie arbeiten beispielsweise über die Schnittstelle der Komponente, IComponentsodass Sie nichts darüber wissen, was Sie gerade aktualisiert haben. Sie wissen wahrscheinlich auch nichts über die Reihenfolge der Komponenten innerhalb der Entität

  1. Es ist wahrscheinlich, dass Sie Komponenten unterschiedlichen Typs häufig aktualisieren (im Wesentlichen schlechte Codelokalität der Referenz).
  2. Dieses System eignet sich nicht für gleichzeitige Aktualisierungen, da Sie nicht in der Lage sind, Datenabhängigkeiten zu identifizieren und die Aktualisierungen daher in lokale Gruppen nicht verwandter Objekte aufzuteilen.

Ich habe darüber nachgedacht, ein Singleton-Manager-Objekt für jeden der Komponententypen zu verwenden, aber das hat die üblichen Nachteile der Verwendung eines Singletons. Eine Möglichkeit, dies zu mildern, ist die Verwendung der Abhängigkeitsinjektion, aber das klingt nach einem Overkill für dieses Problem.

Ein Singleton wird hier nicht wirklich benötigt, und deshalb sollten Sie ihn vermeiden, da er die von Ihnen erwähnten Nachteile mit sich bringt. Die Abhängigkeitsinjektion ist kein Overkill - das Herzstück des Konzepts ist, dass Sie Dinge, die ein Objekt benötigt, an dieses Objekt übergeben, idealerweise im Konstruktor. Sie benötigen hierfür kein schweres DI-Framework (wie Ninject ) - übergeben Sie einfach irgendwo einen zusätzlichen Parameter an einen Konstruktor.

Ein Renderer ist ein grundlegendes System und unterstützt wahrscheinlich das Erstellen und Verwalten der Lebensdauer einer Reihe von renderbaren Objekten, die visuellen Dingen in Ihrem Spiel entsprechen (Sprites oder Modelle, wahrscheinlich). In ähnlicher Weise hat eine Physik-Engine wahrscheinlich eine lebenslange Kontrolle über Dinge, die die Entitäten darstellen, die sich in der Physik-Simulation bewegen können (starre Körper). Jedes dieser relevanten Systeme sollte diese Objekte in gewisser Weise besitzen und für deren Aktualisierung verantwortlich sein.

Die Komponenten, die Sie in Ihrem Spielentitäts-Kompositionssystem verwenden, sollten nur Wrapper um die Instanzen dieser untergeordneten Systeme sein - Ihre Positionskomponente kann nur einen starren Körper umhüllen, Ihre visuelle Komponente umhüllt nur ein renderbares Sprite oder Modell usw.

Dann ist das System selbst, dem die untergeordneten Objekte gehören, für deren Aktualisierung verantwortlich und kann dies in großen Mengen und auf eine Weise tun, die es ihm ermöglicht, diese Aktualisierung gegebenenfalls zu multithreaden. Ihre Hauptspielschleife steuert die grobe Reihenfolge, in der diese Systeme aktualisiert werden (zuerst Physik, dann Renderer oder was auch immer). Wenn Sie ein Subsystem haben, das keine Lebensdauer- oder Aktualisierungskontrolle über die von ihm ausgegebenen Instanzen hat, können Sie einen einfachen Wrapper erstellen, um die Aktualisierung aller für dieses System relevanten Komponenten auch in großen Mengen durchzuführen, und entscheiden, wo diese platziert werden sollen Aktualisierung relativ zu den übrigen Systemaktualisierungen (dies geschieht häufig bei "Skript" -Komponenten).

Dieser Ansatz wird gelegentlich als Außenbordkomponentenansatz bezeichnet , wenn Sie nach mehr Details suchen.


quelle