OOP ECS gegen Pure ECS


11

Erstens ist mir bewusst, dass diese Frage mit dem Thema Spieleentwicklung zusammenhängt, aber ich habe mich entschlossen, sie hier zu stellen, da es sich wirklich um ein allgemeineres Problem der Softwareentwicklung handelt.

Im letzten Monat habe ich viel über Entity-Component-Systeme gelesen und bin jetzt mit dem Konzept ziemlich vertraut. Es gibt jedoch einen Aspekt, dem eine klare „Definition“ zu fehlen scheint, und verschiedene Artikel haben radikal unterschiedliche Lösungen vorgeschlagen:

Dies ist die Frage, ob ein ECS die Kapselung unterbrechen soll oder nicht. Mit anderen Worten, es ist das ECS im OOP-Stil (Komponenten sind Objekte mit Status und Verhalten, die die für sie spezifischen Daten kapseln) im Vergleich zum reinen ECS (Komponenten sind Strukturen im C-Stil, bei denen nur öffentliche Daten und Systeme die Funktionalität bereitstellen).

Beachten Sie, dass ich ein Framework / API / Engine entwickle. Das Ziel ist also, dass es leicht von jedem erweitert werden kann, der es verwendet. Dies beinhaltet Dinge wie das Hinzufügen eines neuen Typs von Render- oder Kollisionskomponente.

Probleme mit dem OOP-Ansatz

  • Komponenten müssen auf Daten anderer Komponenten zugreifen. Beispielsweise muss die Zeichenmethode der Renderkomponente auf die Position der Transformationskomponente zugreifen. Dies erzeugt Abhängigkeiten im Code.

  • Komponenten können polymorph sein, was eine gewisse Komplexität mit sich bringt. Beispiel: Möglicherweise gibt es eine Sprite-Renderkomponente, die die virtuelle Zeichenmethode der Renderkomponente überschreibt.

Probleme mit dem reinen Ansatz

  • Da das polymorphe Verhalten (z. B. zum Rendern) irgendwo implementiert werden muss, wird es nur in die Systeme ausgelagert. (z. B. erstellt das Sprite-Render-System einen Sprite-Render-Knoten, der den Render-Knoten erbt und zur Render-Engine hinzufügt.)

  • Die Kommunikation zwischen Systemen kann schwierig zu vermeiden sein. Beispielsweise benötigt das Kollisionssystem möglicherweise den Begrenzungsrahmen, der aus der konkreten Putzkomponente berechnet wird. Dies kann gelöst werden, indem sie über Daten kommunizieren. Dadurch werden jedoch sofortige Aktualisierungen entfernt, da das Render-System die Begrenzungsrahmenkomponente aktualisieren und das Kollisionssystem sie dann verwenden würde. Dies kann zu Problemen führen, wenn die Reihenfolge des Aufrufs der Aktualisierungsfunktionen des Systems nicht definiert ist. Es gibt ein Ereignissystem, mit dem Systeme Ereignisse auslösen können, die andere Systeme ihren Handlern abonnieren können. Dies funktioniert jedoch nur, um Systemen mitzuteilen, was zu tun ist, dh Funktionen zu stornieren.

  • Es werden zusätzliche Flags benötigt. Nehmen Sie zum Beispiel eine Kachelkartenkomponente. Es hätte ein Feld für Größe, Kachelgröße und Indexliste. Das Kachelkartensystem würde das jeweilige Scheitelpunktarray verarbeiten und die Texturkoordinaten basierend auf den Daten der Komponente zuweisen. Die Neuberechnung der gesamten Tilemap für jeden Frame ist jedoch teuer. Daher wäre eine Liste erforderlich, um alle Änderungen zu verfolgen, die vorgenommen wurden, um sie dann im System zu aktualisieren. Auf OOP-Weise könnte dies durch die Kachelzuordnungskomponente gekapselt werden. Beispielsweise würde die SetTile () -Methode das Vertex-Array bei jedem Aufruf aktualisieren.

Obwohl ich die Schönheit des reinen Ansatzes sehe, verstehe ich nicht wirklich, welche konkreten Vorteile er gegenüber einem traditionelleren OOP hätte. Die Abhängigkeiten zwischen Komponenten bestehen weiterhin, obwohl sie in den Systemen verborgen sind. Außerdem würde ich viel mehr Klassen brauchen, um das gleiche Ziel zu erreichen. Dies scheint mir eine etwas überentwickelte Lösung zu sein, die niemals gut ist.

