Tecniche di gestione dello stato di gioco?


24

Prima di tutto, non mi riferisco alla gestione delle scene; Sto definendo lo stato del gioco liberamente come qualsiasi tipo di stato in un gioco che abbia implicazioni sull'opportunità di abilitare l'input dell'utente o se determinati attori dovrebbero essere temporaneamente disabilitati, ecc.

Ad esempio concreto, diciamo che è un gioco del classico Battlechess. Dopo che ho fatto una mossa per prendere il pezzo di un altro giocatore, viene riprodotta una breve sequenza di battaglie. Durante questa sequenza, al giocatore non dovrebbe essere permesso di spostare pezzi. Quindi, come seguiresti questo tipo di transizione di stato? Una macchina a stati finiti? Un semplice controllo booleano? Sembra che quest'ultimo funzionerebbe bene solo per un gioco con pochissimi cambiamenti di stato di questo tipo.

Posso pensare a molti modi semplici di gestirlo usando macchine a stati finiti, ma posso anche vederli sfuggire rapidamente di mano. Sono solo curioso di sapere se esiste un modo più elegante per tenere traccia degli stati / delle transizioni del gioco.


Hai controllato gamedev.stackexchange.com/questions/1783/game-state-stack e gamedev.stackexchange.com/questions/2423/… ? È un po 'come saltare tutti intorno allo stesso concetto, ma non riesco a pensare a qualcosa di meglio di una macchina statale per lo stato del gioco.
michael.bartnett,

Risposte:


18

Una volta mi sono imbattuto in un articolo che risolve il tuo problema in modo abbastanza elegante. È un'implementazione di base di FSM, che viene chiamata nel tuo ciclo principale. Ho delineato il riassunto di base dell'articolo nel resto di questa risposta.

Il tuo stato di gioco di base è simile al seguente:

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

Ogni stato del gioco è rappresentato da un'implementazione di questa interfaccia. Per il tuo esempio Battlechess, ciò potrebbe significare questi stati:

  • animazione introduttiva
  • menu principale
  • animazione di configurazione della scacchiera
  • input di mossa del giocatore
  • animazione di mossa del giocatore
  • animazione mossa dell'avversario
  • menu di pausa
  • schermata di fine gioco

Gli stati sono gestiti nel tuo motore statale:

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

        // ...
};

Si noti che ogni stato ha bisogno di un puntatore a CGameEngine ad un certo punto, quindi lo stato stesso può decidere se inserire un nuovo stato. L'articolo suggerisce di passare a CGameEngine come parametro per HandleEvents, Update e Draw.

Alla fine, il tuo ciclo principale si occupa solo del motore statale:

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

17
C per classe? Ew. Tuttavia, questo è un buon articolo - +1.
Il comunista Duck il

Da quello che posso raccogliere, questo è il genere di cose che la domanda sta esplicitamente -non-chiedendo. Questo non vuol dire che non puoi gestirlo in questo modo, come certamente puoi, ma se tutto ciò che volevi fare era disabilitare temporaneamente l'input, penso che sia eccessivo e negativo per la manutenzione derivare una nuova sottoclasse di CGameState che sta per essere identico al 99% a un'altra sottoclasse.
Kylotan,

Penso che questo dipenda in larga misura da come il codice si è accoppiato insieme. Posso immaginare una netta separazione tra la selezione di un pezzo e una destinazione (principalmente indicatori dell'interfaccia utente e gestione dell'input) e un'animazione del pezzo degli scacchi verso quella destinazione (un'animazione di un'intera scheda in cui altri pezzi si muovono fuori strada, interagiscono con lo spostamento pezzo ecc.), rendendo gli stati tutt'altro che identici. Ciò separa la responsabilità, consentendo una facile manutenzione e persino riutilizzabilità (introduzione demo, modalità di riproduzione). Penso che questo risponda anche alla domanda dimostrando che l'uso di un FSM non deve essere una seccatura.
fantasma

