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?
źródło
Odpowiedzi:
ECS całkowicie rujnuje ukrywanie danych. Jest to kompromis wzoru.
ECS jest doskonały w odsprzęganiu. Dobry ECS pozwala systemowi deklaracji stwierdzić, że działa na każdym elemencie, który ma komponent prędkości i pozycji, bez konieczności dbania o to, jakie typy jednostek istnieją lub jakie inne systemy mają dostęp do tych komponentów. Jest to co najmniej równoważne w mocy odsprzęgającej, aby obiekty gry implementowały określone interfejsy.
Dwa systemy uzyskujące dostęp do tych samych komponentów to funkcja, a nie problem. Jest to w pełni oczekiwane i w żaden sposób nie łączy systemów. To prawda, że systemy będą miały domyślny wykres zależności, ale te zależności są nieodłącznie związane z modelowanym światem. Stwierdzenie, że system uszkodzeń nie powinien mieć pośredniej zależności od systemu wzmocnienia, oznacza twierdzenie, że ulepszenia nie wpływają na obrażenia, i to prawdopodobnie jest złe. Jednakże, chociaż istnieje zależność, systemy nie są sprzężone - możesz usunąć system ulepszeń z gry bez wpływu na system obrażeń, ponieważ komunikacja odbyła się za pośrednictwem komponentu stat i była całkowicie niejawna.
Rozwiązania tych zależności i systemów zamawiania można dokonać w jednym centralnym miejscu, podobnie jak działa rozwiązywanie zależności w systemie DI. Tak, złożona gra będzie miała złożony wykres systemów, ale ta złożoność jest nieodłączna i przynajmniej jest zawarta.
źródło
Prawie nie da się obejść faktu, że system musi mieć dostęp do wielu komponentów. Aby coś takiego jak VelocitySystem działało, prawdopodobnie potrzebowałby dostępu do VelocityComponent i PositionComponent. Tymczasem RenderingSystem musi również uzyskać dostęp do tych danych. Bez względu na to, co robisz, w pewnym momencie system renderujący musi wiedzieć, gdzie renderować obiekt, a VelocitySystem musi wiedzieć, gdzie przenieść obiekt.
W tym celu wymagana jest jawność zależności. Każdy system musi wyraźnie określać, jakie dane będzie czytać i do jakich danych będzie zapisywać. Gdy system chce pobrać konkretny komponent, musi być w stanie to zrobić tylko jawnie . W najprostszej formie po prostu zawiera komponenty dla każdego wymaganego typu (np. RenderSystem potrzebuje RenderComponents i PositionComponents) jako argumenty i zwraca wszystko, co zmienił (np. Tylko RenderComponents).
Możesz mieć zamówienie w takim projekcie. Nic nie mówi, że dla ECS twoje systemy muszą być niezależne od porządku lub czegoś takiego.
Korzystanie z tego projektu elementu systemu podmiotu i FRP nie wyklucza się wzajemnie. W rzeczywistości systemy mogą być postrzegane jako nic innego, jak bez stanu, po prostu wykonujące transformacje danych (komponenty).
FRP nie rozwiązałoby problemu związanego z koniecznością użycia wymaganych informacji w celu wykonania niektórych operacji.
źródło