Skrypty i filmy bez wątków

12

Mam problemy z implementacją skryptów w silniku gry. Mam tylko kilka wymagań: powinien być intuicyjny, nie chcę pisać niestandardowego języka, analizatora składni i interpretera oraz nie chcę używać wątków. (Jestem pewien, że istnieje prostsze rozwiązanie; nie potrzebuję kłopotów z wieloma wątkami logiki gry.) Oto przykładowy skrypt w Pythonie (inaczej pseudokod):

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    alice.walk_to(bob)
    if bob.can_see(alice):
        bob.say("Hello again!")
    else:
        alice.say("Excuse me, Bob?")

Ten epicki kawałek historii stwarza problemy z implementacją. Nie mogę po prostu ocenić całej metody na raz, ponieważ walk_towymaga to czasu gry. Jeśli powróci od razu, Alice zacznie podchodzić do Boba i (w tej samej ramce) przywitać się (lub przywitać). Ale jeśli walk_tozadzwoni blokujący, który powróci, gdy dotrze do Boba, moja gra utknie, ponieważ blokuje ten sam wątek wykonania, który zmusiłby Alice do chodzenia.

Zastanawiałem się nad tym, by każda funkcja kolejkowała akcję - alice.walk_to(bob)przesuwałaby obiekt do kolejki, która odpadałaby po tym, jak Alice dotarła do Boba, gdziekolwiek był. Jest to bardziej subtelnie zepsute: ifgałąź jest oceniana natychmiast, więc Bob może powitać Alice, nawet jeśli odwróci się do niej plecami.

Jak inne silniki / ludzie radzą sobie ze skryptami bez tworzenia wątków? Zaczynam szukać pomysłów w obszarach nie przeznaczonych dla twórców gier, takich jak łańcuchy animacji jQuery. Wydaje się, że powinny istnieć dobre wzorce dla tego rodzaju problemów.

ojrac
źródło
1
+1 za pytanie, ale szczególnie za „Python (pseudokod)” :)
Ricket
Python jest jak posiadanie supermocarstwa.
ojrac

Odpowiedzi:

3

Sposób, w jaki robi to Panda, polega na wywołaniach zwrotnych. Zamiast blokowania byłoby to coś w rodzaju

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    def cb():
        if bob.can_see(alice):
            bob.say("Hello again!")
        else:
            alice.say("Excuse me, Bob?")
    alice.walk_to(bob, cb)

Posiadanie pełnego wywołania zwrotnego pozwala na łączenie tego rodzaju zdarzeń tak głęboko, jak chcesz.

EDYCJA: Przykład JavaScript, ponieważ ma lepszą składnię dla tego stylu:

function dramatic_scene(actors) {
    var alice = actors.alice;
    var bob = actors.bob;
    alice.walk_to(bob, function() {
        if(bob.can_see(alice)) {
            bob.say('Hello again!');
        } else {
            alice.say('Excuse me, Bob?');
        }
     });
}
koderanger
źródło
Działa wystarczająco dla +1, po prostu myślę, że to źle przy projektowaniu treści. Jestem gotów wykonać dodatkową pracę, aby skrypty wyglądały na proste i przejrzyste.
ojrac
1
@ojrac: Właściwie to ta droga jest lepsza, ponieważ tutaj w tym samym skrypcie możesz powiedzieć wielu aktorom, aby zaczęli chodzić w tym samym czasie.
Bart van Heukelom,
Ugh, dobra uwaga.
ojrac
Jednak w celu poprawy czytelności definicję wywołania zwrotnego można zagnieździć w wywołaniu walk_to () lub umieścić po niej (w obu przypadkach: jeśli język obsługuje), aby kod wywoływany później był widoczny później w źródle.
Bart van Heukelom,
Tak, niestety Python nie jest świetny do tego rodzaju składni. W JavaScript wygląda o wiele ładniej (patrz wyżej, nie można tutaj użyć formatowania kodu).
coderanger
13

Termin, którego chcesz tu szukać, to „ coroutines ” (i zazwyczaj jest to słowo kluczowe języka lub nazwa funkcji yield).

Coroutines to komponenty programu, które uogólniają podprogramy, aby umożliwić wiele punktów wejścia do zawieszania i wznawiania wykonywania w określonych lokalizacjach.

Wdrożenie będzie zależeć przede wszystkim od Twojego języka. W przypadku gry chcesz, aby implementacja była jak najlżejsza (lżejsza niż nici, a nawet włókna). Strona Wikipedii (link) zawiera linki do różnych implementacji specyficznych dla języka.

Słyszałem, że Lua ma wbudowane wsparcie dla coroutines. Podobnie GameMonkey.