Questo è davvero fantastico, grazie. Un punto chiave che hai fatto è stato nel tuo ultimo commento: "l'uso di un FSM non deve essere una seccatura". Avevo erroneamente immaginato che l'uso di un FSM avrebbe comportato l'uso di istruzioni switch, il che non è necessariamente vero. Un'altra conferma chiave è che ogni stato ha bisogno di un riferimento al motore di gioco; Mi chiedevo come avrebbe funzionato diversamente.
Vargonian

2

Comincio gestendo questo genere di cose nel modo più semplice possibile.

bool isPieceMoving;

Quindi aggiungerò i segni di spunta contro quella bandiera booleana nei punti pertinenti.

Se in seguito trovo che ho bisogno di casi più speciali di questo - e solo di esso - ricomprendo in qualcosa di meglio. Di solito ci sono 3 approcci che prenderò:

  • Trasforma in enumerare tutte le bandiere esclusive che rappresentano il sottostato. per esempio. enum { PRE_MOVE, MOVE, POST_MOVE }e aggiungi le transizioni dove necessario. Quindi posso verificare contro questo enum dove ero solito controllare contro la bandiera booleana. Questa è una semplice modifica, ma che riduce il numero di cose da verificare, consente di utilizzare le istruzioni switch per gestire il comportamento in modo efficace, ecc.
  • Disattiva i singoli sottosistemi secondo necessità. Se l'unica differenza durante la sequenza di battaglia è che non puoi spostare pezzi, puoi chiamare pieceSelectionManager->disable()o simili all'inizio della sequenza e pieceSelectionManager->enable(). In sostanza hai ancora flag, ma ora sono memorizzati più vicino all'oggetto che controllano e non è necessario mantenere alcuno stato aggiuntivo nel codice di gioco.
  • La parte precedente implica l'esistenza di un PieceSelectionManager: più in generale, puoi scomporre parti del tuo stato di gioco e comportamento in oggetti più piccoli che gestiscono un sottoinsieme dello stato generale in modo coerente. Ognuno di questi oggetti avrà un suo stato che determina il suo comportamento ma è facile da gestire poiché è isolato dagli altri oggetti. Resisti alla tentazione di consentire al tuo oggetto gamestate o al loop principale di diventare una discarica per gli pseudo-globali e di tenerne conto!

In generale, non ho mai bisogno di andare oltre questo quando si tratta di sottostati per casi speciali, quindi non credo che ci sia il rischio che "sfugga di mano".


1
Sì, immagino che ci sia una linea di demarcazione tra andare a tutto campo con gli stati e usare solo un bool / enum quando appropriato. Ma conoscendo le mie tendenze pedanti, probabilmente finirò per rendere quasi ogni stato la propria classe.
Vargonian

Fai sembrare che una classe sia più corretta delle alternative, ma ricorda che è soggettiva. Se inizi a creare troppe piccole classi per cose che possono essere rappresentate più facilmente da altri costrutti di linguaggio, allora puoi oscurare l'intento del codice.
Kylotan,

1

http://www.ai-junkie.com/architecture/state_driven/tut_state1.html è un adorabile tutorial per la gestione dello stato dei giochi! Puoi usarlo per entità di gioco o per un sistema di menu come sopra.

Comincia a insegnare il modello di progettazione statale , quindi continua a implementare un State Machine, e successivamente lo estende ulteriormente. È un'ottima lettura! Ti darà una solida comprensione di come funziona l'intero concetto e di come applicarlo a nuovi tipi di problemi!


1

Cerco di non usare la macchina a stati e i booleani per questo scopo, perché entrambi non sono scalabili. Entrambi si trasformano in disordine quando cresce il numero di stati.

Di solito disegno il gameplay come una sequenza di azioni e conseguenze, ogni stato del gioco viene naturalmente senza la necessità di definirlo separatamente.

Ad esempio, nel tuo caso con la disabilitazione dell'input del giocatore: hai un gestore di input dell'utente e qualche indicazione visiva sul gioco che l'input è disabilitato, dovresti renderli un oggetto o un componente, quindi per disabilitare l'input devi solo disabilitare l'intero oggetto, non è necessario sincronizzali in qualche macchina a stati o reagisci a qualche indicatore booleano.

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.