Jak rozwinąć gamestate składający się z bytu w grze turowej?

9

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:

  1. 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.
  2. 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?

Aeris130
źródło
Tak naprawdę nie chcesz sondować wydarzeń. Zdarzenie ma miejsce tylko wtedy, gdy ma miejsce. Czy Artemis nie pozwala systemom komunikować się ze sobą?
Sidar
Robi to, ale tylko przez połączenie ich metodami.
Aeris130

Odpowiedzi:

3

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.

Eric Undersander
źródło
Zrobienie tego w ten sposób spowodowałoby, że funkcja hit () byłaby balonem w miarę dodawania kolejnych zachowań. Załóżmy, że wróg spada ze śmiechu za każdym razem, gdy miecz uderza w cel (dowolny cel) w jego polu widzenia. Czy HandleWeaponHit naprawdę powinien być odpowiedzialny za wywołanie tego?
Aeris130
1
Masz ściśle powiązaną sekwencję walki, więc tak, uderzenie odpowiada za wywołanie efektów. Nie wszystko musi być podzielone na małe systemy, niech ten uchwyt jeden system to dlatego, że tak naprawdę to „Walka System” i obsługuje ... walka ...
Patrick Hughes
3

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:

  • Dodaj EventBus, aby umożliwić systemom wydawanie / subskrybowanie obiektów zdarzeń (w rzeczywistości czystych danych, ale chyba nie komponentu)
  • Utwórz komponenty dla każdego stanu pośredniego

Przykład:

  • UserInputSystem odpala zdarzenie Attack z [DamageDealerEntity, DamageReceiverEntity, informacje o użyciu umiejętności / broni]
  • CombatSystem jest subskrybowany i oblicza szansę na unik dla DamageReceiver. Jeśli unikanie zakończy się niepowodzeniem, wywołuje zdarzenie Obrażenia o tych samych parametrach
  • System Damage subskrybuje takie zdarzenie, a tym samym jest uruchamiany
  • DamageSystem używa siły, obrażeń BaseWeapon, rodzaju itp. I zapisuje je do nowego IncomingDamageComponent za pomocą [DamageDealerEntity, FinalOutgoingDamage, DamageType] i dołącza je do jednostki / podmiotów otrzymujących obrażenia
  • DamageSystem odpala OutgoingDamageCalculated
  • ArmorSystem jest przez niego wyzwalany, podnosi Entity z odbiornika lub wyszukuje ten aspekt IncomingDamage w Entities, aby podnieść IncomingDamageComponent (ostatni może być prawdopodobnie lepszy dla wielu ataków z rozproszeniem) i oblicza pancerz i zadane mu obrażenia. Opcjonalnie wyzwala zdarzenia związane z roztrzaskaniem miecza
  • ArmorSystems usuwa IncomingDamageComponent w każdej jednostce i zastępuje ją DamageReceivedComponent z ostatecznymi obliczonymi liczbami, które będą wpływać na HP i redukcję prędkości z ran
  • ArmorSystems wysyła zdarzenie IncomingDamageCalculated
  • System prędkości jest subskrybowany i ponownie oblicza prędkość
  • HealthSystem jest subskrybowany i zmniejsza faktyczne HP
  • itp
  • Jakoś posprzątaj

Plusy:

  • System wyzwala się nawzajem, dostarczając dane pośrednie dla złożonych zdarzeń łańcuchowych
  • Oddzielenie przez EventBus

Cons:

  • Czuję, że łączę dwa sposoby przekazywania rzeczy: w parametrach zdarzeń i w tymczasowych Komponentach. to może być słabe miejsce. Teoretycznie, aby zachować jednorodność rzeczy, mógłbym wywoływać zdarzenia enum bez danych, aby systemy mogły znaleźć implikowane parametry w komponentach encji według aspektu ... Nie jestem jednak pewien, czy jest OK
  • Nie wiem, jak się dowiedzieć, czy wszystkie potencjalnie zainteresowane SystemsHave przetworzyły IncomingDamageCalculated, aby można je było wyczyścić i umożliwić następną kolej. Może jakieś kontrole w CombatSystem ...
Siergiej Jakowlew
źródło
2

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

Aeris130
źródło