Do tej pory systemy komponentów encji, z których korzystałem, działały głównie jak artemidy Javy:
- Wszystkie dane w komponentach
- Bezstanowe niezależne systemy (przynajmniej w takim stopniu, że nie wymagają wkładu przy inicjalizacji) iterujące po każdej jednostce zawierającej tylko komponenty, którymi zainteresowany jest dany system
- Wszystkie systemy przetwarzają swoje byty jednym tikiem, a potem wszystko zaczyna się od nowa.
Teraz próbuję zastosować to po raz pierwszy w grze turowej, z mnóstwem wydarzeń i odpowiedzi, które muszą wystąpić w ustalonej kolejności względem siebie, zanim gra będzie mogła przejść dalej. Przykład:
Gracz A otrzymuje obrażenia od miecza. W odpowiedzi zbroja A włącza się i obniża otrzymywane obrażenia. Prędkość ruchu A jest również obniżana w wyniku osłabienia.
- Otrzymane obrażenia rozpoczynają całą interakcję
- Pancerz musi zostać obliczony i przyłożony do zadawanych obrażeń, zanim obrażenia zostaną zadane graczowi
- Zmniejszenia prędkości ruchu nie można zastosować do jednostki, dopóki obrażenia nie zostaną zadane, ponieważ zależy to od ostatecznej wielkości obrażeń.
Zdarzenia mogą również powodować inne zdarzenia. Zmniejszenie obrażeń miecza za pomocą zbroi może spowodować rozbicie miecza (musi to nastąpić przed zakończeniem redukcji obrażeń), co z kolei może spowodować dodatkowe zdarzenia w odpowiedzi na nie, w zasadzie rekurencyjną ocenę zdarzeń.
Podsumowując, wydaje się, że prowadzi to do kilku problemów:
- Wiele zmarnowanych cykli przetwarzania: większość systemów (z wyjątkiem rzeczy, które zawsze działają, np. Renderowanie) po prostu nie ma nic wartego do zrobienia, gdy nie jest „ich kolej” do pracy i spędza większość czasu czekając na wejście do gry prawidłowy stan pracy. Zaśmieca to każdy taki system czekami, które stają się coraz większe, im więcej stanów jest dodawanych do gry.
- Aby dowiedzieć się, czy system może przetwarzać byty obecne w grze, potrzebują jakiegoś sposobu monitorowania innych niepowiązanych stanów bytu / systemu (systaem odpowiedzialny za zadawanie obrażeń musi wiedzieć, czy zastosowano zbroję, czy nie). To albo wprowadza w błąd systemy z wieloma obowiązkami, albo stwarza potrzebę dodatkowych systemów, które nie mają innego celu niż skanowanie kolekcji jednostek po każdym cyklu przetwarzania i komunikowanie się z zestawem słuchaczy, informując ich, kiedy można coś zrobić.
W powyższych dwóch punktach założono, że systemy działają na tym samym zestawie jednostek, które w końcu zmieniają stan za pomocą flag w swoich komponentach.
Innym sposobem rozwiązania tego problemu byłoby dodanie / usunięcie komponentów (lub stworzenie zupełnie nowych bytów) w wyniku pracy jednego systemu w celu poprawy stanu gry. Oznacza to, że ilekroć system rzeczywiście ma pasujący byt, wie, że może go przetwarzać.
Powoduje to jednak, że systemy są odpowiedzialne za uruchamianie kolejnych systemów, co utrudnia uzasadnienie działania programów, ponieważ błędy nie pojawiają się w wyniku pojedynczej interakcji systemu. Dodawanie nowych systemów również staje się trudniejsze, ponieważ nie można ich wdrożyć, nie wiedząc dokładnie, w jaki sposób wpływają one na inne systemy (a poprzednie systemy mogą wymagać modyfikacji, aby wyzwolić stany, którymi zainteresowany jest nowy system), co w pewnym sensie pokonuje cel posiadania oddzielnych systemów z jednym zadaniem.
Czy to coś, z czym będę musiał żyć? Każdy przykład ECS, który widziałem, był w czasie rzeczywistym i bardzo łatwo jest zobaczyć, jak działa w takich przypadkach pętla jednej iteracji na grę. Nadal potrzebuję go do renderowania, wydaje się po prostu naprawdę nieodpowiedni dla systemów, które zatrzymują większość aspektów siebie za każdym razem, gdy coś się dzieje.
Czy istnieje jakiś wzorzec projektowy do przesuwania stanu gry do przodu, który jest odpowiedni do tego, czy powinienem po prostu przenieść całą logikę z pętli i zamiast tego uruchomić ją tylko w razie potrzeby?
źródło
Odpowiedzi:
Moja rada tutaj pochodzi z przeszłych doświadczeń w projekcie RPG, w którym zastosowaliśmy system komponentów. Powiem, że nienawidziłem pracować w tym kodzie gier, ponieważ był to kod spaghetti. Więc nie oferuję tutaj żadnej odpowiedzi, a jedynie perspektywę:
Logika, którą opisujesz, radząc sobie z uszkodzeniem miecza przez gracza ... wydaje się, że jeden system powinien być odpowiedzialny za to wszystko.
Gdzieś jest funkcja HandleWeaponHit (). Uzyskałby dostęp do elementu zbroi jednostki gracza, aby uzyskać odpowiednią zbroję. Uzyskałby dostęp do elementu broni atakującego bytu, by być może zniszczyć broń. Po obliczeniu ostatecznych obrażeń dotknąłby MovementComponent, aby gracz osiągnął zmniejszenie prędkości.
Jeśli chodzi o zmarnowane cykle przetwarzania ... HandleWeaponHit () powinien być uruchamiany tylko w razie potrzeby (po wykryciu trafienia mieczem).
Być może chodzi o to, że chciałem: z pewnością potrzebujesz miejsca w kodzie, w którym możesz umieścić punkt przerwania, uderzyć go, a następnie przejść przez całą logikę, która powinna działać, gdy nastąpi uderzenie mieczem. Innymi słowy, logika nie powinna być rozproszona w funkcjach tick () wielu systemów.
źródło
To pytanie sprzed lat, ale teraz mam do czynienia z tymi samymi trublami w mojej domowej grze podczas nauki ECS, a więc trochę nekromancji. Mamy nadzieję, że zakończy się to dyskusją lub przynajmniej kilkoma komentarzami.
Nie jestem pewien, czy to narusza koncepcje ECS, ale co jeśli:
Przykład:
Plusy:
Cons:
źródło
Publikując rozwiązanie, w końcu zdecydowałem się, podobnie jak w przypadku Jakowlewa.
Zasadniczo korzystałem z systemu zdarzeń, ponieważ bardzo intuicyjnie podążałem za jego logiką na zmianę. System ostatecznie był odpowiedzialny za jednostki w grze, które stosowały się do logiki turowej (gracz, potwory i wszystko, z czym mogą wchodzić w interakcje), zadania w czasie rzeczywistym, takie jak renderowanie i odpytywanie wejściowe, zostały umieszczone gdzie indziej.
Systemy implementują metodę onEvent, która przyjmuje zdarzenie i jednostkę jako dane wejściowe, sygnalizując, że jednostka odebrała zdarzenie. Każdy system subskrybuje również zdarzenia i jednostki z określonym zestawem komponentów. Jedynym punktem interakcji dostępnym dla systemów jest singleton menedżera encji, używany do wysyłania zdarzeń do encji i pobierania komponentów z konkretnej encji.
Gdy menedżer encji odbiera zdarzenie połączone z encją, do której jest wysyłany, umieszcza zdarzenie na końcu kolejki. Chociaż w kolejce znajdują się zdarzenia, najważniejsze zdarzenie jest pobierane i wysyłane do każdego systemu, który subskrybuje to zdarzenie i jest zainteresowany zestawem komponentów jednostki odbierającej zdarzenie. Systemy te mogą z kolei przetwarzać komponenty jednostki, a także wysyłać dodatkowe zdarzenia do menedżera.
Przykład: Gracz otrzymuje obrażenia, więc gracz otrzymuje wysyłane obrażenie. DamageSystem subskrybuje zdarzenia powodujące uszkodzenie wysyłane do dowolnej jednostki z komponentem zdrowia i ma metodę onEvent (encja, zdarzenie), która zmniejsza kondycję w komponencie encji o kwotę określoną w zdarzeniu.
Ułatwia to wstawienie systemu pancerza, który subskrybuje zdarzenia powodujące obrażenia wysyłane do jednostek z komponentem pancerza. Jego metoda onEvent zmniejsza obrażenia w przypadku o wartość pancerza w elemencie. Oznacza to, że określenie kolejności otrzymywania zdarzeń przez systemy wpływa na logikę gry, ponieważ system zbroi musi przetworzyć zdarzenie uszkodzenia przed systemem uszkodzeń, aby mógł działać.
Czasami jednak system musi wyjść poza jednostkę otrzymującą. Aby kontynuować moją odpowiedź dla Erica Undersandera, byłoby trywialne dodać system, który uzyskuje dostęp do mapy gry i szuka jednostek z FallsDownLaughingComponent w przestrzeni x jednostki otrzymującej obrażenia, a następnie wysyła im FallDownLaughingEvent. System ten musiałby zostać zaplanowany tak, aby przyjmował zdarzenie po systemie uszkodzeń, jeśli zdarzenie uszkodzenia nie zostało w tym momencie anulowane, obrażenia zostały zadane.
Jednym z problemów, które pojawiły się, było zapewnienie, że zdarzenia odpowiedzi są przetwarzane w kolejności, w jakiej zostały wysłane, biorąc pod uwagę, że niektóre odpowiedzi mogą powodować pojawienie się dodatkowych odpowiedzi. Przykład:
Gracz porusza się, powodując wysłanie zdarzenia ruchu do bytu gracza i podniesienia go przez system ruchu.
W kolejce: ruch
Jeśli ruch jest dozwolony, system dostosowuje pozycję graczy. Jeśli nie (gracz próbował wpaść na przeszkodę), oznacza to zdarzenie jako anulowane, co powoduje, że menedżer jednostki odrzuca je zamiast wysyłać do kolejnych systemów. Na końcu listy systemów zainteresowanych wydarzeniem znajduje się TurnFinishedSystem, który potwierdza, że gracz spędził swoją turę na poruszaniu postacią i że jego / jej tura się skończyła. Powoduje to, że zdarzenie TurnOver jest wysyłane do encji gracza i umieszczane w kolejce.
W kolejce: TurnOver
Teraz powiedz, że gracz nadepnął na pułapkę, powodując obrażenia. TrapSystem otrzymuje komunikat o ruchu przed TurnFinishedSystem, więc zdarzenie uszkodzenia jest wysyłane jako pierwsze. Teraz kolejka wygląda następująco:
W kolejce: Obrażenia, TurnOver
Jak dotąd wszystko jest w porządku, zdarzenie uszkodzenia zostanie przetworzone, a następnie tura się skończy. Co jednak, jeśli dodatkowe zdarzenia zostaną wysłane w odpowiedzi na uszkodzenie? Teraz kolejka zdarzeń wyglądałaby następująco:
W kolejce: Damage, TurnOver, ResponseToDamage
Innymi słowy, tura skończy się, zanim zostaną przetworzone jakiekolwiek odpowiedzi na obrażenia.
Aby rozwiązać ten problem, wykorzystałem dwie metody wysyłania zdarzeń: wysyłanie (zdarzenie, encja) i odpowiedź (zdarzenie, eventToRespondTo, encja).
Każde zdarzenie prowadzi rejestr poprzednich zdarzeń w łańcuchu odpowiedzi, a za każdym razem, gdy używana jest metoda respond (), zdarzenie, na które udzielono odpowiedzi (i każde zdarzenie w jego łańcuchu odpowiedzi) kończy się na szczycie łańcucha w zdarzeniu używanym do odpowiedzieć za pomocą. Zdarzenie ruchu początkowego nie ma takich zdarzeń. Kolejna odpowiedź na obrażenia ma zdarzenie ruchu na swojej liście.
Ponadto tablica o zmiennej długości jest używana do przechowywania wielu kolejek zdarzeń. Za każdym razem, gdy menedżer odbierze zdarzenie, jest ono dodawane do kolejki w indeksie w tablicy, który odpowiada ilości zdarzeń w łańcuchu odpowiedzi. Zatem początkowe zdarzenie ruchu jest dodawane do kolejki w [0], a obrażenia, a także zdarzenia TurnOver są dodawane do osobnej kolejki w [1], ponieważ oba zostały wysłane jako odpowiedzi na ruch.
Gdy zostaną wysłane odpowiedzi na zdarzenie uszkodzenia, będą one zawierały zarówno samo zdarzenie uszkodzenia, jak i ruch, ustawiając je w kolejce o indeksie [2]. Dopóki indeks [n] zawiera zdarzenia w swojej kolejce, zdarzenia te będą przetwarzane przed przejściem do [n-1]. To daje kolejność przetwarzania:
Ruch -> Obrażenia [1] -> Reakcja na uszkodzenie [2] -> [2] jest pusty -> TurnOver [1] -> [1] jest pusty -> [0] jest pusty
źródło