Implementing finite state machines

What we really need is the ability to define our states outside the game class, and have the state itself take care of what it needs to load, render, and update. For this we can create what is known as an FSM. The definition of FSM, as we will use it, is a machine that can exist in a finite number of states, can exist in only one state at a time (known as the current state), and can change from one state to another (known as a transition).

A base class for game states

Let's start our implementation by creating a base class for all of our states; create a header file called GameState.h:

#include<string>
class GameState
{
public:
  virtual void update() = 0;
  virtual void render() = 0;

  virtual bool onEnter() = 0;
  virtual bool onExit() = 0;

  virtual std::string getStateID() const = 0;
};

Just like our GameObject class, this is an abstract base class; we aren't actually putting any functionality into it, we just want all of our derived classes to follow this blueprint. The update and render functions are self-explanatory, as they will function just like the functions we created in the Game class. We can think of the onEnter and onExit functions as similar to other load and clean functions; we call the onEnter function as soon as a state is created and onExit once it is removed. The last function is a getter for the state ID; each state will need to define this function and return its own staticconst ID. The ID is used to ensure that states don't get repeated. There should be no need to change to the same state, so we check this using the state ID.

That's it for our GameState base class; we can now create some test states that derive from this class. We will start with a state called MenuState. Go ahead and create MenuState.h and MenuState.cpp in our project, open up MenuState.h, and start coding:

#include"GameState.h"

class MenuState : public GameState
{
public:

  virtual void update();
  virtual void render();

  virtual bool onEnter();
  virtual bool onExit();

  virtual std::string getStateID() const { return s_menuID; }

private:

  static const std::string s_menuID;
};

We can now define these methods in our MenuState.cpp file. We will just display some text in the console window for now while we test our implementation; we will give this state an ID of "MENU":

#include "MenuState.h"

const std::string MenuState::s_menuID = "MENU";

void MenuState::update()
{
  // nothing for now
}

void MenuState::render()
{
  // nothing for now
}

bool MenuState::onEnter()
{
  std::cout << "entering MenuState
";
  return true;
}

bool MenuState::onExit()
{
  std::cout << "exiting MenuState
";
  return true;
}

We will now create another state called PlayState, create PlayState.h and PlayState.cpp in our project, and declare our methods in the header file:

#include "GameState.h"

class PlayState : public GameState
{
public:

  virtual void update();
  virtual void render();

  virtual bool onEnter();
  virtual bool onExit();

  virtual std::string getStateID() const { return s_playID; }

private:

  static const std::string s_playID;
};

This header file is the same as MenuState.h with the only difference being getStateID returning this class' specific ID ("PLAY"). Let's define our functions:

#include "PlayState.h"

const std::string PlayState::s_playID = "PLAY";

void PlayState::update()
{
  // nothing for now
}

void PlayState::render()
{
  // nothing for now
}

bool PlayState::onEnter()
{
  std::cout << "entering PlayState
";
  return true;
}

bool PlayState::onExit()
{
  std::cout << "exiting PlayState
";
  return true;
}

We now have two states ready for testing; we must next create our FSM so that we can handle them.

Implementing FSM

Our FSM is going to need to handle our states in a number of ways, which include:

  • Removing one state and adding another: We will use this way to completely change states without leaving the option to return
  • Adding one state without removing the previous state: This way is useful for pause menus and so on
  • Removing one state without adding another: This way will be used to remove pause states or any other state that had been pushed on top of another one

Now that we have come up with the behavior we want our FSM to have, let's start creating the class. Create the GameStateMachine.h and GameStateMachine.cpp files in our project. We will start by declaring our functions in the header file:

#include "GameState.h"

class GameStateMachine
{
public:

  void pushState(GameState* pState);
  void changeState(GameState* pState);
  void popState();
};

We have declared the three functions we need. The pushState function will add a state without removing the previous state, the changeState function will remove the previous state before adding another, and finally, the popState function will remove whichever state is currently being used without adding another. We will need a place to store these states; we will use a vector:

private:

std::vector<GameState*> m_gameStates;

In the GameStateMachine.cpp file, we can define these functions and then go through them step-by-step:

void GameStateMachine::pushState(GameState *pState)
{
  m_gameStates.push_back(pState);
  m_gameStates.back()->onEnter();
}

This is a very straightforward function; we simply push the passed-in pState parameter into the m_gameStates array and then call its onEnter function:

void GameStateMachine::popState()
{
  if(!m_gameStates.empty())
  {
    if(m_gameStates.back()->onExit())
    {
      delete m_gamestates.back();
      m_gameStates.pop_back();
    }
  }
}

Another simple function is popState. We first check if there are actually any states available to remove, and if so, we call the onExit function of the current state and then remove it:

void GameStateMachine::changeState(GameState *pState)
{
  if(!m_gameStates.empty())
  {
    if(m_gameStates.back()->getStateID() == pState->getStateID())
    {
      return; // do nothing
    }

    if(m_gameStates.back()->onExit())
    {
      delete m_gamestates.back();
      m_gameStates.pop_back();
    }
  }

  // push back our new state
  m_gameStates.push_back(pState);

  // initialise it
  m_gameStates.back()->onEnter();
}

Our third function is a little more complicated. First, we must check if there are already any states in the array, and if there are, we check whether their state ID is the same as the current one, and if it is, then we do nothing. If the state IDs do not match, then we remove the current state, add our new pState, and call its onEnter function. Next, we will add new GameStateMachine as a member of the Game class:

GameStateMachine* m_pGameStateMachine;

We can then use the Game::init function to create our state machine and add our first state:

m_pGameStateMachine = new GameStateMachine();
m_pGameStateMachine->changeState(new MenuState());

The Game::handleEvents function will allow us to move between our states for now:

void Game::handleEvents()
{
  TheInputHandler::Instance()->update();

  if(TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_RETURN))
  {
    m_pGameStateMachine->changeState(new PlayState());
  }
}

When we press the Enter key, the state will change. Test the project and you should get the following output after changing states:

entering MenuState
exiting MenuState
entering PlayState

We now have the beginnings of our FSM and can next add update and render functions to our GameStateMachine header file:

void update();
void render();

We can define them in our GameStateMachine.cpp file:

void GameStateMachine::update()
{
  if(!m_gameStates.empty())
  {
    m_gameStates.back()->update();
  }
}

void GameStateMachine::render()
{
  if(!m_gameStates.empty())
  {
    m_gameStates.back()->render();
  }
}

These functions simply check if there are any states, and if so, they update and render the current state. You will notice that we use back() to get the current state; this is because we have designed our FSM to always use the state at the back of the array. We use push_back() when adding new states so that they get pushed to the back of the array and used immediately. Our Game class will now use the FSM functions in place of its own update and render functions:

void Game::render()
{
  SDL_RenderClear(m_pRenderer); 

  m_pGameStateMachine->render();

  SDL_RenderPresent(m_pRenderer); 
}

void Game::update()
{
  m_pGameStateMachine->update();
}

Our FSM is now in place.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.122.68