Techniken zur Verwaltung des Spielzustands?

24

Zunächst beziehe ich mich nicht auf das Szenenmanagement. Ich definiere den Spielstatus lose als jede Art von Status in einem Spiel, der Auswirkungen darauf hat, ob Benutzereingaben aktiviert werden sollen oder ob bestimmte Akteure vorübergehend deaktiviert werden sollen usw.

Nehmen wir als konkretes Beispiel an, es ist ein Spiel des klassischen Battlechess. Nachdem ich versucht habe, die Figur eines anderen Spielers zu nehmen, wird eine kurze Kampfsequenz abgespielt. Während dieser Sequenz darf der Spieler keine Steine ​​bewegen. Wie würden Sie diese Art von Zustandsübergang verfolgen? Eine endliche Zustandsmaschine? Eine einfache Boolesche Prüfung? Es scheint, dass Letzteres nur für ein Spiel mit sehr wenigen Zustandsänderungen dieser Art gut funktionieren würde.

Ich kann mir viele einfache Möglichkeiten überlegen, wie dies mit endlichen Zustandsautomaten gehandhabt werden kann, aber ich kann auch sehen, wie sie schnell außer Kontrolle geraten. Ich bin nur neugierig, ob es eine elegantere Möglichkeit gibt, Spielzustände / -übergänge im Auge zu behalten.

vargonisch
quelle
Haben Sie gamedev.stackexchange.com/questions/1783/game-state-stack und gamedev.stackexchange.com/questions/2423/… ausgecheckt ? Es dreht sich eigentlich alles um dasselbe Konzept, aber mir fällt nichts ein, das besser wäre als eine Zustandsmaschine für den Spielzustand.
michael.bartnett
Mögliches Duplikat: gamedev.stackexchange.com/questions/12664/…
Tetrad

Antworten:

18

Ich bin einmal auf einen Artikel gestoßen, der Ihr Problem ganz elegant löst. Es ist eine grundlegende FSM-Implementierung, die in Ihrer Hauptschleife aufgerufen wird. Ich habe den grundlegenden Überblick über den Artikel im Rest dieser Antwort skizziert.

Ihr grundlegender Spielstatus sieht folgendermaßen aus:

class CGameState
{
    public:
        // Setup and destroy the state
        void Init();
        void Cleanup();

        // Used when temporarily transitioning to another state
        void Pause();
        void Resume();

        // The three important actions within a game loop
        void HandleEvents();
        void Update();
        void Draw();
};

Jeder Spielzustand wird durch eine Implementierung dieser Schnittstelle dargestellt. Für Ihr Battlechess-Beispiel könnte dies folgende Zustände bedeuten:

  • Intro-Animation
  • Hauptmenü
  • Schachbrett-Setup-Animation
  • Spieler bewegen Eingang
  • Spieler bewegen Animation
  • gegner bewegen animation
  • Menü anhalten
  • Endspiel-Bildschirm

Zustände werden in Ihrer State Engine verwaltet:

class CGameEngine
{
    public:
        // Creating and destroying the state machine
        void Init();
        void Cleanup();

        // Transit between states
        void ChangeState(CGameState* state);
        void PushState(CGameState* state);
        void PopState();

        // The three important actions within a game loop
        // (these will be handled by the top state in the stack)
        void HandleEvents();
        void Update();
        void Draw();

        // ...
};

Beachten Sie, dass jeder Status irgendwann einen Zeiger auf CGameEngine benötigt, damit der Status selbst entscheiden kann, ob ein neuer Status eingegeben werden soll. Der Artikel schlägt vor, die CGameEngine als Parameter für HandleEvents, Update und Draw zu übergeben.

Am Ende befasst sich Ihre Hauptschleife nur mit der State Engine:

int main ( int argc, char *argv[] )
{
    CGameEngine game;

    // initialize the engine
    game.Init( "Engine Test v1.0" );

    // load the intro
    game.ChangeState( CIntroState::Instance() );

    // main loop
    while ( game.Running() )
    {
        game.HandleEvents();
        game.Update();
        game.Draw();
    }

    // cleanup the engine
    game.Cleanup();
    return 0;
}
Geist
quelle
17
C für die Klasse? Ew. Das ist jedoch ein guter Artikel - +1.
Die kommunistische Ente
Soweit ich das beurteilen kann, geht es bei dieser Frage explizit nicht um Fragen. Das heißt nicht, dass Sie nicht so damit umgehen konnten, wie Sie es sicherlich könnten, aber wenn Sie nur die Eingabe vorübergehend deaktivieren wollten, ist es meiner Meinung nach sowohl übertrieben als auch wartungsschädlich, eine neue Unterklasse von CGameState abzuleiten, die ausgeführt wird zu 99% mit einer anderen Unterklasse identisch sein.
Kylotan,
Ich würde denken, dass dies stark davon abhängt, wie der Code zusammengekoppelt wird. Ich kann mir eine saubere Trennung zwischen der Auswahl einer Figur und eines Ziels (hauptsächlich UI-Indikatoren und Eingabehandhabung) und einer Animation der Schachfigur zu diesem Ziel vorstellen (eine ganze Brettanimation, bei der sich andere Figuren aus dem Weg bewegen und mit der Bewegung interagieren) Stück usw.), wodurch die Zustände weit davon entfernt sind, identisch zu sein. Dies trennt die Verantwortung und ermöglicht eine einfache Wartung und sogar Wiederverwendbarkeit (Intro-Demo, Wiedergabemodus). Ich denke, dies beantwortet auch die Frage, wie man zeigt, dass die Verwendung eines FSM kein Aufwand sein muss.
Geist
Das ist wirklich toll, danke. Ein wichtiger Punkt, den Sie angesprochen haben, war in Ihrem letzten Kommentar: "Die Verwendung eines FSM muss kein Aufwand sein." Ich hatte mir fälschlicherweise vorgestellt, dass die Verwendung eines FSM die Verwendung von switch-Anweisungen beinhalten würde, was nicht unbedingt der Fall sein muss. Eine weitere wichtige Bestätigung ist, dass jeder Status einen Verweis auf die Game Engine benötigt. Ich fragte mich, wie das sonst funktionieren würde.
Vargonian
2

