Scripting und Cinematics ohne Threading

12

Ich habe Probleme damit, Skripte in meiner Spiel-Engine zu implementieren. Ich habe nur ein paar Anforderungen: Es sollte intuitiv sein, ich möchte keine benutzerdefinierte Sprache, keinen Parser und keinen Interpreter schreiben und ich möchte kein Threading verwenden. (Ich bin mir sicher, dass es eine einfachere Lösung gibt. Ich brauche nicht den Aufwand mehrerer Game Logic-Threads.) Hier ist ein Beispielskript in Python (auch bekannt als Pseudocode):

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?")

Dieses epische Stück Storytelling wirft Implementierungsprobleme auf. Ich kann nicht einfach die ganze Methode auf einmal bewerten, weilwalk_to das Spiel Zeit kostet. Wenn es sofort zurückkommt, würde Alice auf Bob zugehen und (im selben Frame) Hallo sagen (oder begrüßt werden). Aber wenn walk_toes sich um einen Blockierungsaufruf handelt, der zurückkommt, wenn sie Bob erreicht, bleibt mein Spiel hängen, weil er denselben Ausführungsstrang blockiert, der Alice zum Laufen bringen würde.

Ich überlegte, jede Funktion dazu zu bringen, eine Aktion alice.walk_to(bob)in die Warteschlange zu stellen - ein Objekt in eine Warteschlange zu schieben, das auftauchte, sobald Alice Bob erreichte, wo immer er sich befand. Das ist subtiler gebrochen: dieif Zweig wird sofort ausgewertet, sodass Bob Alice vielleicht begrüßt, selbst wenn er ihr den Rücken zugewandt ist.

Wie gehen andere Engines / Leute mit Skripten um, ohne Threads zu erstellen? Ich fange an, in Bereichen, die keine Spieleentwickler sind, wie jQuery-Animationsketten, nach Ideen zu suchen. Es scheint, dass es einige gute Muster für diese Art von Problem geben sollte.

ojrac
quelle
1
+1 für die Frage, aber vor allem für "Python (aka Pseudocode)" :)
Ricket
Python ist wie eine Supermacht.
Ojrac

Antworten:

3

So etwas wie Panda macht man mit Rückrufen. Anstatt zu blockieren, wäre es so etwas wie

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)

Mit einem vollständigen Rückruf können Sie diese Art von Ereignissen so tief wie Sie möchten verketten.

BEARBEITEN: JavaScript-Beispiel, da dies eine bessere Syntax für diesen Stil hat:

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?');
        }
     });
}
coderanger
quelle
Es funktioniert genug für eine +1, ich denke nur, es ist falsch für die Gestaltung von Inhalten. Ich bin bereit, zusätzliche Arbeit zu leisten, damit die Skripte einfach und klar aussehen.
Ojrac
1
@ojrac: Eigentlich ist dieser Weg besser, weil Sie hier im selben Skript mehreren Schauspielern anweisen können, gleichzeitig loszulaufen.
Bart van Heukelom
Ugh, guter Punkt.
Ojrac
Um die Lesbarkeit zu verbessern, kann die Callback-Definition jedoch im Aufruf walk_to () verschachtelt oder nachgestellt werden (für beides gilt: sofern die Sprache dies unterstützt), sodass später aufgerufener Code später in der Quelle angezeigt wird.
Bart van Heukelom
Ja, Python ist für diese Art von Syntax leider nicht besonders gut geeignet. In JavaScript sieht es viel besser aus (siehe oben, hier kann keine Code-Formatierung verwendet werden).
Coderanger
13

Der Begriff, nach dem Sie hier suchen möchten, ist " Koroutinen " (und normalerweise ist dies das Sprachschlüsselwort oder der Funktionsname yield).

Coroutinen sind Programmkomponenten, die Subroutinen so verallgemeinern, dass mehrere Einstiegspunkte zum Unterbrechen und Fortsetzen der Ausführung an bestimmten Stellen möglich sind.

Die Implementierung hängt in erster Linie von Ihrer Sprache ab. Für ein Spiel möchten Sie, dass die Implementierung so leicht wie möglich ist (leichter als Fäden oder sogar Fasern). Die Wikipedia-Seite (verlinkt) enthält einige Links zu verschiedenen sprachspezifischen Implementierungen.

Ich habe gehört, Lua hat eine eingebaute Unterstützung für Coroutinen. Genauso wie GameMonkey.

UnrealScript implementiert dies mit den so genannten "Zuständen" und "latenten Funktionen".

Wenn Sie C # verwenden, können Sie sich diesen Blog-Beitrag von Nick Gravelyn ansehen.

Darüber hinaus ist die Idee der "Animationsketten" eine praktikable Lösung für dasselbe Problem, obwohl sie nicht dasselbe ist. Nick Gravelyn hat auch eine C # -Implementierung davon .

