Ist es sinnvoll, Anwendungen (nicht Spiele) mit einer Komponenten-Entitäts-System-Architektur zu erstellen?

24

Ich weiß, dass es beim Erstellen von Anwendungen (nativ oder im Web) wie im Apple AppStore oder im Google Play App Store häufig vorkommt, eine Model-View-Controller-Architektur zu verwenden.

Ist es jedoch sinnvoll, Anwendungen auch mit der in Game-Engines üblichen Component-Entity-System-Architektur zu erstellen?

Andrew De Andrade
quelle
1
Schauen Sie sich die Architektur des Leuchttisches an
Hakan Deryal

Antworten:

38

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").

Bildbeschreibung hier eingeben

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:

Bildbeschreibung hier eingeben

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.

Bildbeschreibung hier eingeben

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 IMotionimmer 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 QueryInterfaceals 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ützenIRenderableDies 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).

Bildbeschreibung hier eingeben

Pragmatische Antwort: Zusammensetzung

Eines der Dinge, die wir zuvor bemerkt haben (oder zumindest ich), die Probleme verursacht haben, war, dass sie IMotionmö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 IMotionkomplett abstrakt und zustandslos zu machen, es sei denn, wir sahen einen Vorteil darin, dass es eine Vielzahl von Implementierungen gibt. In unserem Fall IMotionwü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 BehaviorsAssoziation mit einer Entität. IMotionwü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. QueryInterfacewurde 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.

Bildbeschreibung hier eingeben

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 MotionImplementierung 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 IRenderablewürden wir anstelle einer abstrakten Komponente ein Objekt mit einem Beton Meshoder einer PointSpritesKomponente verbinden. Das Wiedergabesystem würde wissen, wie es Meshund PointSpritesKomponenten 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 SceneLabelin 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:

  1. Die Formalisierung von "Systemen" zur Verarbeitung von "Bauteilen".
  2. "Komponenten" sind eher Rohdaten als Verhaltensobjekte, die zu einem größeren Objekt zusammengefasst werden.
  3. 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.

Bildbeschreibung hier eingeben

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 .

Thomas Owens
quelle
1) Was hat Ihr Beispiel-VFX-Programm aus Benutzersicht gemacht? 2) An welchem ​​ECS-Projekt arbeiten Sie gerade? ♥ Danke, dass du das geschrieben hast! ♥
Welpe
1
Sehr gründliche Erklärung - danke. Ich habe das Gefühl, dass ich zu vielen der gleichen Schlussfolgerungen komme, die Sie in Bezug auf die Frage ziehen, wie anwendbar ECS jenseits von Spielen ist. in meinem Fall speziell komplexe GUIs. Es fühlt sich auf jeden Fall auf den ersten Blick wirklich komisch an, wenn man so etwas gegen den Strich der üblichen Vorgehensweise macht (tiefe Vererbungshierarchien spielen in UI-Frameworks eine besonders wichtige Rolle), aber es ist ermutigend, andere zu sehen, die diesen Ansatz effektiver finden.
Danny Yaroslavski
1
Vielen Dank für diesen tollen ... Artikel! Für eine komponentenbasierte Benutzeroberfläche würde ich empfehlen, sich die Benutzeroberfläche von Unity3d anzusehen. Es ist unglaublich flexibel und erweiterbar im Vergleich zu vererbungsbasierten Anwendungen wie CocoaTouch.
Ivan Mir
16

Die Component-Entity-System-Architektur für Game-Engines funktioniert für Spiele aufgrund der Art der Spielesoftware und ihrer einzigartigen Eigenschaften und Qualitätsanforderungen. Zum Beispiel bieten Entitäten ein einheitliches Mittel zum Adressieren und Arbeiten mit Dingen im Spiel, das sich in Zweck und Verwendung drastisch unterscheiden kann, jedoch vom System auf einheitliche Weise gerendert, aktualisiert oder serialisiert / deserialisiert werden muss. Durch die Einbindung eines Komponentenmodells in diese Architektur können sie eine einfache Kernstruktur beibehalten und bei geringer Codekopplung nach Bedarf weitere Features und Funktionen hinzufügen. Es gibt eine Reihe verschiedener Softwaresysteme, die von den Merkmalen dieses Entwurfs profitieren könnten, wie z. B. CAD-Anwendungen, A / V-Codecs,

TL; DR - Entwurfsmuster funktionieren nur dann gut, wenn die Problemdomäne für die Merkmale und Nachteile, die sie dem Entwurf auferlegen, ausreichend geeignet ist.