Ich beginne damit, so etwas so einfach wie möglich zu handhaben.

bool isPieceMoving;

Dann füge ich die Checks gegen diese Boolesche Flagge an den entsprechenden Stellen hinzu.

Wenn ich später herausfinde, dass ich mehr Spezialfälle benötige - und nur das -, berücksichtige ich etwas Besseres. Es gibt normalerweise 3 Ansätze, die ich verfolgen werde:

  • Refaktorieren Sie alle exklusiven Unterzustände repräsentierenden Flags in Aufzählungen. z.B. enum { PRE_MOVE, MOVE, POST_MOVE }und fügen Sie die Übergänge hinzu, wo immer dies erforderlich ist. Dann kann ich anhand dieser Aufzählung prüfen, wo ich früher anhand der Booleschen Flagge geprüft habe. Dies ist eine einfache Änderung, die jedoch die Anzahl der zu überprüfenden Dinge verringert und es Ihnen ermöglicht, switch-Anweisungen zu verwenden, um das Verhalten effektiv zu verwalten usw.
  • Schalten Sie einzelne Subsysteme nach Bedarf aus. Wenn der einzige Unterschied während der Kampfsequenz darin besteht, dass Sie keine Figuren bewegen können, können Sie pieceSelectionManager->disable()zu Beginn der Sequenz callen oder ähnliches und pieceSelectionManager->enable(). Sie haben im Wesentlichen immer noch Flags, aber jetzt sind sie näher an dem Objekt gespeichert, das sie steuern, und Sie müssen keinen zusätzlichen Status in Ihrem Spielcode beibehalten.
  • Der vorherige Teil impliziert die Existenz eines PieceSelectionManager: Im Allgemeinen können Sie Teile Ihres Spielzustands und -verhaltens in kleinere Objekte zerlegen, die eine Teilmenge des Gesamtzustands auf kohärente Weise verarbeiten. Jedes dieser Objekte hat einen eigenen Status, der sein Verhalten bestimmt. Die Verwaltung ist jedoch einfach, da es von den anderen Objekten isoliert ist. Widerstehen Sie dem Drang, zuzulassen, dass Ihr Gamestate-Objekt oder Ihre Hauptschleife zu einem Abladeplatz für Pseudoglobale wird, und faktorisieren Sie dies!

Im Allgemeinen muss ich in Bezug auf Sonderfälle nie weiter gehen, daher glaube ich nicht, dass das Risiko besteht, dass es schnell außer Kontrolle gerät.

Kylotan
quelle
1
Ja, ich stelle mir vor, es gibt eine Grenze zwischen dem Ausstieg aus Zuständen und der Verwendung von Bool / Enums, wenn dies angebracht ist. Aber wenn ich meine pedantischen Tendenzen kenne, werde ich wahrscheinlich fast jeden Staat zu einer eigenen Klasse machen.
Vargonian
Sie haben den Eindruck, dass eine Klasse korrekter ist als die Alternativen, aber denken Sie daran, dass dies subjektiv ist. Wenn Sie anfangen, zu viele kleine Klassen für Dinge zu erstellen, die leichter durch andere Sprachkonstrukte dargestellt werden können, kann dies die Intention des Codes verschleiern.
Kylotan
1

http://www.ai-junkie.com/architecture/state_driven/tut_state1.html ist ein hübsches Tutorial zur Verwaltung des Spielstatus! Sie können es entweder für Spielelemente oder für ein Menüsystem wie oben verwenden.

Er fängt an, über das State Design Pattern zu unterrichten , setzt dann a um State Machineund erweitert es sukzessive immer weiter. Es ist eine sehr gute Lektüre! Sie erhalten ein solides Verständnis dafür, wie das gesamte Konzept funktioniert und wie es auf neue Arten von Problemen angewendet werden kann.

Zolomon
quelle
1

Ich versuche, keine Zustandsmaschine und keine Booleschen Werte für diesen Zweck zu verwenden, da beide nicht skalierbar sind. Beide verwandeln sich in Chaos, wenn die Anzahl der Staaten wächst.

Normalerweise entwerfe ich das Gameplay als eine Abfolge von Aktionen und Konsequenzen. Jeder Spielstatus ist natürlich, ohne dass er separat definiert werden muss.

Zum Beispiel in Ihrem Fall mit der Deaktivierung der Spielereingabe: Sie haben einige Benutzereingabehandler und einige Ingame-visuelle Anzeigen, dass die Eingabe deaktiviert ist. Sie sollten sie daher zu einem Objekt oder einer Komponente machen. Um die Eingabe zu deaktivieren, müssen Sie nur das gesamte Objekt deaktivieren Synchronisieren Sie sie in einer Zustandsmaschine oder reagieren Sie auf einen Booleschen Indikator.

Filipp Keks
quelle