Stato del gioco "Stack"?


52

Stavo pensando a come implementare gli stati di gioco nel mio gioco. Le cose principali che voglio per questo sono:

  • I migliori stati semi-trasparenti sono in grado di vedere attraverso un menu di pausa il gioco dietro

  • Qualcosa di OO-lo trovo più facile da usare e comprendere la teoria che sta dietro, oltre a mantenere o essere pianificato e aggiungere altro.



Stavo pensando di utilizzare un elenco collegato e considerarlo come uno stack. Ciò significa che potrei accedere allo stato seguente per la semi-trasparenza.
Piano: lo stack di stato deve essere un elenco collegato di puntatori a IGameStates. Lo stato superiore gestisce i propri comandi di aggiornamento e input, quindi ha un membro isTransparent per decidere se disegnare lo stato sottostante.
Quindi potrei fare:

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

Per rappresentare il caricamento del giocatore, quindi vai alle opzioni, quindi al menu principale.
È una buona idea o ...? Dovrei guardare qualcos'altro?

Grazie.


Vuoi vedere MainMenuState dietro OptionsMenuState? O solo la schermata di gioco dietro OptionsMenuState?
Skizz,

Il piano era che gli stati avrebbero avuto un valore / flag di opacità / trasparenza. Vorrei verificare se lo stato superiore avesse questo valore vero e, in tal caso, quale valore avesse. Quindi renderlo con tanta opacità sull'altro stato. In questo caso, no, non lo farei.
The Communist Duck

So che è tardi, ma per i futuri lettori: non usare newnel modo mostrato nel codice di esempio, sta solo chiedendo perdite di memoria o altri errori più gravi.
Pharap,

Risposte:


44

Ho lavorato sullo stesso motore di coderanger. Ho un punto di vista diverso. :)

Innanzitutto, non avevamo uno stack di FSM - avevamo uno stack di stati. Una pila di stati crea un unico FSM. Non so come sarebbe uno stack di FSM. Probabilmente troppo complicato per fare qualcosa di pratico.

Il mio più grande problema con la nostra Global State Machine era che era una pila di stati e non un insieme di stati. Ciò significa, ad esempio, ... / MainMenu / Il caricamento era diverso da ... / Loading / MainMenu, a seconda che il menu principale fosse attivo prima o dopo la schermata di caricamento (il gioco è asincrono e il caricamento è principalmente guidato dal server ).

Come due esempi di cose questo ha reso brutto:

  • Ha portato ad esempio allo stato LoadingGameplay, quindi hai avuto Base / Loading e Base / Gameplay / LoadingGameplay per il caricamento all'interno dello stato Gameplay, che ha dovuto ripetere gran parte del codice nello stato di caricamento normale (ma non tutti, e aggiungerne altri ).
  • Avevamo diverse funzioni come "se nel creatore del personaggio vai al gameplay; se nel gameplay vai alla selezione del personaggio; se nella selezione del personaggio torna indietro al login", perché volevamo mostrare le stesse finestre dell'interfaccia in stati diversi ma fare il Indietro / Avanti i pulsanti funzionano ancora.

Nonostante il nome, non era molto "globale". La maggior parte dei sistemi di gioco interni non lo utilizzavano per tracciare i propri stati interni, perché non volevano che i loro stati si confondessero con altri sistemi. Altri, ad esempio il sistema di interfaccia utente, potrebbero usarlo ma solo per copiare lo stato nei propri sistemi di stato locale. (Vorrei in particolare mettere in guardia contro il sistema per gli stati dell'interfaccia utente. Lo stato dell'interfaccia utente non è uno stack, è in realtà un DAG e cercare di forzare qualsiasi altra struttura su di esso renderà solo le interfacce utente frustranti da usare.)

A cosa serviva era isolare le attività per l'integrazione del codice dai programmatori dell'infrastruttura che non sapevano come fosse effettivamente strutturato il flusso di gioco, quindi si poteva dire al tizio che scriveva il patcher "metti il ​​tuo codice in Client_Patch_Update" e il tizio che scriveva la grafica caricando "inserisci il tuo codice in Client_MapTransfer_OnEnter", e potremmo scambiare determinati flussi logici senza troppi problemi.

