2009-03-18 13 views
7

Sto scrivendo un gioco e voglio modellarne i diversi stati (l'analogia con Game Maker sarebbe un frame, suppongo) in un modo pulito e orientato agli oggetti. In precedenza, l'ho eseguito nel seguente modo:Come si modellano gli stati dell'applicazione?

class Game 
{ 
    enum AppStates 
    { 
    APP_STARTING, 
    APP_TITLE, 
    APP_NEWGAME, 
    APP_NEWLEVEL, 
    APP_PLAYING, 
    APP_PAUSED, 
    APP_ENDED 
    }; 

    typedef AppState(Game::*StateFn)(); 
    typedef std::vector<StateFn> StateFnArray; 

    void Run() 
    { 
    // StateFn's to be registered here 

    AppState lastState(APP_STARTING); 
    while(lastState != APP_ENDED) 
    { 
     lastState = GetCycle_(lastState); 
    } 
    // cleanup 
    } 

protected: 
    // define StateFn's here 

    AppState GetCycle_(AppState a) 
    { 
    // pick StateFn based on passed variable, call it and return its result. 
    } 

    StateFnArray states_; 
}; 

Questo era difficilmente gestibile per un progetto più piccolo. Tutte le variabili utilizzate dagli stati sono state scaricate nella classe Game, tuttavia vorrei mantenere l'orientazione degli oggetti al massimo, solo esponendo le variabili condivise da più di uno stato. Voglio anche essere in grado di inizializzare un nuovo stato quando si passa ad esso piuttosto che doverlo fare nello stato che sta terminando (in quanto potrebbe avere più esiti - APP_PLAYING può passare a APP_PAUSED, APP_GAMEOVER, APP_NEWLEVEL, ecc.).

ho pensato a qualcosa di simile (ATTENZIONE STUFF FUZZY!!):

struct AppState 
{ 
    enum { LAST_STATE = -1; } 
    typedef int StateID; 
    typedef std::vector<AppState*> StateArray; 

    static bool Add(AppState *state, StateID desiredID); 
    // return false if desiredID is an id already assigned to 

    static void Execute(StateID state) 
    { 
    while(id != LAST_STATE) 
    { 
     // bounds check etc. 
     states_[id]->Execute(); 
    } 
    } 

    AppState() {}; 
    virtual ~AppState() {}; 

    virtual StateID Execute() =0; // return the ID for the next state to be executed 

protected: 
    static StageArray stages_; 
}; 

Il problema qui è che i livelli di classe e istanza sono sempre mescolate fino (statica e virtuale). Gli stati devono ereditare da AppState, ma - come immagino - la maggior parte di loro sarebbero classi con membri completamente statici, o almeno non avrò bisogno di più di un'istanza da una classe (TitleState, LevelIntroState, PlayingState , GameOverState, EndSequenceState, EditorState ... - la sospensione non sarebbe più uno stato, piuttosto che essere curato negli stati in cui ha senso).

Come può essere fatto in modo elegante ed efficiente?

risposta

10

Il seguente articolo fornisce un modo semplice e piacevole per gestire gli stati di gioco:

http://gamedevgeek.com/tutorials/managing-game-states-in-c/

In sostanza, si mantiene una pila di stati di gioco, e basta eseguire lo stato alto. Hai ragione che molti stati avrebbero solo un'istanza, ma questo non è davvero un problema. In realtà, però, molti degli stati di cui parli potrebbero avere più istanze. Es .:

push TitleState 
push MenuState 
push LevelIntroState 
change_to PlayingState 
change_to GameOverState 
pop (back to MenuState) 

... e si può ricominciare con una nuova istanza di LevelIntroState, e così via.

2

Ecco la mia soluzione:

  • Ogni stato è come un piccolo gioco, in modo da gestire una serie di giochi su una pila.
  • Gli eventi fanno esplodere lo stack fino a quando qualcuno non li ferma (quindi i "giochi" più avanti non li vedono più). Questo mi consente di ingrandire la mappa tramite più/meno in un menu. OTOH, Esc interrompe il bubbling sin da quando il primo menu aperto lo inghiotte.
  • Ogni "gioco" nello stack ha gli stessi metodi: handleUserEvent(), keyDown(), keyUp(), mousePressed(), mouseReleased(), mouseMotion(), update() (calcoli interni prima del rendering), draw() (rendering), prepare() (ottimizza il rendering inserendo nella cache qualcosa in una texture appena stampata sulla superficie di destinazione in draw)

Per il rendering, sto usando i livelli con priorità. Quindi ogni gioco eseguirà il rendering su una tela trasparente e il renderizzatore di livelli li renderà nell'ordine corretto. In questo modo, ogni gioco può aggiornare il proprio livello senza preoccuparsi di ciò che fanno gli altri.

1

Uso un manager di stato di gioco con un elenco di GameState, in cui ciascun elemento nell'elenco è un "oggetto GameState" che implementa IGameState e ha due metodi .render() e.HandleInput()

Questo GameStateManager è implementato come un singleton in modo che qualsiasi Stato può passare a qualsiasi altro stato chiamando

GameStateManager.gi().setState("main menu") 

E il ciclo principale sembra qualcosa di simile

while(isRunning) 
{ 
    GameStateManager.gi().getCurrentState().handleKeyboard(keysobject); 
    GameStateManager.gi().getCurrentState().handleMouse(mouseobject); 

    GameStateManager.gi().getCurrentState().render(screenobject); 

} 

questo modo per creare stati, basta creare una nuova classe che implementa IGameState e aggiungerla al GameStateManager.

(Nota: Questo è un modo molto utile per fare mini-giochi all'interno del vostro gioco principale)

3

sto usando un certo tipo di factory pattern combinato con un state pattern nel mio presto-to-be gioco.

Il codice potrebbe essere un po 'confuso ma proverò a ripulirlo.

Questa è la classe da cui derivano tutti gli stati, come il menu, il gioco o qualsiasi altra cosa.

class GameState { 
public: 
    virtual ~GameState() { } 

    virtual void Logic() = 0; 
    virtual void Render() = 0; 
}; 

Questa classe sarà la vostra interfaccia per gestire i diversi stati. Puoi dinamicamente aggiungere e id dinamicamente.

class State { 
public: 
    State(); 
    virtual ~State(); 

    void Init(); 
    void Shutdown(); 
    void SetNext(std::string next_state); 
    void Exit(); 

    bool Logic(); 
    void Render(); 
protected: 
    bool Change(); 

    std::string state_id; 
    std::string next_state; 

    GameState *current_state; 
    std::vector<std::string> state_ids; 

    StateFactory *state_factory; 

    bool is_init; 
}; 

Sto utilizzando un functor per gestire la creazione di diversi derivati ​​di GameState.

class BasicStateFunctor { 
public: 
    virtual GameState *operator()() = 0; 
}; 

template<class T> 
class StateFunctor : public BasicStateFunctor { 
public: 
    StateFunctor() { } 
    GameState *operator()() { 
     return new T; 
    } 
    typedef T type; 
}; 

Infine una fabbrica che memorizzerà e gestirà i diversi stati.

class StateFactory { 
public: 
    StateFactory(); 
    virtual ~StateFactory(); 

    bool CheckState(std::string id); 
    GameState *GetState(std::string id); 
    template<class T> void AddState(std::string id); 
private: 
    typedef std::map<std::string, BasicStateFunctor*>::iterator StateIt; 
    std::map<std::string, BasicStateFunctor*> state_map; 
}; 

Nel file di definizione: Qui ho lasciato fuori un sacco di roba, ma si spera che si otterrà l'idea.

bool StateFactory::CheckState(std::string id) 
{ 
    StateIt it = state_map.find(id); 
    if(it != state_map.end()) 
     return true; 
    else 
     return false; 
} 

GameState *StateFactory::GetState(std::string id) 
{ 
    StateIt it = state_map.find(id); 
    if(it != state_map.end()) 
    { 
     return (*(*it).second)(); 
    } else { 
     //handle error here 
} 

template<class T> void StateFactory::AddState(std::string id) 
{ 
    StateFunctor<T> *f = new StateFunctor<T>(); 
    state_map.insert(state_map.end(), std::make_pair(id, f)); 
} 

void State::Init() 
{ 
    state_factory = new StateFactory(); 
    state_factory->AddState<Game>("game"); 
    current_state = state_factory->GetState("game"); 
    is_init = true; 
} 

void State::SetNext(std::string new_state) 
{ 
    //if the user doesn't want to exit 
    if(next_state != "exit") { 
     next_state = new_state; 
    } 
} 

bool State::Change() 
{ 
    //if the state needs to be changed 
    if(next_state != "" && next_state != "exit") 
    { 

     //if we're not about to exit(destructor will call delete on current_state), 
     //call destructor if it's a valid new state 
     if(next_state != "exit" && state_factory->CheckState(next_state)) 
     { 
      delete current_state; 

      current_state = state_factory->GetState(next_state); 

     } 
     else if(next_state == "exit") 
     { 
       return true; 
     } 

     state_id = next_state; 

     //set NULL so state doesn't have to be changed 
     next_state = ""; 
    } 
    return false; 
} 

bool State::Logic() 
{ 
    current_state->Logic(); 
    return Change(); 
} 

Ed ecco come lo si utilizza: inizializzazione e aggiungere i diversi stati, lo sto facendo nel Init().

State.Init(); 

//remember, here's the Init() code: 
state_factory = new StateFactory(); 
state_factory->AddState<Game>("game"); 
current_state = state_factory->GetState("game"); 
is_init = true; 

Per la funzione di telaio

State.Logic(); //Here I'm returning true when I want to quit 

E per la funzione di rendering

State.Render(); 

Questo può non essere perfetto, ma funziona bene per me. Per avanzare ulteriormente nella progettazione, aggiungere Singleton for State e magari rendere StateFactory come una classe nascosta all'interno di State.

Problemi correlati