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).
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.
Our FSM is going to need to handle our states in a number of ways, which include:
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.
3.133.122.68