Darüber hinaus bin ich nicht so an Leistung interessiert, so dass diese ganze Idee von datenorientiertem Design und Fehlschlägen für mich nicht wirklich wichtig ist. Ich will nur eine schöne Architektur ^^

Dennoch schlagen die meisten Artikel und Diskussionen, die ich lese, den zweiten Ansatz vor. WARUM?

Animation

Zuletzt möchte ich die Frage stellen, wie ich mit Animation in einem reinen ECS umgehen würde. Derzeit habe ich eine Animation als Funktor definiert, der eine Entität basierend auf einem Fortschritt zwischen 0 und 1 manipuliert. Die Animationskomponente enthält eine Liste von Animatoren mit einer Liste von Animationen. In seiner Aktualisierungsfunktion wendet es dann alle Animationen an, die derzeit auf die Entität aktiv sind.

Hinweis:

Ich habe gerade diesen Beitrag gelesen. Ist das Objekt der Entity Component System-Architektur per Definition orientiert? Das erklärt das Problem etwas besser als ich. Obwohl es sich im Grunde genommen um dasselbe Thema handelt, gibt es immer noch keine Antworten darauf , warum der Ansatz der reinen Daten besser ist.


1
Vielleicht eine einfache, aber ernste Frage: Kennen Sie die Vor- und Nachteile von ECS? Das erklärt meistens das "Warum".
Caramiriel

Nun, ich verstehe den Vorteil der Verwendung von Komponenten, dh Zusammensetzung anstelle von Vererbung, um den Diamanten des Todes durch Mehrfachvererbung usw. zu vermeiden. Die Verwendung von Komponenten ermöglicht auch die Manipulation des Verhaltens zur Laufzeit. Und sie sind modular aufgebaut. Was ich nicht verstehe, ist, warum das Teilen von Daten und Funktionen gewünscht wird. Meine aktuelle Implementierung ist auf github github.com/AdrianKoch3010/MarsBaseProject
Adrian Koch

Nun, ich habe nicht genug Erfahrung mit ECS, um eine vollständige Antwort hinzuzufügen. Komposition wird jedoch nicht nur verwendet, um das DoD zu vermeiden. Sie können zur Laufzeit auch (eindeutige) Entitäten erstellen, die mit einem OO-Ansatz nur schwer zu generieren sind. Das Aufteilen von Daten / Prozeduren ermöglicht es jedoch, Daten leichter zu begründen. Sie können auf einfache Weise Serialisierung, Speichern des Status, Rückgängigmachen / Wiederherstellen und ähnliches implementieren. Da es einfach ist, über Daten nachzudenken, ist es auch einfacher, sie zu optimieren. Sie können die Entitäten höchstwahrscheinlich in Stapel aufteilen (Multithreading) oder sie sogar auf andere Hardware auslagern, um ihr volles Potenzial auszuschöpfen.
Caramiriel

"Möglicherweise gibt es eine Sprite-Renderkomponente, die die virtuelle Zeichenmethode der Renderkomponente überschreibt." Ich würde argumentieren, dass dies kein ECS mehr ist, wenn Sie dies tun / verlangen.
Wondra

Antworten:


10

Dies ist eine schwierige Frage. Ich werde nur versuchen, einige der Fragen zu beantworten, die auf meinen besonderen Erfahrungen (YMMV) beruhen:

Komponenten müssen auf Daten anderer Komponenten zugreifen. Beispielsweise muss die Zeichenmethode der Renderkomponente auf die Position der Transformationskomponente zugreifen. Dies erzeugt Abhängigkeiten im Code.

Unterschätzen Sie hier nicht die Menge und Komplexität (nicht den Grad) der Kopplung / Abhängigkeiten. Sie könnten den Unterschied zwischen diesen betrachten (und dieses Diagramm ist bereits lächerlich auf spielzeugähnliche Ebenen vereinfacht, und das reale Beispiel hätte dazwischen Schnittstellen, um die Kupplung zu lösen):

Geben Sie hier die Bildbeschreibung ein

... und das:

Geben Sie hier die Bildbeschreibung ein

... oder dieses:

Geben Sie hier die Bildbeschreibung ein

Komponenten können polymorph sein, was eine gewisse Komplexität mit sich bringt. Beispiel: Möglicherweise gibt es eine Sprite-Renderkomponente, die die virtuelle Zeichenmethode der Renderkomponente überschreibt.

