Spielstatus 'Stack'?

52

Ich habe darüber nachgedacht, wie ich Spielzustände in mein Spiel implementieren kann. Die wichtigsten Dinge, die ich dafür möchte, sind:

  • Halbtransparente Top-Zustände, die durch ein Pausenmenü auf das dahinterliegende Spiel blicken können

  • Etwas OO - ich finde es einfacher, die dahinter stehende Theorie zu verwenden und zu verstehen sowie die Organisation aufrechtzuerhalten und mehr hinzuzufügen.



Ich hatte vor, eine verknüpfte Liste zu verwenden und sie als Stapel zu behandeln. Dies bedeutet, dass ich für die Halbtransparenz auf den unten stehenden Status zugreifen konnte.
Plan: Der Statusstapel soll eine verknüpfte Liste von Zeigern auf IGameStates sein. Der oberste Status verarbeitet seine eigenen Aktualisierungs- und Eingabebefehle und hat dann den Member isTransparent, um zu entscheiden, ob der darunter liegende Status gezeichnet werden soll.
Dann könnte ich tun:

states.push_back(new MainMenuState());
states.push_back(new OptionsMenuState());
states.pop_front();

Um das Laden des Players darzustellen, gehen Sie zu Optionen und dann zum Hauptmenü.
Ist das eine gute Idee oder ...? Soll ich mir etwas anderes ansehen?

Vielen Dank.

Die kommunistische Ente
quelle
Möchten Sie den MainMenuState hinter dem OptionsMenuState sehen? Oder nur der Spielbildschirm hinter dem OptionsMenuState?
Skizz
Der Plan war, dass die Staaten eine Opazität / einen transparenten Wert / eine Flagge haben würden. Ich würde prüfen, ob der Top-Status dies zutreffend ist und wenn ja, welchen Wert er hat. Dann machen Sie es mit so viel Deckkraft über dem anderen Zustand. In diesem Fall würde ich nein nicht.
Die kommunistische Ente
Ich weiß, dass es spät am Tag ist, aber für zukünftige Leser: Verwenden Sie es nicht newwie im Beispielcode gezeigt, es fragt nur nach Speicherlecks oder anderen, schwerwiegenderen Fehlern.
Pharap

Antworten:

44

Ich habe am selben Motor wie Coderanger gearbeitet. Ich habe einen anderen Standpunkt. :)

Erstens hatten wir keinen Stapel FSMs - wir hatten einen Stapel Staaten. Ein Stapel von Zuständen bildet eine einzelne FSM. Ich weiß nicht, wie ein Stapel FSMs aussehen würde. Wahrscheinlich zu kompliziert, um irgendetwas Praktisches damit anzufangen.

Mein größtes Problem mit unserer globalen Zustandsmaschine war, dass es sich um einen Stapel von Zuständen und nicht um eine Reihe von Zuständen handelte. Dies bedeutet, dass z. B. ... / MainMenu / Loading anders war als ... / Loading / MainMenu, je nachdem, ob Sie das Hauptmenü vor oder nach dem Ladebildschirm geöffnet haben (das Spiel ist asynchron und das Laden erfolgt hauptsächlich auf Servern) ).

Als zwei Beispiele von Dingen, die dies hässlich machten:

  • Es führte zB zum LoadingGameplay-Status, so dass Sie Base / Loading und Base / Gameplay / LoadingGameplay zum Laden innerhalb des Gameplay-Status hatten, die einen Großteil des Codes im normalen Ladezustand (aber nicht alle) wiederholen mussten und etwas mehr hinzufügten ).
  • Wir hatten verschiedene Funktionen wie "Wenn im Charakterersteller zum Gameplay wechseln, wenn im Gameplay zur Charakterauswahl wechseln, wenn im Charakterauswahl zum Login zurückkehren", weil wir das gleiche Interface-Fenster in verschiedenen Zuständen anzeigen wollten, aber das Zurück / Vorwärts machen wollten Tasten funktionieren immer noch.