Schrotflinte Ninja
quelle
8

Wenn die Problemdomäne dafür gut geeignet ist, sicherlich.

Meine aktuelle Arbeit umfasst eine App, die abhängig von einer Reihe von Laufzeitfaktoren eine Vielzahl von Funktionen unterstützen muss. Die Verwendung komponentenbasierter Entitäten, um all diese Funktionen zu entkoppeln und Erweiterbarkeit und Testbarkeit für sich zu ermöglichen, war für uns idyllisch.

Bearbeiten: Meine Arbeit beinhaltet die Bereitstellung von Konnektivität für proprietäre Hardware (in C #). Abhängig vom Formfaktor der Hardware, der installierten Firmware, dem vom Client erworbenen Servicelevel usw. müssen wir dem Gerät unterschiedliche Funktionalitätsebenen bereitstellen. Sogar einige Funktionen mit derselben Schnittstelle sind je nach Version des Geräts unterschiedlich implementiert.

Bisherige Codebasen hatten hier sehr breite Schnittstellen, von denen viele nicht implementiert waren. Einige hatten viele dünne Schnittstellen, die dann statisch in einer Klasse zusammengesetzt wurden. Einige benutzten einfach String -> String-Wörterbücher, um es zu modellieren. (Wir haben viele Abteilungen, die alle glauben, dass sie es besser können)

Diese haben alle ihre Mängel. Breite Schnittstellen sind eineinhalb Schmerz, um effektiv zu verspotten / zu testen. Das Hinzufügen neuer Funktionen bedeutet das Ändern der öffentlichen Schnittstelle (und aller vorhandenen Implementierungen). Viele dünne Interfaces führten zu sehr hässlichem Code-Verbrauch, aber da wir am Ende herumgereicht haben, litten die Tests immer noch darunter. Außerdem haben die Thin-Interfaces ihre Abhängigkeiten nicht gut gemanagt. String-Wörterbücher weisen die üblichen Parsing- und Existenzprobleme sowie Performance-, Lesbarkeits- und Wartbarkeitslücken auf.

Was wir jetzt verwenden, ist eine sehr schlanke Entität, deren Komponenten basierend auf Laufzeitinformationen ermittelt und zusammengestellt werden. Abhängigkeiten werden deklarativ ausgeführt und vom Kernkomponenten-Framework automatisch aufgelöst. Die Komponenten selbst können isoliert getestet werden, da sie direkt mit ihren Abhängigkeiten arbeiten und Probleme mit fehlenden Abhängigkeiten frühzeitig erkannt werden - und zwar an einem Ort, anstatt die Abhängigkeit zum ersten Mal zu verwenden. Neue (oder Test-) Komponenten können eingefügt werden, und kein vorhandener Code ist davon betroffen. Verbraucher fragen die Entität nach einer Schnittstelle zu der Komponente, sodass wir uns mit den verschiedenen Implementierungen (und wie die Implementierungen Laufzeitdaten zugeordnet werden) relativ frei beschäftigen können.

Für eine Situation wie diese, in der die Zusammensetzung des Objekts und seiner Schnittstellen eine (sehr unterschiedliche) Teilmenge gemeinsamer Komponenten enthalten kann, funktioniert dies sehr gut.

Telastyn
quelle
1
Können Sie unter der Voraussetzung, dass Sie dazu berechtigt sind, nähere Angaben zu Ihrer aktuellen Arbeit machen? Ich bin gespannt, auf welche Weise die CES für das, was Sie bauen, idyllisch war.
Andrew De Andrade
Gibt es einen Artikel, ein Papier oder einen Blog über Ihre Erfahrungen? Außerdem hätte ich gerne weitere technische Details dazu :)
user1778770
@ user1778770 - nicht öffentlich verfügbar, nein. Welche Art von Fragen hatten Sie?
Telastyn
Beginnen wir mit etwas Einfachem. Umfasst Ihr Konzept den gesamten Anwendungsstapel (z. B. vom Business bis zum Frontend)? oder nur eine einzelne Schicht eines einzelnen Anwendungsfalls?
user1778770
@ user1778770 - In meiner Implementierung sind Entitäten / Komponenten in einer Ebene vorhanden. In verschiedenen Ebenen können verschiedene Entitäten vorhanden sein, diese sind jedoch häufig nicht 1: 1 (oder die Ebenen bieten keinen Vorteil).
Telastyn