Dobre pytanie! Zanim przejdę do konkretnych pytań, które zadałeś, powiem: nie lekceważ potęgi prostoty. Tenpn ma rację. Należy pamiętać, że wszystko, co próbujesz zrobić z tymi podejściami, to znaleźć elegancki sposób na odroczenie wywołania funkcji lub oddzielenie dzwoniącego od odbiorcy. Mogę polecić coroutines jako zaskakująco intuicyjny sposób na złagodzenie niektórych z tych problemów, ale to trochę nie na temat. Czasami lepiej jest po prostu wywołać funkcję i żyć z faktem, że istota A jest sprzężona bezpośrednio z bytem B. Zobacz YAGNI.
To powiedziawszy, użyłem i byłem zadowolony z modelu sygnału / gniazda połączonego z prostym przekazywaniem wiadomości. Użyłem go w C ++ i Lua do dość udanego tytułu iPhone'a, który miał bardzo napięty harmonogram.
W przypadku sygnału / gniazda, jeśli chcę, aby jednostka A zrobiła coś w odpowiedzi na coś, co zrobiła istota B (np. Odblokowała drzwi, gdy coś umiera), mógłbym mieć jednostkę A subskrybującą bezpośrednio zdarzenie śmierci jednostki B. Albo być może jednostka A zasubskrybuje każdą z grup jednostek, zwiększy licznik każdego wystrzelonego zdarzenia i odblokuje drzwi po śmierci N z nich. Ponadto „grupa jednostek” i „N z nich” zwykle byłyby projektowane i zdefiniowane w danych poziomu. (Nawiasem mówiąc, jest to jeden z obszarów, w którym rogowce mogą naprawdę świecić, np. WaitForMultiple („Dying”, entA, entB, entC); door.Unlock ();)
Ale może to być uciążliwe, jeśli chodzi o reakcje ściśle powiązane z kodem C ++ lub z natury efemeryczne zdarzenia w grze: zadawanie obrażeń, przeładowywanie broni, debugowanie, oparte na lokalizacji informacje zwrotne AI oparte na graczach. W tym miejscu przekazywanie wiadomości może wypełnić luki. Zasadniczo sprowadza się do czegoś w rodzaju „powiedz wszystkim bytom w tym obszarze, by otrzymały obrażenia w ciągu 3 sekund” lub „ilekroć ukończysz fizykę, aby dowiedzieć się, kogo zastrzeliłem, powiedz im, aby uruchomiły tę funkcję skryptu”. Trudno wymyślić, jak to zrobić ładnie, używając publikowania / subskrypcji lub sygnału / automatu.
Może to być łatwo przesada (w porównaniu z przykładem tenpn). Może być również niewydajnym wzdęciem, jeśli masz dużo akcji. Ale pomimo swoich wad, podejście do „wiadomości i zdarzeń” bardzo dobrze łączy się ze skryptowym kodem gry (np. W Lua). Kod skryptu może definiować własne komunikaty i zdarzenia i reagować na nie bez względu na kod C ++. A kod skryptu może z łatwością wysyłać wiadomości, które wyzwalają kod C ++, takie jak zmiana poziomów, odtwarzanie dźwięków, a nawet po prostu pozwalanie broni ustawić, ile szkód zadaje wiadomość TakeDamage. Zaoszczędziło mi to mnóstwo czasu, ponieważ nie musiałem ciągle wygłupiać się z luabind. Pozwoliło mi to zachować cały mój kod luabind w jednym miejscu, ponieważ nie było go wiele. Po prawidłowym sprzężeniu
Z mojego doświadczenia w przypadku użycia # 2 wynika, że lepiej jest traktować to jako wydarzenie w innym kierunku. Zamiast pytać o zdrowie jednostki, uruchom zdarzenie / wyślij wiadomość za każdym razem, gdy zdrowie wprowadzi znaczącą zmianę.
Jeśli chodzi o interfejsy, btw, skończyłem z trzema klasami do wdrożenia tego wszystkiego: EventHost, EventClient i MessageClient. EventHosts tworzą boksy, EventClients subskrybują / łączą się z nimi, a MessageClients kojarzą delegata z wiadomością. Zauważ, że cel delegowany przez MessageClient niekoniecznie musi być tym samym obiektem, który jest właścicielem powiązania. Innymi słowy, MessageClients mogą istnieć wyłącznie w celu przekazywania wiadomości do innych obiektów. FWIW, metafora host / klient jest w pewnym sensie nieodpowiednia. Source / Sink może być lepszym pomysłem.
Przepraszam, trochę się tam bawiłem. To moja pierwsza odpowiedź :) Mam nadzieję, że to miało sens.
Zapytałeś, jak to robią komercyjne gry. ;)
źródło
Poważniejsza odpowiedź:
Często widziałem tablice. Proste wersje to nic innego jak rozpórki, które są aktualizowane o rzeczy takie jak HP jednostki, które jednostki mogą następnie sprawdzać.
Twoje tablice mogą być albo widokiem świata na ten byt (zapytaj tablicę B, jaka jest jego HP), albo widokiem bytu na świat (A pyta swoją tablicę, aby zobaczyć, jaki jest HP celu A).
Jeśli zaktualizujesz tablice tylko w punkcie synchronizacji w ramce, możesz później odczytać je z dowolnego wątku, co znacznie ułatwia implementację wielowątkowości.
Bardziej zaawansowane tablice mogą przypominać tablice skrótów, odwzorowując ciągi znaków na wartości. Jest to łatwiejsze w utrzymaniu, ale oczywiście wiąże się z kosztami w czasie wykonywania.
Tablica jest tradycyjnie tylko komunikacją jednokierunkową - nie poradziłaby sobie z wymykaniem się uszkodzeń.
źródło
long long int
s lub podobne, w czystym systemie ECS.)Trochę przestudiowałem ten problem i widziałem fajne rozwiązanie.
Zasadniczo chodzi o podsystemy. Jest podobny do pomysłu na tablicę wspomnianego przez tenpn.
Jednostki są wykonane z komponentów, ale są to tylko torby własności. Żadne zachowanie nie jest zaimplementowane w samych podmiotach.
Powiedzmy, że istoty mają komponent Zdrowia i komponent Obrażeń.
Następnie masz program MessageManager i trzy podsystemy: ActionSystem, DamageSystem, HealthSystem. W pewnym momencie ActionSystem dokonuje obliczeń na świecie gry i generuje zdarzenie:
To zdarzenie jest publikowane w menedżerze komunikatów MessageManager. Teraz w pewnym momencie Menedżer komunikatów przegląda oczekujące komunikaty i stwierdza, że system Damage subskrybuje komunikaty HIT. Teraz MessageManager dostarcza komunikat HIT do systemu Damage. DamageSystem przegląda listę jednostek, które mają komponent Obrażeń, oblicza punkty obrażeń w zależności od siły trafienia lub innego stanu obu jednostek itp. I publikuje zdarzenie
HealthSystem zasubskrybował komunikaty USZKODZENIA, a teraz, gdy MessageManager publikuje komunikat USZKODZENIE do HealthSystem, HealthSystem ma dostęp zarówno do encji bytu_A, jak i encji_B z ich komponentami zdrowia, więc ponownie HealthSystem może wykonać swoje obliczenia (i może opublikować odpowiednie zdarzenie do menedżera komunikatów).
W takim silniku gry format komunikatów jest jedynym połączeniem między wszystkimi komponentami i podsystemami. Podsystemy i podmioty są całkowicie niezależne i nieświadome siebie.
Nie wiem, czy jakiś prawdziwy silnik gry wdrożył ten pomysł, czy nie, ale wydaje się dość solidny i czysty i mam nadzieję, że kiedyś go wdrożę dla mojego silnika gry na poziomie hobbystycznym.
źródło
entity_b->takeDamage();
)Dlaczego nie mieć globalnej kolejki komunikatów, na przykład:
Z:
I na koniec obsługi pętli / zdarzeń:
Myślę, że to jest wzorzec Dowodzenia. I
Execute()
jest czystym wirtualnym, wEvent
którym pochodne definiują i robią różne rzeczy. Więc tu:źródło
Jeśli grasz w trybie dla jednego gracza, po prostu użyj metody obiektów docelowych (jak sugeruje tenpn).
Jeśli jesteś (lub chcesz wesprzeć) grę wieloosobową (a dokładniej multiclient), użyj kolejki poleceń.
źródło
Powiedziałbym: nie używaj żadnego, o ile nie potrzebujesz natychmiastowej informacji zwrotnej od obrażeń.
Jednostka / komponent / coś przyjmującego obrażenia powinna popchnąć zdarzenia do lokalnej kolejki zdarzeń lub do systemu na równym poziomie, który przechowuje zdarzenia powodujące uszkodzenia.
Następnie powinien istnieć system nakładania z dostępem do obu jednostek, który żąda zdarzeń od obiektu a i przekazuje je do obiektu b. Nie tworząc ogólnego systemu zdarzeń, którego można użyć z dowolnego miejsca do przekazania zdarzenia do dowolnego miejsca w dowolnym momencie, tworzy się jawny przepływ danych, który zawsze ułatwia debugowanie kodu, łatwiej mierzy wydajność, łatwiej jest zrozumieć i odczytać, a często prowadzi ogólnie do bardziej dobrze zaprojektowanego systemu.
źródło
Po prostu zadzwoń. Nie wykonuj request-hp śledzonego przez query-hp - jeśli zastosujesz się do tego modelu, znajdziesz się w świecie bólu.
Możesz także przyjrzeć się Mono Continuations. Myślę, że byłoby idealnie dla NPC.
źródło
Co więc się stanie, jeśli mamy graczy A i B próbujących się trafić w ten sam cykl aktualizacji ()? Załóżmy, że aktualizacja () dla odtwarzacza A zdarza się przed aktualizacją () dla odtwarzacza B w cyklu 1 (lub zaznacz, czy jakkolwiek to nazwiesz). Mogę wymyślić dwa scenariusze:
Natychmiastowe przetwarzanie za pośrednictwem wiadomości:
To niesprawiedliwe, gracze A i B powinni uderzyć się nawzajem, gracz B zmarł przed uderzeniem A tylko dlatego, że ten byt / obiekt gry otrzymał aktualizację () później.
W kolejce wiadomości
Znów jest to niesprawiedliwe .. gracz A powinien zdobyć punkty życia w tej samej turze / cyklu / tyknięciu!
źródło
pEntity->Flush( pMessages );
. Gdy encja_A generuje nowe zdarzenie, encja_B nie odczytuje go w tej ramce (ma również szansę na przyjęcie mikstury), a następnie obaj otrzymują obrażenia, a następnie przetwarzają komunikat o uzdrawianiu mikstury, który byłby ostatni w kolejce . Gracz B i tak wciąż umiera, ponieważ wiadomość mikstury jest ostatnią w kolejce: P, ale może być przydatna w przypadku innych rodzajów wiadomości, takich jak usuwanie wskaźników do martwych istot.