Andrew Russell
quelle
Schöner Fang, Tetrad;)
Andrew Russell
Das ist wirklich gut, aber ich bin nicht sicher, ob es mich zu 100% dorthin bringt. Es sieht so aus, als würden Sie mit Coroutinen der aufrufenden Methode nachgeben, aber ich möchte einen Weg finden, wie Sie mit einem Lua-Skript bis zum C # -Code nachgeben können, ohne dabei (walk_to ()! = Done) {yield} zu schreiben.
Ojrac
@ojrac: Ich weiß nichts über Lua, aber wenn Sie die C # -Methode von Nick Gravelyn verwenden, können Sie einen Delegaten (oder ein Objekt, das einen enthält) zurückgeben, der die Bedingung für die Überprüfung durch Ihren Skriptmanager enthält (Nicks Code gibt nur eine Zeit zurück) das ist implizit eine Bedingung). Sie können sogar die latenten Funktionen selbst den Delegaten zurückgeben lassen, sodass Sie Folgendes schreiben können: yield return walk_to();in Ihr Skript.
Andrew Russell
Die Rendite in C # ist fantastisch, aber ich optimiere meine Lösung für einfaches, unübersichtliches Scripting. Es fällt mir leichter, Rückrufe zu erklären, als Erträge zu erzielen, daher akzeptiere ich die andere Antwort. Ich würde +2, wenn ich könnte.
Ojrac
1
Normalerweise müssen Sie den yield-Aufruf nicht erklären - das können Sie beispielsweise in die Funktion "walk_to" einbinden.
Kylotan 06.09.10
3

Sich nicht verstricken zu lassen ist klug.

Die meisten Game-Engines arbeiten als eine Reihe von modularen Phasen, wobei die einzelnen Phasen von Arbeitsspeicher gesteuert werden. Als Beispiel für das Gehen haben Sie normalerweise eine KI - Phase, in der Ihre gelaufenen Charaktere sich in einem Zustand befinden, in dem sie nicht nach zu tötenden Feinden suchen sollten, eine Animationsphase, in der Animation X ausgeführt werden sollte, eine Physikphase (oder Simulationsphase), in der ihre aktuelle Position aktualisiert wird usw.

In Ihrem obigen Beispiel ist 'alice' ein Schauspieler, der aus Teilen besteht, die in vielen dieser Phasen vorkommen. Ein blockierender actor.walk_to-Aufruf (oder eine Coroutine, die Sie einmal pro Frame als nächstes aufrufen ()), verfügt daher wahrscheinlich nicht über den richtigen Kontext viele Entscheidungen treffen.

Stattdessen würde eine 'start_walk_to'-Funktion wahrscheinlich etwa Folgendes tun:

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

Anschließend werden in der Hauptschleife der Tick ai, der Tick physics, der Tick animation und der Tick cutscene ausgeführt. In der Cutscene wird der Status für jedes Subsystem in Ihrer Engine aktualisiert. Das Cutscene-System sollte auf jeden Fall verfolgen, was jede seiner Cutscenes tut, und ein Coroutine-System für etwas Lineares und Deterministisches wie eine Custscene könnte Sinn machen.

Der Grund für diese Modularität liegt darin, dass die Dinge einfach und schön bleiben und dass Sie bei einigen Systemen (wie Physik und KI) gleichzeitig den Status von allem wissen müssen, um die Dinge richtig aufzulösen und das Spiel in einem konsistenten Zustand zu halten .

hoffe das hilft!

Aaron Brady
quelle
Mir gefällt, was Sie anstreben, aber mir ist der Wert von actor.walk_to ([ein anderer Schauspieler, eine feste Position oder sogar eine Funktion, die eine Position zurückgibt]) sehr wichtig. Mein Ziel ist es, einfache, verständliche Tools bereitzustellen und die gesamte Komplexität zu bewältigen, die nicht zum Erstellen von Inhalten im Spiel gehört. Sie haben mir auch dabei geholfen, zu erkennen, dass ich wirklich nur die Möglichkeit haben möchte, jedes Skript als endliche Zustandsmaschine zu behandeln.
Ojrac
froh, dass ich helfen konnte! Ich hatte das Gefühl, dass meine Antwort ein wenig vom Thema abweicht :) stimme definitiv mit dem Wert einer actor.walk_to-Funktion für die Erreichung Ihrer Ziele überein. Ich freue mich darauf, von Ihrer Implementierung zu hören.
Aaron Brady
Es sieht so aus, als würde ich eine Mischung aus Rückrufen und verketteten Funktionen im Stil von jQuery wählen. Siehe akzeptierte Antwort;)
Ojrac