Jak działa komunikacja encji?

115

Mam dwa przypadki użytkowników:

  1. Jak entity_Awysłać take-damagewiadomość entity_B?
  2. Jak entity_Azapyta entity_BHP?

Oto, co do tej pory spotkałem:

  • Kolejka wiadomości
    1. entity_Atworzy take-damagewiadomość i publikuje ją w entity_Bkolejce wiadomości.
    2. entity_Atworzy query-hpwiadomość i publikuje ją na entity_B. entity_Bw zamian tworzy response-hpwiadomość i publikuje ją na entity_A.
  • Publikuj / subskrybuj
    1. entity_Bsubskrybuje take-damagewiadomości (być może z pewnym filtrowaniem zapobiegawczym, więc dostarczane są tylko odpowiednie wiadomości). entity_Atworzy take-damagekomunikat, który odwołuje się entity_B.
    2. entity_Asubskrybuje update-hpwiadomości (ewentualnie filtrowane). Każda ramka entity_Bnadaje update-hpwiadomości.
  • Sygnał / gniazda
    1. ???
    2. entity_Ałączy się update-hpgniazdo do entity_B„s update-hpsygnału.

Czy jest coś lepszego? Czy dobrze rozumiem, w jaki sposób te schematy komunikacyjne wiązałyby się z systemem encji silnika gry?

deft_code
źródło

Odpowiedzi:

67

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.

BRaffle
źródło
Dziękuję za odpowiedź. Świetne spostrzeżenia. Powodem dla którego projektuję przekazywanie wiadomości jest Lua. Chciałbym móc tworzyć nowe bronie bez nowego kodu C ++. Więc twoje myśli odpowiedziały na niektóre z moich niezadawanych pytań.
deft_code
Jeśli chodzi o coroutines, to ja też jestem wielkim zwolennikiem coroutines, ale nigdy nie gram z nimi w C ++. Miałem niejasną nadzieję, że wykorzystam coroutines w kodzie lua do obsługi blokujących połączeń (np. Czekania na śmierć). Czy było warto? Obawiam się, że może mnie zaślepić moje intensywne pragnienie coroutines w c ++.
deft_code
Wreszcie, jaka była gra na iPhone'a? Czy mogę uzyskać więcej informacji na temat używanego systemu encji?
deft_code
2
System encji był głównie w C ++. Na przykład istniała klasa Imp, która zajmowała się zachowaniem Imp. Lua może zmieniać parametry Imp przy spawnowaniu lub za pośrednictwem wiadomości. Celem Lua było dopasowanie się do napiętego harmonogramu, a debugowanie kodu Lua jest bardzo czasochłonne. Używaliśmy Lua do tworzenia skryptów poziomów (które byty idą gdzie, zdarzenia, które mają miejsce po trafieniu wyzwalaczy). W Lua powiedzielibyśmy takie rzeczy, jak SpawnEnt („Imp”), w którym Imp to ręcznie zarejestrowane stowarzyszenie fabryczne. Zawsze pojawiałby się w jednej globalnej puli bytów. Ładne i proste. Użyliśmy dużo smart_ptr i poor_ptr.
BRaffle,
1
BananaRaffle: Czy powiedziałbyś, że jest to dokładne podsumowanie twojej odpowiedzi: „Wszystkie 3 opublikowane przez Ciebie rozwiązania mają swoje zastosowanie, podobnie jak inne. Nie szukaj jednego idealnego rozwiązania, po prostu użyj tego, czego potrzebujesz, tam gdzie ma to sens . ”
Ipsquiggle
76
// in entity_a's code:
entity_b->takeDamage();

Zapytałeś, jak to robią komercyjne gry. ;)

tenpn
źródło
8
Głos w dół? Poważnie, tak zwykle się to robi! Systemy jednostek są świetne, ale nie pomagają osiągnąć pierwszych kamieni milowych.
tenpn
Robię gry Flash profesjonalnie i tak to robię. Dzwonisz do wroga.damage (10), a następnie wyszukujesz wszelkie potrzebne informacje z publicznych programów pobierających.
Iain
7
Jest to poważny sposób, w jaki robią to komercyjne silniki gier. On nie żartuje. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer itp.) Zwykle działa tak.
AA Grapsas
3
Czy gry komercyjne też źle zapisują „obrażenia”? :-P
Ricket
15
Tak, między innymi wyrządzają szkody w pisowni. :)
LearnCocos2D,
17

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ń.

tenpn
źródło
Nigdy wcześniej nie słyszałem o modelu tablicowym.
deft_code
Są również dobre do zmniejszania zależności, podobnie jak kolejka zdarzeń lub model publikowania / subskrypcji.
tenpn
2
Jest to również kanoniczna „definicja” tego, jak „idealny” system E / C / S „powinien działać”. Komponenty tworzą tablicę; Systemy to kod działający na jego podstawie. (Podmioty, oczywiście, są po prostu long long ints lub podobne, w czystym systemie ECS.)
BRPocock
6

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:

