Trennung von Weltzustand und Animation in einem rundenbasierten Spiel

9

Wie gehen Sie mit der Trennung von Animation und Weltzustand in einem rundenbasierten Spiel um? Ich arbeite derzeit an einem 2D-Grid-basierten Spiel. Der folgende Code wird zur besseren Erläuterung vereinfacht.

Wenn sich ein Schauspieler bewegt, möchte ich den Fluss der Runden unterbrechen, während die Kreatur animiert und sich an die neue Position bewegt. Andernfalls könnte der Bildschirm erheblich hinter dem Weltzustand zurückbleiben, was zu einem seltsamen visuellen Erscheinungsbild führen würde. Ich möchte auch Animationen haben, die den Spielfluss nicht blockieren - ein Partikeleffekt kann sich über mehrere Runden entfalten, ohne das Gameplay zu beeinträchtigen.

Ich habe zwei Arten von Animationen eingeführt, die ich blockierende Animationen und nicht blockierende Animationen nenne. Wenn der Spieler sich bewegen möchte, lautet der Code, der ausgeführt wird

class Actor {
    void move(PositionInfo oldPosition, PositionInfo newPosition) {
        if(isValidMove(oldPosition, newPosition) {
             getGame().pushBlockingAnimation(new MoveAnimation(this, oldPosition, newPosition, ....));
             player.setPosition(newPosition);
        }
    }
}

Dann macht die Hauptaktualisierungsschleife:

class Game {
    void update(float dt) {
        updateNonBlockingAnimations(dt); //Effects like particle explosions, damage numbers, etc. Things that don't block the flow of turns.
        if(!mBlockingAnimations.empty()) {
            mBlockingAnimations.get(0).update(dt);
        } else {
             //.. handle the next turn. This is where new animations will be enqueued..//
        }
        cleanUpAnimations(); // remove finished animations
    }
}

... wo die Animation die Bildschirmposition des Schauspielers aktualisiert.

Eine andere Idee, die ich implementiere, ist eine gleichzeitige Blockierungsanimation, bei der mehrere Blockierungsanimationen gleichzeitig aktualisiert werden, die nächste Runde jedoch erst stattfinden würde, wenn alle abgeschlossen sind.

Scheint dies eine vernünftige Art zu sein, Dinge zu tun? Hat jemand irgendwelche Vorschläge oder sogar Hinweise darauf, wie andere ähnliche Spiele so etwas tun?

mdkess
quelle

Antworten:

2

Wenn Ihr System für Sie funktioniert, sehe ich keinen Grund, dies nicht zu tun.

Ein Grund: Es kann chaotisch werden, wenn Sie die Konsequenzen von "player.setPosition (newPosition)" nicht leicht beurteilen können. nicht mehr.

Beispiel (nur um Dinge zu erfinden): Stellen Sie sich vor, Sie bewegen den Player mit Ihrer setPosition auf eine Falle. Der Aufruf von "player.setPosition" selbst löst einen anderen Aufruf von ... beispielsweise einem Trap-Listener-Code aus, der wiederum eine blockierende Animation von selbst sendet, die einen "verletzenden Spritzer" über sich selbst anzeigt.

Sie sehen das Problem: Der Spritzer wird gleichzeitig mit der Bewegung des Spielers in Richtung der Falle angezeigt . (Nehmen wir hier an, dass Sie dies nicht möchten, sondern dass die Trap-Animation direkt nach Abschluss des Zuges abgespielt wird;)).

Solange Sie in der Lage sind, alle Konsequenzen Ihrer Handlungen im Auge zu behalten, die Sie nach diesen "pushBlockingAnimation" -Aufrufen ausführen, ist alles in Ordnung.

Möglicherweise müssen Sie damit beginnen, die Spielelogik in "Blocking Something" -Klassen zu verpacken, damit sie nach Abschluss der Animation in die Warteschlange gestellt werden können. Oder Sie übergeben einen Rückruf "callThisAfterAnimationFinishes" an pushBlockingAnimation und verschieben die setPosition in die Rückruffunktion.

Ein anderer Ansatz wären Skriptsprachen. Zum Beispiel hat Lua "coroutine.yield", das die Funktion mit einem speziellen Status zurückgibt, damit sie später aufgerufen werden kann, um mit der nächsten Anweisung fortzufahren. Auf diese Weise können Sie leicht warten, bis das Ende der Animation die Spielelogik ausgeführt hat, ohne das "normale" Programmablauf-Aussehen Ihres Codes zu beeinträchtigen. (bedeutet, dass Ihr Code immer noch ein bisschen wie "playAnimation (); setPosition ()" aussieht, anstatt Rückrufe weiterzugeben und die Spielelogik über mehrere Funktionen hinweg zu überladen).

Unity3D (und mindestens ein anderes mir bekanntes Spielsystem) enthält die C # -Anweisung "Yield Return", um dies direkt im C # -Code zu simulieren.

// instead of simple call to move(), you have to "iterate" it in a foreach-like loop
IEnumerable<Updatable> move(...) {
    // playAnimation returns an object that contain an update() and finished() method.
    // the caller will not iterate over move() again until the object is "finished"
    yield return getGame().playAnimation(...)
    // the next statement will be executed *after* the animation finished
    player.setPosition(newPosition);
}

In diesem Schema erhält der Aufruf von move () jetzt natürlich die ganze Drecksarbeit. Sie würden diese Technik wahrscheinlich nur für bestimmte Funktionen anwenden, bei denen Sie genau wissen, wer sie von wo aus aufruft. Nehmen Sie dies also nur als Grundidee, wie andere Engines organisiert sind - es ist wahrscheinlich viel zu kompliziert für Ihr aktuelles spezifisches Problem.

Ich empfehle Ihnen, sich an Ihre pushBlockingAnimation-Idee zu halten, bis Sie Probleme in der Praxis haben. Dann ändern Sie es. ;)

Imi
quelle
Vielen Dank, dass Sie sich die Zeit genommen haben, diese Antwort zu schreiben. Ich weiß das wirklich zu schätzen.
Mdkess