3

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?

JFMR
  • 23,265
  • 4
  • 52
  • 76
Thorvas
  • 45
  • 10

2 Answers2

7

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.

JFMR
  • 23,265
  • 4
  • 52
  • 76
  • 1
    Your answer was very informative and useful! However, how could I learn more about this first architecture you mentioned? Where did you learn about it? – Thorvas Aug 26 '20 at 21:41
  • And why you have private reference to `menuState_` and `playingState_` in `PausedState` but in `Game` class you create whole objects without references? – Thorvas Aug 26 '20 at 21:57
  • The first architecture is *The State Pattern*. A `Game` object owns all state objects (i.e., objects of type `MenuState`, `PlayingState` and `PausedState`), whereas `PausedState` doesn't; it, however, needs references to the two other states because it can make the `Game` object to transition into one of those states: while being in the paused state, if the *space bar* is pressed the game continues, whereas if *backspace* is pressed instead, it returns to the menu. I'd suggest you have a look at `PausedState::handleEvent()`, which returns a `GameState *` pointing to the next state object. – JFMR Aug 27 '20 at 06:42
  • @Thorvas In any case, I strongly recommend you have a look at the architecture consisting of the *stack of states* as it is usually a better fit for dealing with the states a game has. For example, coming back to the ownership issue, the state stack architecture wouldn't need the `PausedState` to own a `PlayingState` object. Instead, the `PausedState` object in the stack of states would be sitting on top of the `PlayingState` object. – JFMR Sep 02 '20 at 13:12
1

For you design, i think you can use incremented loop for the different state:

Simple example:

// main loop
while (window.isOpen()) {
    // I tink you can simplify this "if tree"
    if (state == "MainMenu")
        state = run_main_menu(/* args */);
    else if (state == "Play")
        state = run_game(/* args */);
    // Other state here
    else
        // error state unknow
        // exit the app
}

And when the game is running:

state run_game(/* args */)
{
    // loading texture, sprite,...
    // or they was passe in args

    while (window.isOpen()) {
        while (window.pollEvent(event)) {
            // checking event for your game
        }
        // maybe modifying the state
        // Display your game
        // Going to the end game menu if the player win/loose
        if (state == "End")
            return run_end_menu(/* args */);
            // returning the new state, certainly MainMenu
        else if (state != "Play")
            return state;
    }
}

You have a main menu and the game, your state by default is "MainMenu".

When you enter in your main menu you click the play button, then the state returns "Play" and you go back to the main loop.

The state is "Play" so you go to the game menu and your start your game.

When the game ends, you change your state to "EndGame" and go out of the game menu to the end menu.

The end menu returns the new menu to display, so you go back to the main loop and check every available menu.

With this design you can add a new menu without changing the entire architecture.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
DipStax
  • 343
  • 2
  • 12