Trotz des Namens war es nicht sehr "global". Die meisten internen Spielesysteme verwendeten es nicht, um ihre internen Zustände zu verfolgen, weil sie nicht wollten, dass sich ihre Zustände mit anderen Systemen herumschlagen. Andere, z. B. das UI-System, könnten es nur verwenden, um den Status in ihre eigenen lokalen Statussysteme zu kopieren. (Ich warne besonders vor dem System für UI-Status. Der UI-Status ist kein Stack, sondern eine echte DAG. Wenn Sie versuchen, eine andere Struktur zu erzwingen, ist die Verwendung von UIs nur frustrierend.)

Wofür war es gut, Aufgaben für die Integration von Code von Infrastruktur-Programmierern zu isolieren, die nicht wussten, wie der Spielfluss tatsächlich aufgebaut ist, sodass Sie dem Typ, der den Patcher schreibt, mitteilen konnten, dass er den Code in Client_Patch_Update einfügt, und dem Typ, der die Grafiken schreibt Wenn Sie "Setzen Sie Ihren Code in Client_MapTransfer_OnEnter" laden, können Sie bestimmte logische Abläufe problemlos austauschen.

Auf einem Nebenprojekt habe ich mehr Glück mit einem Zustand hatte gesetzt anstatt einen Stapel , nicht Angst zu haben mehrere Maschinen für unabhängige Systeme zu machen, und sich weigern , mich in die Falle fallen lassen einen „globalen Zustand“ zu haben, was wirklich ist Nur ein komplizierter Weg, um Dinge über globale Variablen zu synchronisieren - Sicher, Sie werden es in der Nähe einer bestimmten Frist tun, aber entwerfen Sie das nicht als Ihr Ziel . Grundsätzlich ist ein Zustand in einem Spiel kein Stapel, und Zustände in einem Spiel sind nicht alle miteinander verbunden.

Da Funktionszeiger und nicht lokales Verhalten dazu neigen, erschwerte das GSM das Debuggen von Dingen, obwohl das Debuggen derartiger großer Zustandsübergänge nicht sehr viel Spaß machte, bevor wir es hatten. Zustandsmengen anstelle von Zustandsstapeln helfen dies nicht wirklich, aber Sie sollten sich dessen bewusst sein. Virtuelle Funktionen anstelle von Funktionszeigern können dies etwas abmildern.


quelle
Tolle Antwort, danke! Ich denke, ich kann viel von Ihrem Beitrag und Ihren bisherigen Erfahrungen nehmen. : D + 1 / Tick.
Die kommunistische Ente
Das Schöne an einer Hierarchie ist, dass Sie Utility-Zustände erstellen können, die einfach nach oben verschoben werden und sich keine Gedanken darüber machen müssen, was sonst noch läuft.
Coderanger
Ich verstehe nicht, wie das ein Argument für eine Hierarchie ist und nicht für Mengen. Vielmehr macht eine Hierarchie die gesamte Kommunikation zwischen Staaten komplizierter, da Sie keine Ahnung haben, wohin sie verschoben wurden.
Der Punkt, dass Benutzeroberflächen tatsächlich DAGs sind, ist gut angenommen, aber ich bin nicht einverstanden, dass es sicherlich in einem Stapel dargestellt werden kann. Jeder verbundene gerichtete azyklische Graph (und ich kann mir keinen Fall vorstellen, in dem es sich nicht um eine verbundene DAG handeln würde) kann als Baum angezeigt werden, und ein Stapel ist im Wesentlichen ein Baum.
Ed Ropple
2
Stapel sind eine Untermenge von Bäumen, die eine Untermenge von DAGs sind, die eine Untermenge aller Graphen sind. Alle Stapel sind Bäume, alle Bäume sind DAGs, aber die meisten DAGs sind keine Bäume, und die meisten Bäume sind keine Stapel. DAGs haben eine topologische Anordnung, die es Ihnen ermöglicht, sie in einem Stapel zu speichern (zum Durchlaufen, z. B. für die Auflösung von Abhängigkeiten), aber sobald Sie sie in den Stapel stopfen, haben Sie wertvolle Informationen verloren. In diesem Fall die Möglichkeit, zwischen einem Bildschirm und seinem übergeordneten Bildschirm zu navigieren, wenn dieser einen früheren Bruder hat.
11