HIT, source=entity_A target=entity_B power=5

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

DAMAGE, source=entity_A target=entity_B amount=7

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.

JustAMartin
źródło
Jest to znacznie lepsza odpowiedź niż zaakceptowana odpowiedź IMO. Oddzielony, możliwy do utrzymania i rozszerzalny (a także nie katastrofa łącząca, jak żart entity_b->takeDamage();)
Danny Jarosławski
4

Dlaczego nie mieć globalnej kolejki komunikatów, na przykład:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

Z:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

I na koniec obsługi pętli / zdarzeń:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

Myślę, że to jest wzorzec Dowodzenia. I Execute()jest czystym wirtualnym, w Eventktórym pochodne definiują i robią różne rzeczy. Więc tu:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}
Kaczka komunistyczna
źródło
3

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ń.

  • Kiedy A zadaje obrażenia B klientowi 1, po prostu ustaw kolejkę w zdarzeniu uszkodzenia.
  • Zsynchronizuj kolejki poleceń przez sieć
  • Obsługuj polecenia w kolejce po obu stronach.
Andreas
źródło
2
Jeśli poważnie myślisz o unikaniu oszustwa, A wcale nie zadaje B klientowi. Klient będący właścicielem A wysyła polecenie „ataku B” do serwera, co robi dokładnie to, co powiedział tenpn; serwer następnie synchronizuje ten stan ze wszystkimi odpowiednimi klientami.
@Joe: Tak, jeśli istnieje serwer, który warto rozważyć, ale czasami można zaufać klientowi (np. Na konsoli), aby uniknąć dużego obciążenia serwera.
Andreas
2

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.

Szymon
źródło
1

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.

James Bellinger
źródło
1

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:

  1. Natychmiastowe przetwarzanie za pośrednictwem wiadomości:

    • gracz A.Update () widzi, że gracz chce trafić B, gracz B dostaje komunikat z informacją o obrażeniach.
    • gracz B.HandleMessage () aktualizuje punkty życia gracza B (umiera)
    • gracz B.Update () widzi, że gracz B nie żyje .. nie może zaatakować gracza A.

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.

  1. W kolejce wiadomości

    • Gracz A.Update () widzi, że gracz chce trafić B, gracz B otrzymuje komunikat z informacją o uszkodzeniach i przechowuje go w kolejce
    • Gracz A.Update () sprawdza swoją kolejkę, jest pusta
    • gracz B.Update () najpierw sprawdza ruchy, więc gracz B wysyła wiadomość do gracza A z uszkodzeniem
    • player B.Update () obsługuje także wiadomości w kolejce, przetwarza uszkodzenia od gracza A.
    • Nowy cykl (2): Gracz A chce wypić miksturę zdrowia, więc gracz A.Aktualizacja () zostaje wywołana, a ruch jest przetwarzany
    • Gracz A.Update () sprawdza kolejkę komunikatów i przetwarza uszkodzenia od gracza B.

Znów jest to niesprawiedliwe .. gracz A powinien zdobyć punkty życia w tej samej turze / cyklu / tyknięciu!


źródło
4
Naprawdę nie odpowiadasz na pytanie, ale myślę, że twoja odpowiedź sama w sobie byłaby doskonałym pytaniem. Dlaczego nie zapytać, jak rozwiązać taki „niesprawiedliwy” priorytet?
bummzack
Wątpię, czy w większości gier chodzi o tę niesprawiedliwość, ponieważ aktualizują się tak często, że rzadko stanowi to problem. Jednym prostym obejściem jest przełączanie między iterowaniem do przodu i do tyłu przez listę encji podczas aktualizacji.
Kylotan
Używam 2 wywołań, więc wywołuję Update () dla wszystkich jednostek, a następnie po pętli iteruję ponownie i wywołuję coś podobnego 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.
Pablo Ariel
Myślę, że na poziomie ramowym większość implementacji gier jest po prostu niesprawiedliwa. jak powiedział Kylotan.
v.oddou,
Ten problem jest niezwykle łatwy do rozwiązania. Po prostu zadajcie sobie nawzajem obrażenia w modułach obsługi wiadomości lub cokolwiek innego. Zdecydowanie nie powinieneś oznaczać gracza jako martwego w module obsługi wiadomości. W „Update ()” po prostu robisz „if (hp <= 0) die ();” (na przykład na początku „Update ()”). W ten sposób oboje mogą się nawzajem zabijać. Ponadto: Często nie zadajesz obrażeń bezpośrednio graczowi, ale przez jakiś pośredni przedmiot, taki jak kula.
Tara,