Ist ein Entity-Component-System nicht schrecklich zum Entkoppeln / Verstecken von Informationen?

11

Der Titel ist absichtlich hyperbolisch und es mag nur meine Unerfahrenheit mit dem Muster sein, aber hier ist meine Argumentation:

Die "übliche" oder wohl unkomplizierte Art, Entitäten zu implementieren, besteht darin, sie als Objekte zu implementieren und gemeinsames Verhalten in Unterklassen einzuteilen. Dies führt zu dem klassischen Problem "Ist EvilTreeeine Unterklasse von Treeoder Enemy?". Wenn wir Mehrfachvererbung zulassen, entsteht das Diamantproblem. Wir könnten stattdessen die kombinierte Funktionalität der Hierarchie, die zu Gottklassen führt, herausziehen Treeund Enemyweiter nach oben ziehen , oder wir können absichtlich das Verhalten in unseren Klassen Treeund Entity(was sie im Extremfall zu Schnittstellen macht) weglassen, damit sie dies EvilTreeselbst implementieren können - was dazu führt Codeduplizierung, falls wir jemals eine haben SomewhatEvilTree.

Entity-Komponentensysteme versuchen , dieses Problem zu lösen , indem die Teilung Treeund EnemyObjekt in verschiedene Komponenten - sagen wir Position, Healthund AI- und implementieren Systeme, wie zum Beispiel eine , AISystemdie eine Entitiy Position nach AI Entscheidungen ändert. So weit so gut, aber was ist, wenn EvilTreeman ein Powerup aufnehmen und Schaden verursachen kann? Zuerst brauchen wir ein CollisionSystemund ein DamageSystem(diese haben wir wahrscheinlich schon). Die CollisionSystemNotwendigkeit, mit dem zu kommunizieren DamageSystem: Jedes Mal, wenn zwei Dinge kollidieren, CollisionSystemsendet das eine Nachricht an das, DamageSystemdamit es die Gesundheit subtrahieren kann. Der Schaden wird auch durch Powerups beeinflusst, daher müssen wir ihn irgendwo aufbewahren. Erstellen wir eine neue PowerupComponent, die wir an Entitäten anhängen? Aber dann dieDamageSystemmuss über etwas Bescheid wissen, von dem es lieber nichts wissen möchte - schließlich gibt es auch Dinge, die Schaden verursachen und keine Powerups aufnehmen können (z Spike. B. a ). Erlauben wir dem PowerupSystem, ein zu ändern StatComponent, das auch für Schadensberechnungen ähnlich dieser Antwort verwendet wird ? Jetzt greifen zwei Systeme auf dieselben Daten zu. Wenn unser Spiel komplexer wird, wird es zu einem immateriellen Abhängigkeitsgraphen, in dem Komponenten von vielen Systemen gemeinsam genutzt werden. An diesem Punkt können wir einfach globale statische Variablen verwenden und alle Boilerplates entfernen.

Gibt es einen effektiven Weg, um dies zu lösen? Eine Idee, die ich hatte, war, Komponenten bestimmte Funktionen zu überlassen, z. B. die, StatComponent attack()die standardmäßig nur eine Ganzzahl zurückgibt, aber beim Einschalten zusammengesetzt werden kann:

attack = getAttack compose powerupBy(20) compose powerdownBy(40)

Dies löst nicht das Problem, attackdas in einer Komponente gespeichert werden muss, auf die mehrere Systeme zugreifen, aber zumindest könnte ich die Funktionen richtig eingeben, wenn ich eine Sprache habe, die dies ausreichend unterstützt:

// In StatComponent
type Strength = PrePowerup | PostPowerup
type Damage = Int
type PrePowerup = Int
type PostPowerup = Int
attack: Strength = getAttack //default value, can be changed by systems
getAttack: PrePowerup

// these functions can be defined in other components or in PowerupSystems
powerupBy: Strength -> PostPowerup
powerdownBy: Strength -> PostPowerup
subtractArmor: Strength -> Damage

// in DamageSystem
dealDamage: Damage -> () = attack compose subtractArmor compose hurtSomeEntity

Auf diese Weise garantiere ich zumindest die korrekte Reihenfolge der verschiedenen Funktionen, die von Systemen hinzugefügt werden. Wie auch immer, es scheint, dass ich mich hier schnell der funktionalen reaktiven Programmierung nähere, also frage ich mich, ob ich das nicht von Anfang an hätte verwenden sollen (ich habe mich gerade erst mit FRP befasst, also kann ich mich hier irren). Ich sehe, dass ECS eine Verbesserung gegenüber komplexen Klassenhierarchien darstellt, bin aber nicht davon überzeugt, dass es ideal ist.

