Как вы моделируете состояния приложений?

Я пишу игру и хочу смоделировать ее различные состояния (полагаю, аналогия с Game Maker — кадры) чистым, объектно-ориентированным способом. Раньше я делал это следующим образом:

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

Это было едва ли управляемым для меньшего проекта. Все переменные, которые использовались состояниями, были сброшены в класс Game, однако я хотел бы максимально сохранить объектно-ориентированность, предоставляя только те переменные, которые используются более чем одним состоянием. Я также хочу иметь возможность инициализировать новое состояние при переключении на него, а не делать это в состоянии, которое только что закончилось (поскольку оно может иметь несколько результатов — APP_PLAYING может переключиться на APP_PAUSED, APP_GAMEOVER, APP_NEWLEVEL и т. д.).

Я подумал о чем-то вроде этого (ВНИМАНИЕ! НЕЧЕТКАЯ ВЕЩЬ!):

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

Проблема здесь в том, что уровни класса и экземпляра смешиваются (статические и виртуальные). Состояния должны наследоваться от AppState, но, как я себе представляю, большинство из них будут классами со полностью статическими членами или, по крайней мере, мне не понадобится более одного экземпляра из одного класса (TitleState, LevelIntroState, PlayingState , GameOverState, EndSequenceState, EditorState... — пауза больше не будет состоянием, а будет выполняться в тех состояниях, где это имеет смысл).

Как это сделать красиво и эффективно?


person zyndor    schedule 18.03.2009    source источник


Ответы (4)



Я использую какой-то фабричный шаблон в сочетании с шаблон состояния в моей будущей игре.

Код может быть немного беспорядочным, но я постараюсь его почистить.

Это класс, из которого вы будете получать все состояния, такие как меню, игра или что-то еще.

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

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

Этот класс будет вашим интерфейсом для обработки различных состояний. Вы можете динамически добавлять и идентифицировать динамически.

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

Я использую функтор для создания различных производных от 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;
};

Наконец, фабрика, которая будет хранить и управлять различными состояниями.

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

В вашем файле определения: Здесь я многое упустил, но, надеюсь, вы поняли идею.

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

И вот как вы его используете: инициализируйте и добавляйте различные состояния, я делаю это в 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;

Для функции кадра

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

И для функции рендеринга

State.Render();

Это может быть не идеально, но это отлично работает для меня. Чтобы еще больше усовершенствовать дизайн, вы захотите добавить Singleton для State и, возможно, сделать StateFactory скрытым классом внутри State.

person Jonas    schedule 18.03.2009

Вот мое решение:

  • Каждое состояние похоже на небольшую игру, поэтому я управляю набором игр в стеке.
  • События всплывают в стеке до тех пор, пока кто-нибудь их не остановит (так что «игры» дальше их больше не видят). Это позволяет мне масштабировать карту с помощью плюс/минус в меню. OTOH, Esc рано останавливает всплытие, так как первое открытое меню проглатывает его.
  • Каждая «игра» в стеке имеет одни и те же методы: handleUserEvent(), keyDown(), keyUp(), mousePressed(), mouseReleased(), mouseMotion(), update() (внутренние вычисления перед рендерингом), draw() ( рендеринг), prepare() (оптимизируйте рендеринг, кэшируя что-то в текстуре, которая только что отпечатана на целевой поверхности в draw())

Для рендеринга я использую слои с приоритетами. Таким образом, каждая игра будет отображаться на прозрачном холсте, а средство визуализации слоев будет отображать их в правильном порядке. Таким образом, каждая игра может обновлять свой собственный слой, не беспокоясь о том, что делают все остальные.

person Aaron Digulla    schedule 18.03.2009

Я использую менеджер Game State со списком GameStates, где каждый Item в списке представляет собой «GameState Object», который реализует IGameState и имеет два метода .render() и .HandleInput().

Этот GameStateManager реализован как синглтон, поэтому любое состояние может перейти в любое другое состояние, вызвав

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

И основной цикл выглядит примерно так

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

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

}

Таким образом, для создания состояний просто создайте новый класс, реализующий IGameState, и добавьте его в GameStateManager.

(Примечание: это очень удобный способ создавать мини-игры в основной игре)

person JSmyth    schedule 18.03.2009