In un progetto secondario, ho avuto più fortuna con uno stato impostato anziché uno stack , non avendo paura di creare più macchine per sistemi non correlati e rifiutando di lasciarmi cadere nella trappola di avere uno "stato globale", che è davvero solo un modo complicato per sincronizzare le cose attraverso variabili globali - Certo, finirai per farlo vicino a una scadenza, ma non progettare con quello come obiettivo . Fondamentalmente, lo stato in un gioco non è uno stack e gli stati in un gioco non sono tutti correlati.

Inoltre, come indicano i puntatori a funzione e il comportamento non locale, il GSM ha reso più difficile il debug delle cose, anche se il debug di quel tipo di grandi transizioni di stato non era molto divertente prima di noi. I set di stati invece di stack di stato non aiutano davvero questo, ma dovresti esserne consapevole. Le funzioni virtuali anziché i puntatori di funzione possono alleviarlo in qualche modo.


Ottima risposta, grazie! Penso di poter prendere molto dal tuo post e dalle tue esperienze passate. : D + 1 / Tick.
Il comunista Duck il

La cosa bella di una gerarchia è che puoi creare stati di utilità che vengono semplicemente spinti in alto e non devi preoccuparti di cos'altro è in esecuzione.
coderanger,

Non vedo come sia un argomento per una gerarchia piuttosto che per gli insiemi. Piuttosto, una gerarchia rende tutte le comunicazioni tra stati più complicate, perché non hai idea di dove siano state spinte.

Il punto che le UI sono in realtà DAG è ben preso, ma non sono d'accordo sul fatto che certamente può essere rappresentato in uno stack. Qualsiasi grafico aciclico diretto collegato (e non riesco a pensare a un caso in cui non sarebbe un DAG collegato) può essere visualizzato come un albero e uno stack è essenzialmente un albero.
Ed Ropple,

2
Le pile sono un sottoinsieme di alberi, che sono un sottoinsieme di DAG, che sono un sottoinsieme di tutti i grafici. Tutte le pile sono alberi, tutti gli alberi sono DAG, ma la maggior parte dei DAG non sono alberi e la maggior parte degli alberi non sono pile. I DAG dispongono di un ordinamento topologico che consente di archiviarli in uno stack (per attraversare, ad esempio, la risoluzione delle dipendenze), ma una volta stipati nello stack hai perso informazioni preziose. In questo caso, la possibilità di navigare tra uno schermo e il suo genitore se ha un fratello precedente.

11

Ecco un esempio di implementazione di uno stack di gamestate che ho trovato molto utile: http://creators.xna.com/en-US/samples/gamestatemanagement

È scritto in C # e per compilarlo è necessario il framework XNA, tuttavia è possibile semplicemente controllare il codice, la documentazione e il video per avere l'idea.

Può supportare transizioni di stato, stati trasparenti (come finestre di messaggio modali) e stati di caricamento (che gestiscono lo scarico degli stati esistenti e il caricamento dello stato successivo).

