Как вы моделируете состояния приложений? - PullRequest
7 голосов
/ 18 марта 2009

Я пишу игру и хочу смоделировать ее различные состояния (я полагаю, что аналогией 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 и т. 1004 *

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

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 ... - приостановка больше не будет состоянием, а не будет решаться в тех состояниях, где это имеет смысл).

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

Ответы [ 4 ]

10 голосов
/ 18 марта 2009

Следующая статья дает хороший, простой способ управления состояниями игры:

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

По сути, вы поддерживаете набор игровых состояний и просто запускаете верхнее состояние. Вы правы, что во многих штатах будет только один экземпляр, но это не проблема. Однако на самом деле многие из состояний, о которых вы говорите, могут иметь несколько экземпляров. E.g.:

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

... и вы можете начать заново с нового экземпляра LevelIntroState и т. Д.

3 голосов
/ 19 марта 2009

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

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

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

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.

2 голосов
/ 18 марта 2009

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

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

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

1 голос
/ 19 марта 2009

Я использую менеджер игрового состояния со списком GameStates, где каждый элемент в списке является «объектом GameState», который реализует 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.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...