c++design-patternssfmlgame-developmentstate-pattern

Using The State Pattern in games


Recently, I've tried to create Snake game in SFML. However, I also wanted to use some design pattern to make some good habits for future programming - it was The State Pattern. But - there is some problem that I am unable to solve.

To make everything clear, I've tried to make several Menus - one main menu, and others, like "Options", or something like this. The first option of the main menu would take the player to the "Playing State". But then, the problem appears - I think the whole game should be an independent module implemented to program. So, what should I do with the actual state which program is in? (for example, let's call this state "MainMenu").

Should I make an additional state called "PlayingState", which would represent the whole game? How would I do it? How is it possible to add new functionality to a single state? Do you have any ideas?


Solution

  • The State Pattern allows you, for example, to have an object of a class Game and alter its behavior when the game state changes providing the illusion that this Game object had changed its type.

    As an example, imagine a game that has an initial menu and can be paused while playing if you press the space bar. When the game is paused, you can either go back to the initial menu by pressing the backspace key or continue playing by pressing the space bar again: State-Diagram

    First, we define an abstract class, GameState:

    struct GameState {
        virtual GameState* handleEvent(const sf::Event&) = 0;
        virtual void update(sf::Time) = 0;
        virtual void render() = 0;
        virtual ~GameState() = default; 
    };
    

    All the state classes – i.e., MenuState, PlayingState, PausedState – will publicly derive from this GameState class. Note that handleEvent() returns a GameState *; this is for providing the transitions between the states (i.e., the next state, if a transition occurs).

    Let's focus for the moment on the Game class instead. Eventually, our intention is to use the Game class as in the following way:

    auto main() -> int {
       Game game;
       game.run();
    }
    

    That is, it has basically a run() member function that returns when the game is over. We define the Game class:

    class Game {
    public:
       Game();
        void run();
    private:
       sf::RenderWindow window_;
    
       MenuState menuState_;
       PausedState pausedState_;
       PlayingState playingState_;
    
       GameState *currentState_; // <-- delegate to the object pointed
    };
    

    The key point here is the currentState_ data member. At all times, currentState_ points to one of the three possible states for the game (i.e., menuState_, pausedState_, playingState_).

    The run() member function relies on delegation; it delegates to the object pointed by currentState_:

    void Game::run() {
       sf::Clock clock;
    
       while (window_.isOpen()) {
          // handle user-input
          sf::Event event;
          while (window_.pollEvent(event)) {
             GameState* nextState = currentState_->handleEvent(event);
             if (nextState) // must change state?
                currentState_ = nextState;
          }
         
          // update game world
          auto deltaTime = clock.restart();
          currentState_->update(deltaTime);
    
          currentState_->render();
       }
    }
    

    Game::run() calls the GameState::handleEvent(), GameState::update() and GameState::render() member functions that every concrete class that derives from GameState must override. That is, Game does not implement the logic for handling the events, updating the game state and rendering; it just delegates these responsabilities to the GameState object pointed by its data member currentState_. The illusion that Game appears to change its type when its internal state is altered is achieved through this delegation.

    Now, back to the concrete states. We define the PausedState class:

    class PausedState: public GameState {
    public:
       PausedState(MenuState& menuState, PlayingState& playingState):
          menuState_(menuState), playingState_(playingState) {}
    
        GameState* handleEvent(const sf::Event&) override;
        void update(sf::Time) override;
        void render() override;
    private:
       MenuState& menuState_;
       PlayingState& playingState_;
    };
    

    PlayingState::handleEvent() must at some time return the next state to transition into, and this will correspond to either Game::menuState_ or Game::playingState_. Therefore, this implementation contains references to both MenuState and PlayingState objects; they will be set to point to Game::menuState_ and Game::playingState_ data members at PlayState's construction. Also, when the game is paused, we ideally want to render the screen corresponding to the playing state as the starting point, as we will see below.

    The implementation of PauseState::update() consists of doing nothing, the game world simply remains the same:

    void PausedState::update(sf::Time) { /* do nothing */ }
    

    PausedState::handleEvent() only reacts to the events of either pressing the space bar or the backspace:

    GameState* PausedState::handleEvent(const sf::Event& event) {
       if (event.type == sf::Event::KeyPressed) {
    
          if (event.key.code == sf::Keyboard::Space)
             return &playingState_; // change to playing state
    
          if (event.key.code == sf::Keyboard::Backspace) {
             playingState_.reset(); // clear the play state
             return &menuState_; // change to menu state
          }
       }
       // remain in the current state
       return nullptr; // no transition
    }
    

    PlayingState::reset() is for clearing the PlayingState to its initial state after construction as we go back to the initial menu before we start to play.

    Finally, we define PausedState::render():

    void PausedState::render() {
       // render the PlayingState screen
       playingState_.render();
    
       // render a whole window rectangle
       // ...
    
       // write the text "Paused"
       // ...
    }
    

    First, this member function renders the screen corresponding to the playing state. Then, on top of this rendered screen of the playing state, it renders a rectangle with a transparent background that fits the whole window; this way, we darken the screen. On top of this rendered rectangle, it can render something like the "Pause" text.

    A stack of states

    Another architecture consists of a stack of states: states stack up on top of other states. For example, the pause state will live on top of the playing state. Events are delivered from the topmost state to the bottommost, and so are states updated as well. The rendering is performed from the bottom to the top.

    This variation can be considered a generalization of the case exposed above as you can always have – as a particular case – a stack that consists of just a single state object, and this case would correspond to the ordinary State Pattern.

    If you are interested in learning more about this other architecture, I would recommend reading the fifth chapter of the book SFML Game Development.