Ora uso gli stessi concetti nei miei progetti di hobby (non C #) (garantito, potrebbe non essere adatto a progetti più grandi) e per progetti di piccole dimensioni / hobby posso sicuramente consigliare l'approccio.


5

Questo è simile a quello che usiamo, uno stack di FSM. Fondamentalmente basta dare a ogni stato una funzione di invio, uscita e tick e chiamarli in ordine. Funziona molto bene anche per gestire cose come il caricamento.


3

Uno dei volumi "Gemme di programmazione del gioco" aveva un'implementazione della macchina a stati destinata agli stati di gioco; http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdf ha un esempio di come usarlo per un piccolo gioco e non dovrebbe essere troppo specifico per Gamebryo per essere leggibile.


La prima sezione di "Programmazione di giochi di ruolo con DirectX" implementa anche un sistema statale (e un sistema di processo - distinzione molto interessante).
Ricket,

È un ottimo documento e lo spiega quasi esattamente come l'ho implementato in passato, a meno della gerarchia di oggetti inutili che usano negli esempi.
dash-tom-bang,

3

Solo per aggiungere un po 'di standardizzazione alla discussione, il classico termine CS per questo tipo di strutture dati è un automa pushdown .


Non sono sicuro che qualsiasi implementazione nel mondo reale di stack di stato sia quasi equivalente a un automa pushdown. Come menzionato in altre risposte, le implementazioni pratiche finiscono inevitabilmente con comandi come "pop due stati", "scambia questi stati" o "passa questi dati allo stato successivo all'esterno dello stack". E un automa è un automa - un computer - non una struttura di dati. Sia gli stack di stato che gli automi pushdown utilizzano uno stack come struttura di dati.

1
"Non sono sicuro che qualsiasi implementazione nel mondo reale di stack statali sia quasi equivalente a un automa pushdown". Qual è la differenza? Entrambi hanno un insieme finito di stati, una storia di stati e operazioni primitive per spingere e pop stati. Nessuna delle altre operazioni menzionate è sostanzialmente diversa da quella. "Pop two States" sta saltando fuori due volte. "swap" è un pop e una spinta. Passare i dati è al di fuori dell'idea di base, ma ogni gioco che utilizza un "FSM" punta anche su dati aggiuntivi senza sentirsi come se il nome non fosse più applicabile.
munificente

In un automa pushdown, l'unico stato che può influenzare la transizione è lo stato in alto. Non è consentito scambiare due stati nel mezzo; nemmeno guardando gli stati nel mezzo non è permesso. Sento che l'espansione semantica del termine "FSM" è ragionevole e ha vantaggi (e abbiamo ancora i termini "DFA" e "NFA" per il significato più limitato), ma "automa pushdown" è strettamente un termine di informatica e c'è solo confusione in attesa se la applichiamo a ogni singolo sistema basato su stack disponibile.

Preferisco quelle implementazioni in cui l'unico stato che può influire su qualsiasi cosa è lo stato in cima, anche se in alcuni casi è utile poter filtrare l'input di stato e passare l'elaborazione a uno stato "inferiore". (Ad esempio, l'elaborazione dell'input del controller esegue il mapping a questo metodo, lo stato superiore prende i bit a cui tiene e probabilmente li cancella, quindi passa il controllo allo stato successivo nello stack.)
dash-tom-bang,

1
Buon punto, risolto!
munifico

1

Non sono sicuro che uno stack sia del tutto necessario oltre a limitare la funzionalità del sistema statale. Utilizzando uno stack, non è possibile "uscire" da uno stato per una delle diverse possibilità. Supponi di iniziare nel "Menu principale", quindi vai a "Carica partita", potresti voler passare allo stato "Pausa" dopo aver caricato correttamente la partita salvata e tornare al "Menu principale" se l'utente annulla il caricamento.

Vorrei solo che lo stato specificasse lo stato da seguire quando esce.

Per quei casi in cui si desidera tornare allo stato precedente lo stato corrente, ad esempio "Menu principale-> Opzioni-> Menu principale" e "Pausa-> Opzioni-> Pausa", passare come parametro di avvio allo stato stato a cui tornare.


Forse ho frainteso la domanda?
Skizz,

No, tu non l'hai fatto. Penso che lo abbia fatto il voto negativo.
Il comunista Duck il

L'uso di uno stack non preclude l'uso di transizioni di stato esplicite.
dash-tom-bang,

1

Un'altra soluzione alle transizioni e ad altre cose simili è quella di fornire lo stato di destinazione e di origine, insieme alla macchina a stati, che potrebbe essere collegata al "motore", qualunque esso sia. La verità è che probabilmente la maggior parte delle macchine statali dovrà essere adattata al progetto in questione. Una soluzione potrebbe avvantaggiare questo o quel gioco, altre soluzioni potrebbero ostacolarlo.

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

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

Gli stati vengono spinti con lo stato corrente e la macchina come parametri.

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

Gli stati sono spuntati allo stesso modo. Se si chiama Enter()in basso Stateè una domanda di implementazione.

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;
}

Quando si entra, si aggiorna o si esce, si Stateottengono tutte le informazioni necessarie.

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);
}

0

Ho usato un sistema molto simile in diversi giochi e ho scoperto che, con un paio di eccezioni, funziona come un eccellente modello di interfaccia utente.

