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 EvilTree
eine Unterklasse von Tree
oder Enemy
?". Wenn wir Mehrfachvererbung zulassen, entsteht das Diamantproblem. Wir könnten stattdessen die kombinierte Funktionalität der Hierarchie, die zu Gottklassen führt, herausziehen Tree
und Enemy
weiter nach oben ziehen , oder wir können absichtlich das Verhalten in unseren Klassen Tree
und Entity
(was sie im Extremfall zu Schnittstellen macht) weglassen, damit sie dies EvilTree
selbst 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 Tree
und Enemy
Objekt in verschiedene Komponenten - sagen wir Position
, Health
und AI
- und implementieren Systeme, wie zum Beispiel eine , AISystem
die eine Entitiy Position nach AI Entscheidungen ändert. So weit so gut, aber was ist, wenn EvilTree
man ein Powerup aufnehmen und Schaden verursachen kann? Zuerst brauchen wir ein CollisionSystem
und ein DamageSystem
(diese haben wir wahrscheinlich schon). Die CollisionSystem
Notwendigkeit, mit dem zu kommunizieren DamageSystem
: Jedes Mal, wenn zwei Dinge kollidieren, CollisionSystem
sendet das eine Nachricht an das, DamageSystem
damit 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 dieDamageSystem
muss ü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, attack
das 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?