Hier ist eine Beispielimplementierung eines Gamestate-Stacks, die ich als sehr nützlich empfand: http://creators.xna.com/en-US/samples/gamestatemanagement

Es ist in C # geschrieben und zum Kompilieren benötigen Sie das XNA-Framework. Sie können sich jedoch den Code, die Dokumentation und das Video ansehen, um die Idee zu erhalten.

Es kann Zustandsübergänge, transparente Zustände (wie modale Meldungsfelder) und Ladezustände (die das Entladen vorhandener Zustände und das Laden des nächsten Zustands verwalten) unterstützen.

Ich verwende die gleichen Konzepte jetzt in meinen (Nicht-C #) Hobbyprojekten (unter Umständen nicht für größere Projekte geeignet) und für kleine / Hobbyprojekte kann ich den Ansatz definitiv empfehlen.

Janis Kirsteins
quelle
5

Dies ähnelt dem, was wir verwenden, einem Stapel von FSMs. Geben Sie einfach jedem Zustand eine Eingabe-, Ausgabe- und Ankreuzfunktion und rufen Sie sie der Reihe nach auf. Funktioniert sehr gut, um Dinge wie das Laden zu handhaben.

coderanger
quelle
3

In einem der "Game Programming Gems" -Volumes war eine Zustandsmaschinenimplementierung für Spielzustände vorgesehen. http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdf enthält ein Beispiel für die Verwendung in einem kleinen Spiel. Es sollte nicht zu gamebryospezifisch sein, um lesbar zu sein.

Tom Hudson
quelle
Der erste Abschnitt von "Programmieren von Rollenspielen mit DirectX" implementiert auch ein Statussystem (und ein Prozesssystem - eine sehr interessante Unterscheidung).
Ricket
Das ist ein großartiges Dokument und erklärt es fast genau, wie ich es in der Vergangenheit implementiert habe, abgesehen von der unnötigen Objekthierarchie, die sie in den Beispielen verwenden.
Dash-Tom-Bang
3

Um die Diskussion ein wenig zu standardisieren, ist der klassische CS-Begriff für diese Art von Datenstrukturen ein Pushdown-Automat .

herrlich
quelle
Ich bin mir nicht sicher, ob eine reale Implementierung von State Stacks einem Pushdown-Automaten entspricht. Wie in anderen Antworten erwähnt, führen praktische Implementierungen ausnahmslos zu Befehlen wie "Zwei Status einfügen", "Diese Status austauschen" oder "Diese Daten an den nächsten Status außerhalb des Stapels übergeben". Und ein Automat ist ein Automat - ein Computer - keine Datenstruktur. Sowohl Statusstapel als auch Pushdown-Automaten verwenden einen Stapel als Datenstruktur.
1
"Ich bin mir nicht sicher, ob eine reale Implementierung von State Stacks einem Pushdown-Automaten entspricht." Was ist der Unterschied? Beide haben eine endliche Menge von Zuständen, eine Geschichte von Zuständen und primitive Operationen, um Zustände zu pushen und zu popen. Keine der anderen Operationen, die Sie erwähnen, unterscheidet sich grundlegend davon. "Pop two states" taucht nur zweimal auf. "Swap" ist ein Pop und ein Push. Das Weitergeben von Daten liegt außerhalb der Kernidee, aber jedes Spiel, das eine "FSM" verwendet, greift auch auf zusätzliche Daten zu, ohne das Gefühl zu haben, dass der Name nicht mehr gültig ist.
Munificent
In einem Pushdown-Automaten ist der oberste Status der einzige Status, der sich auf Ihren Übergang auswirken kann. Das Vertauschen von zwei Zuständen in der Mitte ist nicht zulässig. selbst ein blick auf die zustände in der mitte ist nicht erlaubt. Ich halte die semantische Erweiterung des Begriffs "FSM" für sinnvoll und hat Vorteile (und wir haben immer noch die Begriffe "DFA" und "NFA" für die engste Bedeutung), aber "Pushdown-Automat" ist streng genommen ein Informatikbegriff und Es gibt nur Verwirrung, wenn wir es auf jedes einzelne Stack-basierte System anwenden.
Ich bevorzuge solche Implementierungen, bei denen der einzige Zustand, der irgendetwas beeinflussen kann, der Zustand ist, der oben ist, obwohl es in einigen Fällen praktisch ist, die Zustandseingabe zu filtern und die Verarbeitung in einen "niedrigeren" Zustand zu überführen. (ZB ist die Verarbeitung von Controllereingaben dieser Methode zugeordnet. Der oberste Status nimmt die betroffenen Bits und löscht sie möglicherweise. Anschließend geht die Steuerung an den nächsten Status auf dem Stapel über.)
dash-tom-bang
1
Guter Punkt, behoben!
Munificent
1

Ich bin mir nicht sicher, ob ein Stack unbedingt erforderlich ist und die Funktionalität des Staatssystems einschränkt. Wenn Sie einen Stapel verwenden, können Sie einen Zustand nicht auf eine von mehreren Möglichkeiten verlassen. Angenommen, Sie starten im "Hauptmenü" und gehen dann zu "Spiel laden". Möglicherweise möchten Sie nach dem erfolgreichen Laden des gespeicherten Spiels in den Status "Pause" wechseln und zum "Hauptmenü" zurückkehren, wenn der Benutzer das Laden abbricht.

Ich würde nur den Zustand angeben lassen, dem zu folgen ist, wenn er beendet wird.

In Fällen, in denen Sie in den Status vor dem aktuellen Status zurückkehren möchten, z. B. "Hauptmenü-> Optionen-> Hauptmenü" und "Pause-> Optionen-> Pause", übergeben Sie einfach als Startparameter den Status Zustand zurück zu gehen.

Skizz
quelle
Vielleicht habe ich die Frage falsch verstanden?
Skizz 30.07.10
Nein hast du nicht. Ich denke, der Abwähler hat es getan.
Die kommunistische Ente
Die Verwendung eines Stacks schließt die Verwendung expliziter Statusübergänge nicht aus.
Dash-Tom-Bang
1

Eine andere Lösung für Übergänge und andere derartige Dinge besteht darin, den Ziel- und Quellzustand zusammen mit der Zustandsmaschine bereitzustellen, die mit der "Maschine" verbunden sein könnte, wie auch immer dies sein mag. Die Wahrheit ist, dass die meisten Zustandsautomaten wahrscheinlich auf das jeweilige Projekt zugeschnitten werden müssen. Eine Lösung könnte diesem oder jenem Spiel zugute kommen, andere Lösungen könnten es behindern.

class StateMachine
{
public:
    StateMachine(Engine *);
    void Push(State *);
    State *Pop();
    void Update();
    Engine *GetEngine();

private:
    std::stack<State *> _states;
    Engine *_engine;
};

Zustände werden mit dem aktuellen Zustand und der Maschine als Parameter gepusht.

void StateMachine::Push(State *state)
{
    State *from = 0;
    if (!_states.empty()) from = _states.top();
    _states.push(state);
    state->Enter(this, from);
}

Staaten werden auf die gleiche Weise geknallt. Ob Sie Enter()den unteren anrufen, Stateist eine Umsetzungsfrage.

State *StateMachine::Pop()
{
    _ASSERT(!_states.empty());
    State *state = _states.top();
    State *to = 0;
    _states.pop();
    if (!_states.empty()) to = _states.top();
    state->Exit(this, to);
    return state;
}

Beim Betreten, Aktualisieren oder Verlassen Stateerhält der alle Informationen, die er benötigt.

void SomeGameState::Enter(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.Bind(this, &SomeGameState::KeyDown);
    LoadLevelState *state = new LoadLevelState();
    state->SetLevel(eng->GetSaveGame()->GetLevelName());
    state->Load.Bind(this, &SomeGameState::OnLevelLoaded);
    sm->Push(state);
}

void SomeGameState::Update(StateMachine *sm)
{
    Engine *eng = sm->GetEngine();
    float time = eng->GetFrameTime();
    if (shouldExit)
        sm->Pop();
}

void SomeGameState::Exit(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.UnsubscribeAll(this);
}
Nick Bedford
quelle
0

Ich habe ein sehr ähnliches System für mehrere Spiele verwendet und festgestellt, dass es mit ein paar Ausnahmen als hervorragendes UI-Modell dient.

Die einzigen Probleme, auf die wir gestoßen sind, waren Fälle, in denen es in bestimmten Fällen erwünscht ist, mehrere Status zurückzusetzen, bevor ein neuer Status gesendet wird (wir haben die Benutzeroberfläche erneut geändert, um die Anforderung zu entfernen, da dies normalerweise ein Zeichen für eine fehlerhafte Benutzeroberfläche war) und einen Assistentenstil zu erstellen lineare Flüsse (leicht gelöst durch Weitergabe der Daten an den nächsten Zustand).

Die von uns verwendete Implementierung hat den Stack tatsächlich verpackt und die Logik für das Aktualisieren und Rendern sowie die Operationen auf dem Stack verwaltet. Jede Operation auf dem Stapel löste Ereignisse in den Zuständen aus, um sie über das Auftreten der Operation zu benachrichtigen.

Es wurden auch einige Hilfsfunktionen hinzugefügt, um allgemeine Aufgaben zu vereinfachen, wie z. B. Tauschen (Pop & Push, für lineare Flüsse) und Zurücksetzen (um zum Hauptmenü zurückzukehren oder einen Fluss zu beenden).

Jason Kozak
quelle
Als UI-Modell ist dies sinnvoll. Ich würde zögern, sie Zustände zu nennen, da ich das in meinem Kopf mit den Interna der Hauptspielmaschine in Verbindung bringen würde, während "Hauptmenü", "Optionsmenü", "Spielebildschirm" und "Pausenbildschirm" eine höhere Ebene sind. und haben oft nur keine Interaktion mit dem internen Zustand des Kernspiels, und senden Sie einfach Befehle an die Core-Engine der Form "Pause", "Pause", "Lade Level 1", "Start Level", "Neustart Level", "Speichern" und "Wiederherstellen", "Lautstärkestufe 57 einstellen" usw. Dies kann natürlich von Spiel zu Spiel erheblich variieren.
Kevin Cathcart
0

Dies ist der Ansatz, den ich für fast alle meine Projekte verwende, weil er unglaublich gut funktioniert und extrem einfach ist.

Mein letztes Projekt, Sharplike , handhabt den Kontrollfluss genau so. Unsere Zustände sind alle mit einer Reihe von Ereignisfunktionen verkabelt, die aufgerufen werden, wenn sich Zustände ändern, und es gibt ein "benanntes Stapel" -Konzept, bei dem Sie mehrere Stapel von Zuständen innerhalb derselben Zustandsmaschine und Verzweigung haben können - ein Konzept Werkzeug und nicht notwendig, aber handlich zu haben.

Ich würde davor warnen, das von Skizz vorgeschlagene Paradigma "Dem Controller zu sagen, welcher Status nach dem Beenden folgen soll" zu verwenden: Es ist nicht strukturell einwandfrei und es erstellt Dinge wie Dialogfelder (was im Standard-Stack-State-Paradigma nur das Erstellen eines neuen umfasst) State-Unterklasse mit neuen Mitgliedern, und lesen Sie es dann ab, wenn Sie in den aufrufenden Zustand zurückkehren.

Ed Ropple
quelle
0

Ich habe im Grunde genommen genau dieses System in mehreren Systemen orthogonal verwendet. So hatten beispielsweise das Frontend und das In-Game-Menü (auch "Pause" genannt) ihre eigenen Status-Stacks. Die spielinterne Benutzeroberfläche verwendete auch so etwas, obwohl sie "globale" Aspekte (wie die Gesundheitsleiste und die Karte / das Radar) aufwies, die möglicherweise durch das Umschalten des Status hervorgerufen wurden, die jedoch in allen Staaten auf eine gemeinsame Weise aktualisiert wurden.

Das Menü im Spiel kann durch eine DAG "besser" dargestellt werden, aber mit einer impliziten Zustandsmaschine (jede Menüoption, die zu einem anderen Bildschirm wechselt, weiß, wie man dorthin geht, und das Drücken der Zurück-Taste hat immer den obersten Zustand erreicht) war der Effekt genauso.

Einige dieser anderen Systeme verfügten ebenfalls über die Funktion "Ersetzen des Top-Status", diese wurde jedoch in der Regel wie StatePop()folgt implementiert StatePush(x);.

Die Handhabung von Speicherkarten war ähnlich, da ich tatsächlich eine Tonne "Operationen" in die Operationswarteschlange geschoben habe (die funktionell dasselbe tat wie der Stapel, nur als FIFO und nicht als LIFO); Sobald Sie anfangen, diese Art von Struktur zu verwenden ("es passiert gerade etwas und wenn es fertig ist, erscheint es von selbst"), infiziert es jeden Bereich des Codes. Sogar die KI begann so etwas zu benutzen. Die KI war "ahnungslos" und wurde auf "vorsichtig" umgeschaltet, wenn der Spieler Geräusche machte, aber nicht gesehen wurde, und schließlich auf "aktiv" angehoben, wenn er den Spieler sah (und im Gegensatz zu kleineren Spielen der Zeit konnte man sich nicht verstecken in einer Pappschachtel und lass den Feind dich vergessen! Nicht, dass ich bitter bin ...).

GameState.h:

enum GameState
{
   k_frontend,
   k_gameplay,
   k_inGameMenu,
   k_moviePlayback,
   k_numStates
};

void GameStatePush(GameState);
void GameStatePop();
void GameStateUpdate();

GameState.cpp:

// k_maxNumStates could be bigger, but we don't need more than
// one of each state on the stack.
static const int k_maxNumStates = k_numStates;
static GameState s_states[k_maxNumStates] = { k_frontEnd };
static int s_numStates = 1;

static void (*s_startupFunctions)()[] =
   { FrontEndStart, GameplayStart, InGameMenuStart, MovieStart };
static void (*s_shutdownFunctions)()[] =
   { FrontEndStop, GameplayStop, InGameMenuStop, MovieStop };
static void (*s_updateFunctions)()[] =
   { FrontEndUpdate, GameplayUpdate, InGameMenuUpdate, MovieUpdate };

static void GameStateStart(GameState);
static void GameStateStop(GameState);

void GameStatePush(GameState gs)
{
   Assert(s_numStates < k_maxNumStates);
   GameStateStop(s_states[s_numStates - 1])
   s_states[s_numStates] = gs;
   s_numStates++;
   GameStateStart(gs);
}

void GameStatePop()
{
   Assert(s_numStates > 1);  // can't pop last state
   s_numStates--;
   GameStateStop(s_states[s_numStates]);
   GameStateStart(s_states[s_numStates - 1]);
}

void GameStateUpdate()
{
   GameState current = s_states[s_numStates - 1];
   s_updateFunctions[current]();
}

void GameStateStart(GameState gs)
{
   s_startupFunctions[gs]();
}

void GameStateStop(GameState gs)
{
   s_shutdownFunctions[gs]();
}
Dash-Tom-Bang
quelle