Gli unici problemi che abbiamo riscontrato sono stati casi in cui in alcuni casi si desidera ripristinare più stati prima di inviare un nuovo stato (abbiamo reindirizzato l'interfaccia utente per rimuovere il requisito, poiché di solito era un segno di interfaccia utente errata) e creare uno stile di procedura guidata flussi lineari (risolti facilmente passando i dati allo stato successivo).

L'implementazione che abbiamo effettivamente utilizzato ha impacchettato lo stack e gestito la logica per l'aggiornamento e il rendering, nonché le operazioni sullo stack. Ogni operazione nello stack ha innescato eventi sugli stati per notificare loro l'operazione in corso.

Sono state aggiunte anche alcune funzioni di supporto per semplificare le attività comuni, come Scambia (Pop & Push, per flussi lineari) e Ripristina (per tornare al menu principale o terminare un flusso).


Come modello di interfaccia utente questo ha un senso. Esiterei a chiamarli stati, poiché nella mia testa lo assocerei agli interni del motore di gioco principale, mentre "Menu principale", "Menu Opzioni", "Schermata di gioco" e "Schermata di pausa" sono di livello superiore, e spesso non hanno alcuna interazione con lo stato interno del gioco principale e semplicemente inviano comandi al motore principale del modulo "Pausa", "Ripresa", "Livello di carico 1", "Livello di avvio", "Livello di riavvio", "Salva" e "Ripristina", "imposta il livello del volume 57", ecc. Ovviamente, ciò può variare in modo significativo a seconda del gioco.
Kevin Cathcart,

0

Questo è l'approccio che prendo per quasi tutti i miei progetti, perché funziona incredibilmente bene ed è estremamente semplice.

Il mio progetto più recente, Sharplike , gestisce il flusso di controllo in questo modo esatto. I nostri stati sono tutti cablati con una serie di funzioni di evento che vengono chiamate quando gli stati cambiano e presenta un concetto di "stack denominato" in cui è possibile avere più pile di stati all'interno della stessa macchina a stati e ramo tra loro - un concetto strumento, e non necessario, ma utile da avere.

Vorrei mettere in guardia contro il paradigma "dire al controllore quale stato dovrebbe seguire questo quando finisce" suggerito da Skizz: non è strutturalmente solido, e crea cose come finestre di dialogo (che nel paradigma standard dello stato dello stack implica solo la creazione di un nuovo sottoclasse di stato con nuovi membri, quindi leggendola quando torni allo stato invocante) molto più difficile di quanto debba essere.


0

Ho usato fondamentalmente questo esatto sistema in diversi sistemi ortogonalmente; gli stati del menu front-end e in-game (aka "pause"), per esempio, avevano le loro pile di stati. Anche l'interfaccia utente del gioco usava qualcosa di simile, sebbene avesse aspetti "globali" (come la barra della salute e la mappa / radar) che il cambio di stato avrebbe potuto tingere ma che si aggiornavano in modo comune tra gli stati.

Il menu in-game può essere "meglio" rappresentato da un DAG, ma con una macchina a stati impliciti (ogni opzione di menu che passa a un'altra schermata sa come andarci, e premendo il pulsante indietro fa sempre scattare lo stato superiore) l'effetto è stato esattamente la stessa.

Alcuni di questi altri sistemi avevano anche la funzionalità di "sostituzione dello stato superiore", ma quella era tipicamente implementata come StatePop()seguita da StatePush(x);.

La gestione della scheda di memoria era simile dal momento che in realtà avevo inserito un sacco di "operazioni" nella coda delle operazioni (che funzionalmente faceva la stessa cosa dello stack, proprio come FIFO piuttosto che LIFO); una volta che inizi a utilizzare questo tipo di struttura ("sta succedendo una cosa adesso, e quando è fatto si apre da sola") inizia a infettare ogni area del codice. Anche l'IA ha iniziato a usare qualcosa del genere; l'intelligenza artificiale era "all'oscuro", quindi è diventata "diffidente" quando il giocatore ha fatto rumore ma non è stato visto, e infine è stata elevata a "attiva" quando ha visto il giocatore (e, a differenza dei giochi minori dell'epoca, non si poteva nascondere in una scatola di cartone e fai dimenticare al nemico di te! Non che io sia amaro ...).

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]();
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.