So? Das analoge (oder wörtliche) Äquivalent einer vtable und eines virtuellen Versands kann über das System aufgerufen werden, anstatt dass das Objekt seinen zugrunde liegenden Status / seine zugrunde liegenden Daten verbirgt. Polymorphismus ist mit der "reinen" ECS-Implementierung immer noch sehr praktisch und machbar, wenn die analogen vtable- oder Funktionszeiger in "Daten" umgewandelt werden, die das System aufrufen kann.

Da das polymorphe Verhalten (z. B. zum Rendern) irgendwo implementiert werden muss, wird es nur in die Systeme ausgelagert. (z. B. erstellt das Sprite-Render-System einen Sprite-Render-Knoten, der den Render-Knoten erbt und zur Render-Engine hinzufügt.)

So? Ich hoffe, dass dies nicht als Sarkasmus abläuft (nicht meine Absicht, obwohl ich oft beschuldigt wurde, aber ich wünschte, ich könnte Emotionen besser durch Text kommunizieren), aber das "Outsourcing" von polymorphem Verhalten führt in diesem Fall nicht unbedingt zu einem zusätzlichen Kosten für die Produktivität.

Die Kommunikation zwischen Systemen kann schwierig zu vermeiden sein. Beispielsweise benötigt das Kollisionssystem möglicherweise den Begrenzungsrahmen, der aus der konkreten Putzkomponente berechnet wird.

Dieses Beispiel erscheint mir besonders seltsam. Ich weiß nicht, warum ein Renderer Daten an die Szene zurückgibt (ich betrachte Renderer in diesem Zusammenhang im Allgemeinen als schreibgeschützt) oder dass ein Renderer AABBs anstelle eines anderen Systems herausfindet, um dies sowohl für den Renderer als auch für den Renderer zu tun Kollision / Physik (ich könnte hier auf den Namen "Renderkomponente" hängen bleiben). Ich möchte mich jedoch nicht zu sehr auf dieses Beispiel einlassen, da mir klar ist, dass dies nicht der Punkt ist, den Sie anstreben. Dennoch sollte die Kommunikation zwischen Systemen (auch in indirekter Form des Lesens / Schreibens in die zentrale ECS-Datenbank mit Systemen, die eher direkt von Transformationen anderer abhängen) nicht häufig sein, wenn überhaupt notwendig. Das'

Dies kann zu Problemen führen, wenn die Reihenfolge des Aufrufs der Aktualisierungsfunktionen des Systems nicht definiert ist.

Dies sollte unbedingt definiert werden. Das ECS ist nicht die Komplettlösung, um die Bewertungsreihenfolge der Systemverarbeitung für jedes mögliche System in der Codebasis neu zu ordnen und dem Endbenutzer, der sich mit Frames und FPS befasst, genau die gleichen Ergebnisse zurückzugeben. Dies ist eines der Dinge, die ich beim Entwerfen eines ECS zumindest dringend im Voraus erwarten sollte (obwohl mit viel verzeihendem Freiraum, um später die Meinung zu ändern, vorausgesetzt, es ändert nicht die kritischsten Aspekte der Reihenfolge von Systemaufruf / Auswertung).

Die Neuberechnung der gesamten Tilemap für jeden Frame ist jedoch teuer. Daher wäre eine Liste erforderlich, um alle Änderungen zu verfolgen, die vorgenommen wurden, um sie dann im System zu aktualisieren. Auf OOP-Weise könnte dies durch die Kachelzuordnungskomponente gekapselt werden. Beispielsweise würde die SetTile () -Methode das Vertex-Array bei jedem Aufruf aktualisieren.

Ich habe dieses Problem nicht ganz verstanden, außer dass es ein datenorientiertes Problem ist. Und es gibt keine Fallstricke bei der Darstellung und Speicherung von Daten in einem ECS, einschließlich Memoisierung, um solche Leistungsprobleme zu vermeiden (die größten mit einem ECS beziehen sich in der Regel auf Dinge wie Systeme, die nach verfügbaren Instanzen bestimmter Komponententypen abfragen, was einer der Gründe ist schwierigste Aspekte bei der Optimierung eines verallgemeinerten ECS). Die Tatsache, dass Logik und Daten in einem "reinen" ECS getrennt sind, bedeutet nicht, dass Sie plötzlich Dinge neu berechnen müssen, die Sie sonst in einer OOP-Darstellung zwischengespeichert / gespeichert hätten. Das ist ein strittiger / irrelevanter Punkt, es sei denn, ich habe etwas sehr Wichtiges beschönigt.

