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 EvilTree
podklasą Tree
lub Enemy
?”. Jeśli pozwolimy na wielokrotne dziedziczenie, powstanie problem z diamentem. Mogliśmy zamiast ciągnąć łączny funkcjonalność Tree
i Enemy
dalej w górę hierarchii, która prowadzi do klas Boga, albo możemy celowo opuścić zachowanie w naszych Tree
and Entity
zajęciach (co czyni je Interfejsy w przypadku skrajnego), tak, że EvilTree
moż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 Tree
i Enemy
przedmiotów w różnych komponentów - mówią Position
, Health
i 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 EvilTree
można podnieść ulepszenie i zadać obrażenia? Najpierw potrzebujemy A CollisionSystem
i A DamageSystem
(prawdopodobnie już je mamy). Na CollisionSystem
potrzeby komunikowania się z DamageSystem
: Za każdym razem dwie rzeczy zderzają CollisionSystem
wysyła wiadomość do DamageSystem
zdrowia 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 potemDamageSystem
musi 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 PowerupSystem
modyfikację, StatComponent
któ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 attack
musi 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?