Tytuł jest celowo hiperboliczny i może to być tylko mój brak doświadczenia ze schematem, ale oto moje rozumowanie:
„Zwykłym” lub prawdopodobnie prostym sposobem implementacji jednostek jest implementowanie ich jako obiektów i wspólne zachowanie podklas. Prowadzi to do klasycznego problemu „jest EvilTreepodklasą Treelub Enemy?”. Jeśli pozwolimy na wielokrotne dziedziczenie, powstanie problem z diamentem. Mogliśmy zamiast ciągnąć łączny funkcjonalność Treei Enemydalej w górę hierarchii, która prowadzi do klas Boga, albo możemy celowo opuścić zachowanie w naszych Treeand Entityzajęciach (co czyni je Interfejsy w przypadku skrajnego), tak, że EvilTreemożna realizować ten sam - co prowadzi do powielanie kodu, jeśli kiedykolwiek mamy SomewhatEvilTree.
Entity-Component Systems starają się rozwiązać ten problem poprzez podzielenie Treei Enemyprzedmiotów w różnych komponentów - mówią Position, Healthi AI- i wdrożenie systemów, takim jak AISystem, który zmienia pozycję danej Entitiy za zgodnie z decyzjami AI. Jak dotąd tak dobrze, ale co, jeśli EvilTreemożna podnieść ulepszenie i zadać obrażenia? Najpierw potrzebujemy A CollisionSystemi A DamageSystem(prawdopodobnie już je mamy). Na CollisionSystempotrzeby komunikowania się z DamageSystem: Za każdym razem dwie rzeczy zderzają CollisionSystemwysyła wiadomość do DamageSystemzdrowia więc może odjąć. Na obrażenia mają również wpływ ulepszenia, więc musimy je gdzieś przechowywać. Czy tworzymy nowy PowerupComponent, który dołączamy do bytów? Ale potemDamageSystemmusi wiedzieć o czymś, o czym wolałby nic nie wiedzieć - w końcu są też rzeczy zadające obrażenia, które nie mogą podnieść bonusów (np. a Spike). Czy zezwalamy na PowerupSystemmodyfikację, StatComponentktóra jest również używana do obliczania szkód podobnych do tej odpowiedzi ? Ale teraz dwa systemy mają dostęp do tych samych danych. W miarę jak nasza gra staje się bardziej złożona, staje się niematerialnym wykresem zależności, w którym komponenty są współużytkowane przez wiele systemów. W tym momencie możemy po prostu użyć globalnych zmiennych statycznych i pozbyć się całej płyty kotłowej.
Czy istnieje skuteczny sposób na rozwiązanie tego problemu? Jednym z moich pomysłów było zezwolenie komponentom na pewne funkcje, np. Podanie tej, StatComponent attack()która domyślnie zwraca liczbę całkowitą, ale można ją skomponować, gdy nastąpi ulepszenie:
attack = getAttack compose powerupBy(20) compose powerdownBy(40)
To nie rozwiązuje problemu, który attackmusi być zapisany w komponencie, do którego dostęp ma wiele systemów, ale przynajmniej mógłbym poprawnie wpisać funkcje, jeśli mam język, który obsługuje go wystarczająco:
// 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
W ten sposób gwarantuję przynajmniej prawidłowe uporządkowanie różnych funkcji dodawanych przez systemy. Tak czy inaczej, wydaje mi się, że szybko zbliżam się tutaj do programowania funkcjonalnego, więc zadaję sobie pytanie, czy nie powinienem był tego używać od samego początku (tylko sprawdziłem FRP, więc mogę się tutaj mylić). Widzę, że ECS stanowi ulepszenie w stosunku do złożonych hierarchii klas, ale nie jestem przekonany, że jest idealny.
Czy istnieje rozwiązanie tego problemu? Czy brakuje mi funkcjonalności / wzorca, aby lepiej rozdzielić ECS? Czy FRP jest po prostu lepiej dostosowane do tego problemu? Czy problemy te wynikają z wewnętrznej złożoności tego, co próbuję zaprogramować; tj. czy FRP miałby podobne problemy?