Mit dem "reinen" ECS können Sie diese Daten weiterhin in der Kachelkartenkomponente speichern. Der einzige große Unterschied besteht darin, dass die Logik zum Aktualisieren dieses Scheitelpunktarrays irgendwo auf ein System verschoben wird.

Sie können sich sogar auf das ECS stützen, um die Ungültigmachung und das Entfernen dieses Caches aus der Entität zu vereinfachen, wenn Sie eine separate Komponente wie erstellen TileMapCache. An dem Punkt, an dem der Cache gewünscht, aber in einer Entität mit einer TileMapKomponente nicht verfügbar ist , können Sie ihn berechnen und hinzufügen. Wenn es ungültig ist oder nicht mehr benötigt wird, können Sie es über das ECS entfernen, ohne mehr Code speziell für eine solche Ungültigmachung und Entfernung schreiben zu müssen.

Die Abhängigkeiten zwischen Komponenten bestehen weiterhin, obwohl sie in den Systemen verborgen sind

Es gibt keine Abhängigkeit zwischen Komponenten in einem "reinen" Repräsentanten (ich denke nicht, dass es ganz richtig ist zu sagen, dass Abhängigkeiten hier von den Systemen verborgen werden). Daten hängen sozusagen nicht von Daten ab. Logik hängt von Logik ab. Und ein "reines" ECS tendiert dazu, die zu schreibende Logik so zu fördern, dass sie von der absolut minimalen Teilmenge von Daten und Logik abhängt (oft keine), die ein System benötigt, um zu funktionieren, was im Gegensatz zu vielen Alternativen, die oft abhängig von fördern, ist weitaus mehr Funktionalität als für die eigentliche Aufgabe erforderlich. Wenn Sie das reine ECS-Recht verwenden, sollten Sie als Erstes die Entkopplungsvorteile schätzen und gleichzeitig alles in Frage stellen, was Sie jemals in OOP über Kapselung und spezifisches Verstecken von Informationen zu schätzen gelernt haben.

Mit Entkopplung meine ich insbesondere, wie wenig Informationen Ihre Systeme benötigen, um zu funktionieren. Ihr Bewegungssystem muss nicht einmal über etwas viel Komplexeres wie ein Particleoder Bescheid wissen Character(der Entwickler des Systems muss nicht unbedingt wissen, dass solche Entitätsideen überhaupt im System vorhanden sind). Es muss nur die absoluten Mindestdaten wie eine Positionskomponente kennen, die so einfach sein kann wie ein paar Gleitkommazahlen in einer Struktur. Es sind noch weniger Informationen und weniger externe Abhängigkeiten als das, was eine reine Schnittstelle IMotionmit sich bringt. Dies ist in erster Linie auf dieses minimale Wissen zurückzuführen, das jedes System benötigt, um zu arbeiten, was es dem ECS oft so verzeiht, im Nachhinein mit sehr unerwarteten Designänderungen umzugehen, ohne dass überall kaskadierende Schnittstellenbrüche auftreten.

Der von Ihnen vorgeschlagene "unreine" Ansatz verringert diesen Vorteil etwas, da Ihre Logik jetzt nicht ausschließlich auf Systeme beschränkt ist, bei denen Änderungen keine kaskadierenden Brüche verursachen. Die Logik würde nun zu einem gewissen Grad in den Komponenten zentralisiert, auf die mehrere Systeme zugreifen, die nun die Schnittstellenanforderungen aller verschiedenen Systeme erfüllen müssen, die sie verwenden könnten, und jetzt ist es so, als müsste jedes System über Kenntnisse von (abhängig von) mehr verfügen Informationen, als es unbedingt erforderlich ist, um mit dieser Komponente zu arbeiten.

Abhängigkeiten zu Daten