UnrealScript implementuje to za pomocą tak zwanych „stanów” i „funkcji ukrytych”.

Jeśli używasz C #, możesz spojrzeć na ten post na blogu autorstwa Nicka Gravelyn.

Dodatkowo pomysł „łańcuchów animacji”, choć nie to samo, jest praktycznym rozwiązaniem tego samego problemu. Nick Gravelyn ma również implementację tego języka C # .

Andrew Russell
źródło
Niezły haczyk, Tetrad;)
Andrew Russell
To jest naprawdę dobre, ale nie jestem pewien, czy dostanie mnie w 100%. Wygląda na to, że coroutines pozwalają na poddanie się metodzie wywoływania, ale chcę sposób na poddanie się ze skryptu Lua, aż do stosu do kodu C # bez pisania podczas (walk_to ()! = Done) {fed}.
ojrac
@ojrac: Nie wiem o Lui, ale jeśli używasz metody Nicka Gravelyna w języku C #, możesz zwrócić delegata (lub obiekt zawierający taki obiekt), który ma warunek sprawdzenia przez menedżera skryptów (kod Nicka tylko zwraca czas co jest domyślnie warunkowe). Możesz nawet sprawić, że ukryte funkcje same zwrócą delegata, abyś mógł napisać: yield return walk_to();w swoim skrypcie.
Andrew Russell,
Wydajność w C # jest niesamowita, ale optymalizuję swoje rozwiązanie pod kątem prostych, trudnych do zrobienia skryptów. Łatwiej mi wyjaśnić oddzwanianie niż wydajność, więc przyjmuję inną odpowiedź. Dałbym +2, gdybym mógł.
ojrac
1
Zwykle nie musisz wyjaśniać wywołania dochodu - możesz na przykład zawinąć to w funkcję „walk_to”.
Kylotan
3

brak wątków jest mądry.

Większość silników do gier działa jako seria etapów modułowych z pamięcią sterującą każdym etapem. W przypadku „przejścia do przykładu” zazwyczaj masz etap sztucznej inteligencji, w którym chodzone postacie znajdują się w stanie, w którym nie powinny szukać wrogów do zabicia, etap animacji, w którym powinni uruchomić animację X, etap fizyki (lub etap symulacji), gdzie aktualizowana jest ich rzeczywista pozycja itp.

w powyższym przykładzie „alicja” jest aktorem złożonym z kawałków, które żyją na wielu z tych etapów, więc blokujący aktor call.walk_to (lub coroutine, który wywołujesz next () raz na klatkę) prawdopodobnie nie miałby odpowiedniego kontekstu podejmować wiele decyzji.

Zamiast tego funkcja „start_walk_to” prawdopodobnie zrobiłaby coś takiego:

def start_cutscene_walk_to(actor,target):
    actor.ai.setbrain(cutscene_brain)
    actor.physics.nocoll = 1
    actor.anims.force_anim('walk')
    # etc.

Następnie główna pętla uruchamia tik ai, tik fizyki, tik animacji i tik przerywnika, a przerywnik aktualizuje stan każdego z podsystemów w silniku. System przerywników filmowych powinien zdecydowanie śledzić, co robi każdy ze scen przerywnikowych, a system oparty na coroutine dla czegoś liniowego i deterministycznego, jak na przykład scenka przerywnikowa, może mieć sens.

Powodem tej modułowości jest to, że po prostu utrzymuje rzeczy ładne i proste, a dla niektórych systemów (takich jak fizyka i sztuczna inteligencja), musisz znać stan wszystkiego w tym samym czasie, aby poprawnie rozwiązać problemy i utrzymać grę w spójnym stanie .

mam nadzieję że to pomoże!

Aaron Brady
źródło
Podoba mi się to, do czego zmierzasz, ale tak naprawdę mocno odczuwam wartość actor.walk_to ([inny aktor, stała pozycja, a nawet funkcja, która zwraca pozycję]). Moim celem jest dostarczenie prostych, zrozumiałych narzędzi i radzenie sobie z całą złożonością z dala od tworzenia treści w grze. Pomogłeś mi również zrozumieć, że wszystko, czego naprawdę chcę, to sposób traktowania każdego skryptu jako skończonej maszyny stanów.
ojrac
cieszę się, że mogłem pomóc! Czułem, że moja odpowiedź była trochę nie na temat :) zdecydowanie zgadzam się z wartością funkcji actor.walk_to dla osiągnięcia twoich celów, nie mogę się doczekać, aby usłyszeć o twojej realizacji.
Aaron Brady
Wygląda na to, że pójdę z zestawem zwrotnym i połączonymi funkcjami w stylu jQuery. Zobacz zaakceptowaną odpowiedź;)
ojrac