Ist es jedoch sinnvoll, Anwendungen auch mit der in Game-Engines üblichen Component-Entity-System-Architektur zu erstellen?
Für mich absolut. Ich arbeite mit Visual FX und habe eine Vielzahl von Systemen in diesem Bereich untersucht, deren Architekturen (einschließlich CAD / CAM), hungrig nach SDKs und Papieren, die mir ein Gefühl für die Vor- und Nachteile der scheinbar unendlichen Architekturentscheidungen geben könnte gemacht werden, wobei selbst die subtilsten nicht immer einen subtilen Einfluss haben.
VFX ist Spielen insofern ziemlich ähnlich, als es ein zentrales Konzept einer "Szene" gibt, mit Ansichtsfenstern, die die gerenderten Ergebnisse anzeigen. Es gibt auch eine Menge zentraler Loop-Prozesse, die sich ständig um diese Szene drehen, und zwar in Animationskontexten, in denen möglicherweise Physik stattfindet, Partikel emittieren, Partikel spawnen, Netze animiert und gerendert werden, Bewegungsanimationen usw. und letztendlich, um sie zu rendern alles an den Benutzer am Ende.
Ein weiteres ähnliches Konzept für zumindest sehr komplexe Spiele-Engines war die Notwendigkeit eines "Designer" -Aspekts, bei dem Designer Szenen flexibel entwerfen konnten, einschließlich der Möglichkeit, eigene leichte Programme (Skripte und Knoten) zu erstellen.
Im Laufe der Jahre fand ich heraus, dass ECS am besten zu mir passte. Natürlich ist das nie vollständig von der Subjektivität getrennt, aber ich würde sagen, es schien die wenigsten Probleme zu geben. Es löste viel größere Probleme, mit denen wir immer zu kämpfen hatten, und gab uns nur ein paar neue kleinere zurück.
Traditionelles OOP
Traditionellere OOP-Ansätze können sehr effektiv sein, wenn Sie die Entwurfsanforderungen im Voraus genau kennen, aber nicht die Implementierungsanforderungen. Ob durch einen flacheren Ansatz mit mehreren Schnittstellen oder einen verschachtelten hierarchischen ABC-Ansatz, er zementiert tendenziell das Design und erschwert Änderungen, während die Implementierung einfacher und sicherer zu ändern ist. Es gibt immer ein Bedürfnis nach Instabilität in jedem Produkt, das über eine einzelne Version hinausgeht. Daher tendieren OOP-Ansätze dazu, die Stabilität (Schwierigkeit der Änderung und Fehlen von Gründen für die Änderung) in Richtung Designebene und Instabilität (Leichtigkeit der Änderung und Gründe für die Änderung) zu verzerren. auf die Implementierungsebene.
Im Gegensatz zu den sich ändernden Anforderungen für Benutzer müssen Design und Implementierung möglicherweise häufig geändert werden. Möglicherweise finden Sie etwas Seltsames wie ein starkes Bedürfnis der Benutzer nach der analogen Kreatur, die gleichzeitig sowohl Pflanze als auch Tier sein muss, was das gesamte von Ihnen erstellte konzeptionelle Modell vollständig ungültig macht. Normale objektorientierte Ansätze schützen Sie hier nicht und können solche unerwarteten, konzeptionellen Änderungen manchmal noch schwieriger machen. Wenn sehr leistungskritische Bereiche betroffen sind, multiplizieren sich die Gründe für das Design weiter.
Das Kombinieren mehrerer granularer Schnittstellen zu einer konformen Schnittstelle eines Objekts kann viel zur Stabilisierung des Clientcodes beitragen, nicht jedoch zur Stabilisierung der Subtypen, die manchmal die Anzahl der Clientabhängigkeiten in den Schatten stellen. Beispielsweise kann eine Schnittstelle nur von einem Teil Ihres Systems verwendet werden, aber mit tausend verschiedenen Subtypen, die diese Schnittstelle implementieren. In diesem Fall kann die Verwaltung der komplexen Subtypen (komplex, weil sie so viele unterschiedliche Schnittstellenverantwortlichkeiten zu erfüllen haben) eher zum Albtraum als zum Code werden, der sie über eine Schnittstelle verwendet. OOP tendiert dazu, Komplexität auf die Objektebene zu übertragen, während ECS sie auf die Clientebene ("Systeme") überträgt. Dies kann ideal sein, wenn es nur sehr wenige Systeme gibt, aber eine ganze Reihe konformer "Objekte" ("Entitäten").
Eine Klasse besitzt ihre Daten auch privat und kann so Invarianten alleine verwalten. Trotzdem gibt es "grobe" Invarianten, die eigentlich immer noch schwer zu pflegen sind, wenn Objekte miteinander interagieren. Damit ein komplexes System als Ganzes in einem gültigen Zustand ist, muss häufig ein komplexes Diagramm von Objekten berücksichtigt werden, auch wenn die einzelnen Invarianten ordnungsgemäß verwaltet werden. Traditionelle Ansätze im OOP-Stil können bei der Aufrechterhaltung granularer Invarianten hilfreich sein, können es jedoch tatsächlich schwierig machen, breite, grobe Invarianten aufrechtzuerhalten, wenn sich die Objekte auf winzige Facetten des Systems konzentrieren.
Das ist der Punkt, an dem solche ECS-Ansätze oder -Varianten zum Aufbau von Legoblöcken hilfreich sein können. Auch wenn Systeme gröber gestaltet sind als das übliche Objekt, ist es einfacher, solche groben Invarianten aus der Vogelperspektive des Systems zu betrachten. Viele Teeny-Objekt-Interaktionen werden zu einem großen System, das sich auf eine große Aufgabe konzentriert, anstatt auf kleine Teeny-Objekte, die sich auf kleine Teeny-Aufgaben mit einem Abhängigkeitsdiagramm konzentrieren, das einen Kilometer Papier abdecken würde.
Trotzdem musste ich mich außerhalb meines Fachgebiets in der Spieleindustrie umsehen, um mehr über ECS zu erfahren, obwohl ich immer eine datenorientierte Denkweise hatte. Lustigerweise habe ich mich auch fast selbstständig auf den Weg zu ECS gemacht, indem ich mich durchgearbeitet und versucht habe, bessere Designs zu finden. Ich habe es jedoch nicht bis zum Ende geschafft und ein sehr wichtiges Detail übersehen, nämlich die Formalisierung des Teils "Systeme" und das Zerquetschen von Komponenten bis hin zu Rohdaten.
Ich werde versuchen, herauszufinden, wie ich mich für ECS entschieden habe und wie ich alle Probleme mit früheren Entwurfsiterationen gelöst habe. Ich denke, das wird helfen, um genau zu verdeutlichen, warum die Antwort hier ein sehr starkes "Ja" sein könnte, dass ECS möglicherweise weit über die Gaming-Branche hinaus anwendbar ist.
Brute-Force-Architektur der 1980er Jahre
Die erste Architektur, an der ich in der VFX-Branche gearbeitet habe, hat ein langes Erbe hinter sich, das bereits ein Jahrzehnt zurückliegt, seit ich in das Unternehmen eingetreten bin. Es war eine rohe C-Codierung mit Brute-Force-Methode (keine Neigung zu C, wie ich C liebe, aber die Art und Weise, wie sie hier verwendet wurde, war wirklich roh). Ein miniaturisiertes und stark vereinfachtes Stück ähnelte den folgenden Abhängigkeiten:
Und dies ist ein enorm vereinfachtes Diagramm eines winzigen Teils des Systems. Jeder dieser Clients im Diagramm ("Rendern", "Physik", "Bewegung") würde ein "generisches" Objekt erhalten, durch das sie ein Typfeld wie folgt prüfen würden:
void transform(struct Object* obj, const float mat[16])
{
switch (obj->type)
{
case camera:
// cast to camera and do something with camera fields
break;
case light:
// cast to light and do something with light fields
break;
...
}
}
Natürlich mit deutlich hässlichem und komplexerem Code. Oft werden aus diesen Schalterfällen zusätzliche Funktionen aufgerufen, die den Schalter immer wieder und immer wieder rekursiv ausführen. Dieses Diagramm und Code aussehen könnten fast wie ECS-lite, aber es gab keine starke Einheit-Komponente Unterscheidung ( „ ist dieses Objekt eine Kamera?“, Nicht ‚dieses Objekt bietet Bewegung?‘), Und keine Formalisierung von ‚System‘ ( nur ein Bündel verschachtelter Funktionen, die überall verteilt sind und Verantwortlichkeiten vertauschen). In diesem Fall war fast alles kompliziert, jede Funktion war ein potenzielles Katastrophenrisiko.
Unsere Testprozedur hier musste oft Dinge wie Maschen, die von anderen Arten von Gegenständen getrennt sind, überprüfen, auch wenn beides identisch war, da die Brute-Force-Natur der hier vorgenommenen Codierung (oft begleitet von viel Kopieren und Einfügen) häufig vorkam Es ist sehr wahrscheinlich, dass ansonsten genau dieselbe Logik von einem Elementtyp zum nächsten fehlschlagen kann. Der Versuch, das System auf neue Arten von Gegenständen auszudehnen, war ziemlich aussichtslos, obwohl es ein stark geäußertes Bedürfnis der Benutzer gab, da es zu schwierig war, mit den vorhandenen Arten von Gegenständen so viel zu kämpfen.
Einige Profis:
- Ähh ... ich nehme an, dass ich keine Ingenieurerfahrung habe? Dieses System erfordert keine Kenntnisse von grundlegenden Konzepten wie Polymorphismus, es ist absolut brachial, so dass selbst Anfänger in der Lage sein könnten, einen Teil des Codes zu verstehen, selbst wenn ein Profi beim Debuggen ihn kaum bewahren kann.
Einige Nachteile:
- Wartungs-Albtraum. Unser Marketing-Team hatte tatsächlich das Bedürfnis, die über 2000 einzigartigen Fehler in einem 3-Jahres-Zyklus zu beheben. Für mich ist es etwas peinlich, dass wir so viele Fehler hatten, und dieser Prozess hat wahrscheinlich immer noch nur etwa 10% der Fehler behoben, deren Anzahl die ganze Zeit gewachsen ist.
- Über die unflexibelste Lösung.
1990er Jahre COM-Architektur
Der Großteil der VFX-Industrie verwendet diesen Architekturstil aus dem, was ich gesammelt habe, um Dokumente über ihre Designentscheidungen zu lesen und einen Blick auf ihre Softwareentwicklungskits zu werfen.
Auf der ABI-Ebene kann es sich nicht unbedingt um COM handeln (einige dieser Architekturen können nur Plugins enthalten, die mit demselben Compiler geschrieben wurden), sie weisen jedoch eine Reihe ähnlicher Merkmale bei Schnittstellenabfragen auf, die für Objekte durchgeführt werden, um festzustellen, welche Schnittstellen ihre Komponenten unterstützen.
Bei diesem Ansatz transform
ähnelte die obige analoge Funktion dieser Form:
void transform(Object obj, const Matrix& mat)
{
// Wrapper that performs an interface query to see if the
// object implements the IMotion interface.
MotionRef motion(obj);
// If the object supported the IMotion interface:
if (motion.valid())
{
// Transform the item through the IMotion interface.
motion->transform(mat);
...
}
}
Dies ist der Ansatz, auf den sich das neue Team dieser alten Codebasis festgelegt hat, um schließlich eine Umgestaltung zu erreichen. Und es war eine dramatische Verbesserung gegenüber dem Original in Bezug auf Flexibilität und Wartbarkeit, aber es gab noch einige Probleme, die ich im nächsten Abschnitt behandeln werde.
Einige Profis:
- Erheblich flexibler / erweiterbarer / wartbarer als die vorherige Brute-Force-Lösung.
- Fördert eine starke Übereinstimmung mit vielen Prinzipien von SOLID, indem jede Schnittstelle vollständig abstrakt gemacht wird (zustandslos, keine Implementierung, nur reine Schnittstellen).
Einige Nachteile:
- Viele Boilerplate. Unsere Komponenten mussten über eine Registrierung veröffentlicht werden, um Objekte zu instanziieren. Die von ihnen unterstützten Schnittstellen mussten sowohl die Schnittstelle erben ("implementieren" "in Java") als auch Code bereitstellen, um anzugeben, welche Schnittstellen in einer Abfrage verfügbar waren.
- Durch die reinen Schnittstellen wurde die doppelte Logik überall gefördert. Beispielsweise hätten alle implementierten Komponenten
IMotion
immer den exakt gleichen Status und die exakt gleiche Implementierung für alle Funktionen. Um dies zu entschärfen, haben wir damit begonnen, Basisklassen und Hilfsfunktionen im gesamten System zu zentralisieren, um sicherzustellen, dass sie auf dieselbe Weise für dieselbe Schnittstelle redundant implementiert werden und möglicherweise mehrere Vererbungen hinter der Haube stattfinden chaotisch unter der Haube, obwohl der Client-Code es einfach hatte.
- Ineffizienz: In vtune-Sitzungen wurde die Grundfunktion häufig
QueryInterface
als mittlerer bis oberer Hotspot und gelegentlich sogar als Hotspot Nr. 1 angezeigt. Um dies zu entschärfen, müssten wir beispielsweise Teile des Codebasis-Cache mit einer Liste von Objekten rendern, von denen bekannt ist, dass sie diese unterstützenIRenderable
Dies erhöhte jedoch die Komplexität und die Wartungskosten erheblich. Dies war ebenfalls schwieriger zu messen, aber wir bemerkten einige deutliche Verlangsamungen im Vergleich zu der C-artigen Codierung, die wir zuvor durchgeführt hatten, als für jede einzelne Schnittstelle ein dynamischer Versand erforderlich war. Dinge wie Verzweigungsfehlvorhersagen und Optimierungsbarrieren sind außerhalb einer kleinen Codefacette schwer zu messen, aber die Benutzer bemerkten im Allgemeinen nur die Reaktionsfähigkeit der Benutzeroberfläche und solche Dinge, die sich verschlechterten, indem sie frühere und neuere Versionen der Software nebeneinander verglichen. Seite für Bereiche, in denen sich die algorithmische Komplexität nicht geändert hat, nur die Konstanten.
- Es war immer noch schwierig, über die Korrektheit auf einer breiteren Systemebene zu urteilen. Obwohl dies erheblich einfacher war als der vorherige Ansatz, war es immer noch schwierig, die komplexen Wechselwirkungen zwischen Objekten in diesem System zu erfassen, insbesondere angesichts einiger Optimierungen, die dagegen erforderlich wurden.
- Wir hatten Probleme, die richtigen Schnittstellen zu finden. Auch wenn es möglicherweise nur eine breite Stelle im System gibt, die eine Schnittstelle verwendet, ändern sich die Anforderungen für das Benutzerende gegenüber den Versionen, und wir müssten am Ende kaskadierende Änderungen an allen Klassen vornehmen, die die Schnittstelle implementieren, um eine neue hinzugefügte Funktion aufzunehmen B. die Schnittstelle, es sei denn, es gab eine abstrakte Basisklasse, die bereits die Logik unter der Haube zentralisierte (einige davon würden sich inmitten dieser kaskadierenden Änderungen in der Hoffnung manifestieren, dies nicht wiederholt und immer wieder zu tun).
Pragmatische Antwort: Zusammensetzung
Eines der Dinge, die wir zuvor bemerkt haben (oder zumindest ich), die Probleme verursacht haben, war, dass sie IMotion
möglicherweise von 100 verschiedenen Klassen implementiert werden, aber mit genau der gleichen Implementierung und dem gleichen Status verbunden sind. Darüber hinaus würde es nur von wenigen Systemen wie Rendering, Keyframe-Bewegung und Physik verwendet.
In einem solchen Fall besteht möglicherweise eine 3-zu-1-Beziehung zwischen den Systemen, die die Schnittstelle zur Schnittstelle verwenden, und eine 100-zu-1-Beziehung zwischen den Subtypen, die die Schnittstelle zur Schnittstelle implementieren.
Die Komplexität und Wartung würde dann drastisch auf die Implementierung und Wartung von 100 Subtypen anstatt von 3 Client-Systemen, die davon abhängen, verzerrt IMotion
. Dies verlagerte alle unsere Wartungsschwierigkeiten auf die Wartung dieser 100 Untertypen, nicht der 3 Stellen, die die Schnittstelle verwenden. Aktualisierung von 3 Stellen im Code mit wenigen oder keinen "indirekten efferenten Kopplungen" (wie in Abhängigkeit davon, aber indirekt über eine Schnittstelle, keine direkte Abhängigkeit), keine große Sache: Aktualisierung von 100 Subtypstellen mit einer Bootsladung von "indirekten efferenten Kopplungen" , ziemlich große Sache *.
* Mir ist klar, dass es seltsam und falsch ist, mit der Definition von "efferenten Kopplungen" in diesem Sinne aus der Sicht der Implementierung zu schrauben. Ich habe einfach keinen besseren Weg gefunden, um die Wartungskomplexität zu beschreiben, die mit der Schnittstelle und den entsprechenden Implementierungen von hundert Subtypen verbunden ist muss sich ändern.
Also musste ich hart pushen, aber ich schlug vor, dass wir versuchen, etwas pragmatischer zu werden und die Idee der "reinen Benutzeroberfläche" zu lockern. Es machte für mich keinen Sinn, so etwas wie IMotion
komplett abstrakt und zustandslos zu machen, es sei denn, wir sahen einen Vorteil darin, dass es eine Vielzahl von Implementierungen gibt. In unserem Fall IMotion
würde eine Vielzahl von Implementierungen tatsächlich zu einem ziemlichen Wartungs-Albtraum werden, da wir keine Vielfalt wollten . Stattdessen haben wir versucht, eine Single-Motion-Implementierung zu erstellen, die wirklich gut gegen sich ändernde Client-Anforderungen ist, und haben häufig die reine Schnittstellenidee umgangen, um jeden Implementierer zu zwingen IMotion
, die gleiche Implementierung und den gleichen Status zu verwenden, damit wir nicht ' t Ziele duplizieren.
Interfaces wurden so mehr wie eine breite Behaviors
Assoziation mit einer Entität. IMotion
würde einfach zu einer Motion
"Komponente" werden (ich habe die Art und Weise, wie wir "Komponente" definiert haben, von COM zu einer geändert, bei der die übliche Definition eines Teils, das eine "vollständige" Entität bildet, näher rückt).
An Stelle von:
class IMotion
{
public:
virtual ~IMotion() {}
virtual void transform(const Matrix& mat) = 0;
...
};
Wir haben es so weiterentwickelt:
class Motion
{
public:
void transform(const Matrix& mat)
{
...
}
...
private:
Matrix transformation;
...
};
Dies ist ein offensichtlicher Verstoß gegen das Prinzip der Abhängigkeitsumkehrung, um vom Abstrakten zum Konkreten zurückzukehren, aber für mich ist eine solche Abstraktionsebene nur dann nützlich, wenn wir in naher Zukunft zweifelsfrei einen echten Bedarf vorhersehen können und nicht für eine solche Flexibilität lächerliche "Was-wäre-wenn" -Szenarien auszuüben, die völlig unabhängig von der Benutzererfahrung sind (was wahrscheinlich ohnehin eine Designänderung erfordern würde).
Also entwickelten wir uns zu diesem Design. QueryInterface
wurde mehr wie QueryBehavior
. Außerdem schien es sinnlos, hier Vererbung zu betreiben. Wir haben stattdessen Komposition verwendet. Aus Objekten wurde eine Sammlung von Komponenten, deren Verfügbarkeit zur Laufzeit abgefragt und injiziert werden konnte.
Einige Profis:
- War in unserem Fall noch viel einfacher zu warten als das vorherige COM-System mit reiner Schnittstelle. Unvorhergesehene Überraschungen wie eine Änderung der Anforderungen oder Workflow-Beschwerden könnten mit einer sehr zentralen und offensichtlichen
Motion
Implementierung leichter bewältigt werden , z. B. und nicht auf hundert Subtypen verteilt.
- Hat ein völlig neues Maß an Flexibilität geschaffen, wie wir es tatsächlich benötigten. In unserem vorherigen System konnten wir, da die Vererbung eine statische Beziehung modelliert, neue Entitäten nur zur Kompilierungszeit in C ++ effektiv definieren. Aus der Skriptsprache heraus konnten wir das nicht tun, z. B. konnten wir mit dem Kompositionsansatz neue Entitäten zur Laufzeit im Handumdrehen aneinanderreihen, indem wir ihnen lediglich Komponenten anfügten und sie einer Liste hinzufügten. Eine "Entität" wurde zu einer leeren Leinwand, auf der wir einfach eine Collage von allem zusammenfügen konnten, was wir im laufenden Betrieb brauchten, wobei relevante Systeme diese Entitäten automatisch erkannten und verarbeiteten.
Einige Nachteile:
- In der Effizienzabteilung hatten wir immer noch Schwierigkeiten und in den leistungskritischen Bereichen waren wir wartungsfreundlich. Jedes System würde am Ende immer noch Komponenten von Entitäten zwischenspeichern wollen, die diese Verhaltensweisen bereitstellten, um zu vermeiden, dass sie alle wiederholt durchlaufen und überprüft werden, was verfügbar ist. Jedes System, das Leistung verlangte, tat dies etwas anders und neigte dazu, diese zwischengespeicherte Liste und möglicherweise eine Datenstruktur (wenn irgendeine Form der Suche wie z. B. Ausmerzen oder Raytracing involviert war) auf einigen zu aktualisieren obskures Szenenänderungsereignis, z
- Es gab immer noch etwas Unbeholfenes und Komplexes, auf das ich bei all diesen körnigen kleinen verhaltensbezogenen, einfachen Objekten nicht eingehen konnte. Wir haben immer noch viele Ereignisse ausgelöst, um die Interaktionen zwischen diesen "Verhaltens" -Objekten zu behandeln, die manchmal notwendig waren, und das Ergebnis war sehr dezentraler Code. Jedes kleine Objekt war leicht auf Korrektheit zu prüfen und für sich genommen oft vollkommen korrekt. Trotzdem hatten wir das Gefühl, wir wollten ein riesiges Ökosystem aus kleinen Dörfern aufrechterhalten und darüber nachdenken, was sie alle einzeln tun und zu einem Ganzen zusammenfügen. Die C-Style-Codebasis der 80er Jahre fühlte sich an wie eine epische, übervölkerte Großstadt, die definitiv ein Alptraum für die Instandhaltung war.
- Flexibilitätsverlust durch fehlende Abstraktion, aber in einem Bereich, in dem wir nie auf ein echtes Bedürfnis gestoßen sind, also kaum ein praktischer Nachteil (wenn auch definitiv zumindest ein theoretischer).
- Die Aufrechterhaltung der ABI-Kompatibilität war immer schwierig, und dies machte es schwieriger, stabile Daten und nicht nur eine stabile Schnittstelle für ein "Verhalten" zu benötigen. Es ist jedoch problemlos möglich, neue Verhaltensweisen hinzuzufügen und vorhandene Verhaltensweisen einfach zu verwerfen, wenn eine Statusänderung erforderlich ist. Dies ist vermutlich einfacher, als auf Subtypebene Backflips unter den Schnittstellen durchzuführen, um Versionsprobleme zu lösen.
Ein Phänomen war, dass wir, da wir die Abstraktion dieser Verhaltenskomponenten verloren haben, mehr von ihnen hatten. Beispielsweise IRenderable
würden wir anstelle einer abstrakten Komponente ein Objekt mit einem Beton Mesh
oder einer PointSprites
Komponente verbinden. Das Wiedergabesystem würde wissen, wie es Mesh
und PointSprites
Komponenten wiedergibt, und würde Entitäten finden, die solche Komponenten bereitstellen und diese zeichnen. Zu anderen Zeiten hatten wir verschiedene Renderables SceneLabel
, von denen wir im Nachhinein festgestellt haben, dass sie erforderlich sind, und daher haben wir SceneLabel
in diesen Fällen ein an relevante Entitäten angehängt (möglicherweise zusätzlich zu einem Mesh
). Die Rendering-System-Implementierung würde dann aktualisiert, um zu wissen, wie Entitäten gerendert werden, die diese bereitgestellt haben, und dies war eine ziemlich einfache Änderung.
In diesem Fall kann eine Entität, die aus Komponenten besteht, auch als Komponente für eine andere Entität verwendet werden. Wir würden die Dinge auf diese Weise aufbauen, indem wir Legoblöcke anschließen.
ECS: Systeme und Rohdatenkomponenten
Das letzte System war so weit, dass ich es alleine geschafft habe, und wir haben es immer noch mit COM bastardiert. Es fühlte sich an, als wolle es ein Entity-Component-System werden, aber ich war zu der Zeit nicht damit vertraut. Ich habe mich nach Beispielen im COM-Stil umgesehen, die mein Fach gesättigt haben, als ich AAA-Game-Engines als Inspiration für die Architektur hätte betrachten sollen. Endlich habe ich damit angefangen.
Was mir fehlte, waren einige Schlüsselideen:
- Die Formalisierung von "Systemen" zur Verarbeitung von "Bauteilen".
- "Komponenten" sind eher Rohdaten als Verhaltensobjekte, die zu einem größeren Objekt zusammengefasst werden.
- Entitäten als nichts anderes als eine strikte ID, die einer Sammlung von Komponenten zugeordnet ist.
Schließlich verließ ich diese Firma und begann an einem ECS als Indy zu arbeiten (wobei ich immer noch daran arbeitete, während ich meine Ersparnisse abbaute), und es war bei weitem das am einfachsten zu verwaltende System.
Was mir beim ECS-Ansatz auffiel, war, dass es die Probleme löste, mit denen ich oben immer noch zu kämpfen hatte. Am wichtigsten für mich war, dass wir statt winziger kleiner Dörfer mit komplexen Interaktionen "Städte" in gesunder Größe verwalten. Es war nicht so schwer zu erhalten wie eine monolithische "Großstadt", zu groß in ihrer Bevölkerung, um effektiv verwaltet zu werden, aber nicht so chaotisch wie eine Welt voller winziger kleiner Dörfer, die miteinander interagieren und nur an die Handelsrouten denken zwischen ihnen bildete sich ein alptraumhaftes Diagramm. ECS hat die ganze Komplexität in Richtung sperriger "Systeme" destilliert, wie ein Rendering-System, eine "Stadt" von gesunder Größe, aber keine "übervölkerte Großstadt".
Komponenten, die zu Rohdaten wurden, fühlten sich für mich anfangs wirklich seltsam an, da dies sogar das grundlegende Prinzip des Versteckens von Informationen in OOP verletzt. Es war eine Art Herausforderung für einen der größten Werte, die mir an OOP besonders am Herzen lag, nämlich die Fähigkeit, Invarianten beizubehalten, die eine Kapselung und das Verstecken von Informationen erforderten. Aber es begann sich nicht weiter darum zu kümmern, wie schnell klar wurde, was mit nur einem Dutzend oder so breiten Systemen passierte, die diese Daten transformierten, anstatt dass eine solche Logik auf Hunderte bis Tausende von Subtypen verteilt wurde, die eine Kombination von Schnittstellen implementierten. Ich neige dazu, es als immer noch OOP-artig zu betrachten, mit der Ausnahme, dass die Systeme die Funktionalität und Implementierung bereitstellen, die auf die Daten zugreifen, die Komponenten die Daten bereitstellen und die Entitäten Komponenten bereitstellen.
Es wurde noch einfacher , die vom System verursachten Nebenwirkungen zu beurteilen, als es nur eine Handvoll sperriger Systeme gab, die die Daten in großen Schritten umwandelten. Das System wurde viel "flacher", meine Call-Stacks wurden für jeden Thread flacher als je zuvor. Ich könnte an das System auf dieser Ebene denken und nicht auf seltsame Überraschungen stoßen.
Ebenso wurden auch die leistungskritischen Bereiche hinsichtlich der Beseitigung dieser Abfragen vereinfacht. Da die Idee von "System" sehr formalisiert wurde, konnte ein System die Komponenten abonnieren, an denen es interessiert war, und nur eine zwischengespeicherte Liste von Entitäten erhalten, die diese Kriterien erfüllen. Diese Caching-Optimierung musste nicht von jedem Einzelnen durchgeführt werden, sondern wurde zentralisiert.
Einige Profis:
- Scheint fast jedes große architektonische Problem zu lösen, auf das ich in meiner Karriere gestoßen bin, ohne mich jemals in einer Design-Ecke gefangen zu fühlen, wenn ich auf unerwartete Bedürfnisse stoße.
Einige Nachteile:
- Manchmal fällt es mir immer noch schwer, mich damit zu beschäftigen, und es ist nicht das ausgereifteste oder etablierteste Paradigma, selbst in der Spielebranche, wo sich die Leute darüber streiten, was es genau bedeutet und wie man Dinge macht. Mit dem früheren Team, mit dem ich zusammengearbeitet habe, hätte ich das definitiv nicht machen können. Es bestand aus Mitgliedern, die tief in die Denkweise im COM-Stil oder in die Denkweise im C-Stil der 1980er-Jahre der ursprünglichen Codebasis verstrickt waren. Wenn ich manchmal verwirrt bin, geht es darum, wie man grafische Beziehungen zwischen Komponenten modelliert, aber ich habe immer eine Lösung gefunden, die sich später nicht als schrecklich herausstellte, wenn ich eine Komponente nur von einer anderen abhängig machen kann ("diese Bewegung") Die Komponente ist von der anderen Komponente als übergeordnete Komponente abhängig, und das System verwendet die Speicherung, um zu vermeiden, dass wiederholt dieselben rekursiven Bewegungsberechnungen durchgeführt werden. ", z. B.
- ABI ist immer noch schwierig, aber bisher würde ich sogar sagen, dass es einfacher ist als ein reiner Schnittstellenansatz. Eine veränderte Denkweise: Datenstabilität wird zum alleinigen Schwerpunkt von ABI und nicht zur Schnittstellenstabilität. In mancher Hinsicht ist es einfacher, Datenstabilität als Schnittstellenstabilität zu erreichen (zum Beispiel: Keine Versuchung, eine Funktion zu ändern, nur weil sie einen neuen Parameter benötigt. Solche Dinge passieren in groben Systemimplementierungen, die ABI nicht beschädigen.
Ist es jedoch sinnvoll, Anwendungen auch mit der in Game-Engines üblichen Component-Entity-System-Architektur zu erstellen?
Ich würde also auf jeden Fall "Ja" sagen, wobei mein persönliches VFX-Beispiel ein starker Kandidat ist. Aber das ist immer noch ziemlich ähnlich wie beim Spielen.
Ich habe es nicht in entlegeneren Gegenden praktiziert, die völlig von den Bedenken der Game-Engines losgelöst sind (VFX ist ziemlich ähnlich), aber es scheint mir, dass weit mehr Gebiete gute Kandidaten für einen ECS-Ansatz sind. Vielleicht wäre sogar ein GUI-System für eines geeignet, aber ich verwende dort immer noch einen OOP-Ansatz (aber ohne tiefe Vererbung im Gegensatz zu Qt, zB).
Es ist ein weitgehend unerforschtes Gebiet, aber es scheint mir immer dann geeignet, wenn sich Ihre Entitäten aus einer reichen Kombination von "Merkmalen" zusammensetzen lassen (und genau, welche Kombination von Merkmalen sich je ändern wird) und wenn Sie eine Handvoll verallgemeinerter Merkmale haben Systeme, die Entitäten verarbeiten, die die erforderlichen Merkmale aufweisen.
In solchen Fällen wird es zu einer sehr praktischen Alternative zu jedem Szenario, in dem Sie möglicherweise versucht sind, so etwas wie Mehrfachvererbung oder eine Emulation des Konzepts (z. B. Mixins) zu verwenden, um nur Hunderte oder mehr Combos in einer Deep-Inheritance-Hierarchie oder Hunderte von Combos zu erstellen von Klassen in einer flachen Hierarchie, die eine bestimmte Kombination von Schnittstellen implementieren, bei denen jedoch nur wenige Systeme vorhanden sind (z. B. Dutzende).
In diesen Fällen fühlt sich die Komplexität der Codebasis proportionaler an als die Anzahl der Systeme anstelle der Anzahl der Typkombinationen, da jeder Typ nur noch eine Entität ist, die Komponenten zusammensetzt, die nichts weiter als Rohdaten sind. GUI-Systeme passen natürlich zu diesen Arten von Spezifikationen, bei denen Hunderte von möglichen Widget-Typen aus anderen Basistypen oder Schnittstellen kombiniert werden können, aber nur eine Handvoll von Systemen, um sie zu verarbeiten (Layout-System, Rendering-System usw.). Wenn ein GUI-System ECS verwendet, wäre es wahrscheinlich viel einfacher, über die Richtigkeit des Systems nachzudenken, wenn die gesamte Funktionalität von einer Handvoll dieser Systeme anstelle von Hunderten verschiedener Objekttypen mit vererbten Schnittstellen oder Basisklassen bereitgestellt wird. Wenn ein GUI-System ECS verwendet, haben Widgets keine Funktionalität, nur Daten. Nur eine Handvoll Systeme, die Widget-Entitäten verarbeiten, verfügen über Funktionen. Wie überschreibbare Ereignisse für ein Widget gehandhabt werden, ist mir unklar, aber aufgrund meiner bisher begrenzten Erfahrung habe ich keinen Fall gefunden, in dem diese Art von Logik nicht auf eine Weise zentral auf ein bestimmtes System übertragen werden konnte, die z Rückblickend ergab sich eine viel elegantere Lösung, die ich jemals erwarten würde.
Ich würde es gerne in mehr Bereichen einsetzen sehen, da es in meinen Bereichen ein Lebensretter war. Natürlich ist es ungeeignet, wenn Ihr Design nicht auf diese Weise zerfällt, von Einheiten, die Komponenten aggregieren, bis zu groben Systemen, die diese Komponenten verarbeiten. Wenn sie jedoch auf natürliche Weise zu dieser Art von Modell passen, ist es das Schönste, das mir bisher begegnet ist .