Gibt es eine Lösung dafür? Gibt es eine Funktionalität / ein Muster, das mir fehlt, um ECS sauberer zu entkoppeln? Ist FRP für dieses Problem nur strikt besser geeignet? Entstehen diese Probleme nur aus der inhärenten Komplexität dessen, was ich zu programmieren versuche? dh hätte FRP ähnliche Probleme?

PawkyPenguin
quelle
9
Literaturempfehlung: Wizards and Warriors von Eric Lippert .
Robert Harvey
Ich vermisse Erics Blog wirklich (seit es um C # ging).
OldFart

Antworten:

21

ECS ruiniert das Verstecken von Daten vollständig. Dies ist ein Kompromiss des Musters.

ECS kann hervorragend entkoppelt werden. Mit einem guten ECS kann ein Verschiebungssystem erklären, dass es für jede Entität mit einer Geschwindigkeits- und einer Positionskomponente funktioniert, ohne sich darum kümmern zu müssen, welche Entitätstypen vorhanden sind oder welche anderen Systeme auf diese Komponenten zugreifen. Dies entspricht mindestens der Entkopplungsleistung der Implementierung bestimmter Schnittstellen durch Spielobjekte.

Zwei Systeme, die auf dieselben Komponenten zugreifen, sind eine Funktion, kein Problem. Es wird voll erwartet und koppelt Systeme in keiner Weise. Es ist wahr, dass Systeme einen impliziten Abhängigkeitsgraphen haben, aber diese Abhängigkeiten sind der modellierten Welt inhärent. Zu sagen, dass das Schadenssystem nicht die implizite Abhängigkeit vom Powerup-System haben sollte, bedeutet zu behaupten, dass Powerups keinen Einfluss auf den Schaden haben, und das ist wahrscheinlich falsch. Während die Abhängigkeit besteht, sind die Systeme jedoch nicht gekoppelt. Sie können das Powerup-System aus dem Spiel entfernen, ohne das Schadenssystem zu beeinträchtigen, da die Kommunikation über die stat-Komponente erfolgte und vollständig implizit war.

Das Auflösen dieser Abhängigkeiten und Ordnungssysteme kann an einem zentralen Ort erfolgen, ähnlich wie die Auflösung von Abhängigkeiten in einem DI-System funktioniert. Ja, ein komplexes Spiel wird einen komplexen Graphen von Systemen haben, aber diese Komplexität ist inhärent und zumindest enthalten.

Sebastian Redl
quelle
7

Es führt fast kein Weg daran vorbei, dass ein System auf mehrere Komponenten zugreifen muss. Damit so etwas wie ein VelocitySystem funktioniert, muss es wahrscheinlich auf eine VelocityComponent und eine PositionComponent zugreifen. In der Zwischenzeit muss das RenderingSystem auch auf diese Daten zugreifen. Unabhängig davon, was Sie tun, muss das Rendering-System irgendwann wissen, wo das Objekt gerendert werden soll, und das VelocitySystem muss wissen, wohin das Objekt verschoben werden soll.

Was Sie dafür benötigen, ist die explizite Darstellung von Abhängigkeiten. Jedes System muss explizit angeben, welche Daten es lesen und in welche Daten es schreiben wird. Wenn ein System eine bestimmte Komponente abrufen möchte, muss es dies nur explizit tun können . In seiner einfachsten Form hat es einfach die Komponenten für jeden Typ, den es benötigt (z. B. das RenderSystem benötigt die RenderComponents und PositionComponents) als Argumente und gibt alles zurück, was es geändert hat (z. B. nur die RenderComponents).

Auf diese Weise garantiere ich zumindest die korrekte Reihenfolge der verschiedenen Funktionen, die von Systemen hinzugefügt werden

Sie können in einem solchen Design bestellen. Nichts sagt aus, dass Ihre Systeme für ECS unabhängig von der Reihenfolge oder dergleichen sein müssen.

Ist FRP für dieses Problem nur strikt besser geeignet? Entstehen diese Probleme nur aus der inhärenten Komplexität dessen, was ich zu programmieren versuche? dh hätte FRP ähnliche Probleme?

Die Verwendung dieses Entity-Component-System-Designs und von FRP schließt sich nicht gegenseitig aus. Tatsächlich können die Systeme als nichts anderes angesehen werden, da sie keinen Zustand haben und lediglich Datentransformationen (die Komponenten) durchführen.

FRP würde das Problem nicht lösen, die Informationen verwenden zu müssen, die Sie benötigen, um eine Operation auszuführen.

Athos vk
quelle