Eines der Dinge, die am ECS umstritten sind, ist, dass es dazu neigt, Abhängigkeiten von abstrakten Schnittstellen durch reine Rohdaten zu ersetzen, und dies wird im Allgemeinen als weniger wünschenswerte und engere Form der Kopplung angesehen. In Bereichen wie Spielen, in denen ECS sehr nützlich sein kann, ist es jedoch oft einfacher, die Datendarstellung im Voraus zu entwerfen und stabil zu halten, als zu entwerfen, was Sie mit diesen Daten auf einer zentralen Ebene des Systems tun können. Das habe ich selbst unter erfahrenen Veteranen in Codebasen schmerzlich beobachtet, die eher einen reinen Schnittstellenansatz im COM-Stil mit Dingen wie verwenden IMotion.

Die Entwickler fanden immer wieder Gründe, dieser zentralen Schnittstelle Funktionen hinzuzufügen, zu entfernen oder zu ändern, und jede Änderung war furchtbar und kostspielig, da sie dazu neigte, jede einzelne Klasse, die implementiert wurde, IMotionzusammen mit jedem seither im verwendeten System zu beschädigen IMotion. Während der gesamten Zeit mit so vielen schmerzhaften und kaskadierenden Änderungen IMotionspeicherten die implementierten Objekte nur eine 4x4-Matrix von Floats, und die gesamte Benutzeroberfläche befasste sich nur mit der Transformation und dem Zugriff auf diese Floats. Die Datendarstellung war von Anfang an stabil, und es hätte viel Schmerz vermieden werden können, wenn diese zentralisierte Schnittstelle, die sich aufgrund unerwarteter Designanforderungen ändern könnte, überhaupt nicht vorhanden gewesen wäre.

Dies alles könnte fast so ekelhaft klingen wie globale Variablen, aber die Art und Weise, wie das ECS diese Daten in Komponenten organisiert, die explizit nach Typ durch Systeme abgerufen werden, macht es so, während Compiler nichts wie das Verstecken von Informationen erzwingen können, die Orte, die darauf zugreifen und mutieren Die Daten sind im Allgemeinen sehr explizit und offensichtlich genug, um Invarianten effektiv beizubehalten und vorherzusagen, welche Art von Transformationen und Nebenwirkungen von einem System zum nächsten ablaufen (tatsächlich auf eine Weise, die in bestimmten Bereichen möglicherweise einfacher und vorhersehbarer ist als OOP, wenn man bedenkt, wie Das System verwandelt sich in eine flache Pipeline.

Geben Sie hier die Bildbeschreibung ein

Zuletzt möchte ich die Frage stellen, wie ich mit Animation in einem reinen ECS umgehen würde. Derzeit habe ich eine Animation als Funktor definiert, der eine Entität basierend auf einem Fortschritt zwischen 0 und 1 manipuliert. Die Animationskomponente enthält eine Liste von Animatoren mit einer Liste von Animationen. In seiner Aktualisierungsfunktion wendet es dann alle Animationen an, die derzeit auf die Entität aktiv sind.

Wir sind alle Pragmatiker hier. Selbst in Gamedev werden Sie wahrscheinlich widersprüchliche Ideen / Antworten erhalten. Selbst das reinste ECS ist ein relativ neues Phänomen, ein Pioniergebiet, für das die Menschen nicht unbedingt die stärksten Meinungen darüber formuliert haben, wie man Katzen häutet. Meine Bauchreaktion ist ein Animationssystem, das diese Art von Animationsfortschritt in animierten Komponenten erhöht, damit das Rendering-System angezeigt wird, aber das ignoriert so viele Nuancen für die jeweilige Anwendung und den jeweiligen Kontext.

Mit dem ECS ist es kein Wundermittel, und ich habe immer noch die Tendenz, neue Systeme hinzuzufügen, einige zu entfernen, neue Komponenten hinzuzufügen, ein vorhandenes System zu ändern, um diesen neuen Komponententyp aufzunehmen usw. Ich verstehe nicht die Dinge beim ersten Mal überhaupt noch richtig. Der Unterschied in meinem Fall besteht jedoch darin, dass ich nichts Zentrales ändere, wenn ich bestimmte Designanforderungen nicht im Voraus antizipiere. Ich bekomme nicht den Welleneffekt von kaskadierenden Brüchen, bei denen ich überall hingehen und so viel Code ändern muss, um neue Bedürfnisse zu bewältigen, die auftauchen, und das spart Zeit. Ich finde es auch einfacher für mein Gehirn, weil ich, wenn ich mich mit einem bestimmten System hinsetze, nicht so viel über irgendetwas anderes wissen / merken muss als über die relevanten Komponenten (die nur Daten sind), um daran zu arbeiten.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.