State Machines
Last Updated: Nov 26th, 2024
Up until now our applications have had a single game state. Here we'll be switching between game states with a state machine.
This is going to be huge tutorial. The source code for the demo program is over 1500 lines of code. We're going to have multiple game states we're going to switching between.
For each of these stae the events, logic, and rendering work differently. To change the way your game loop works, we use a state machine to transition between the different states.
Here we're going to be using a finite state machine and there are different ways to implement it.
We're going to have a intro screen and title screen:
Then we're going to have an overworld you can move around in:
From that overworld you can enter a red room or a blue room:
For each of these stae the events, logic, and rendering work differently. To change the way your game loop works, we use a state machine to transition between the different states.
Here we're going to be using a finite state machine and there are different ways to implement it.
//Do logic switch( gameState ) { case STATE_INTRO: intro_logic(); break; case STATE_TITLE: title_logic(); break; case STATE_OVERWORLD: overworld_logic(); break; }
Here's what a state machine's logic module would look like if it was used implementing the switch/case method where the code we run is based on a switch statement. The problem with this method is that it doesn't scale very well.
while ( quit == false ) { //Do events switch( gameState ) { case STATE_INTRO: intro_events(); break; case STATE_TITLE: title_events(); break; case STATE_MENU: menu_events(); break; case STATE_STAGE01: stage01_events(); break; case STATE_STAGE02: stage02_events(); break; case STATE_STAGE03: stage03_events(); break; case STATE_STAGE04: stage04_events(); break; case STATE_STAGE05: stage05_events(); break; case STATE_BONUS_STAGE: bonus_stage_events(); break; } //Do logic switch( gameState ) { case STATE_INTRO: intro_logic(); break; case STATE_TITLE: title_logic(); break; case STATE_MENU: menu_logic(); break; case STATE_STAGE01: stage01_logic(); break; case STATE_STAGE02: stage02_logic(); break; case STATE_STAGE03: stage03_logic(); break; case STATE_STAGE04: stage04_logic(); break; case STATE_STAGE05: stage05_logic(); break; case STATE_BONUS_STAGE: bonus_stage_logic(); break; } //Change the state if needed change_state(); //Do Rendering switch( gameState ) { case STATE_INTRO: intro_render(); break; case STATE_TITLE: title_render(); break; case STATE_MENU: menu_render(); break; case STATE_STAGE01: stage01_render(); break; case STATE_STAGE02: stage02_render(); break; case STATE_STAGE03: stage03_render(); break; case STATE_STAGE04: stage04_render(); break; case STATE_STAGE05: stage05_render(); break; case STATE_BONUS_STAGE: bonus_stage_render(); break; } }
As you can see this got cluttered pretty quickly. There's only 9 states represented here and you can only imagine how many game states a real game has.
//Run main loop while( quit == false ) { //Do events currentState->events(); //Do logic currentState->logic(); //Change state if needed changeState(); //Render currentState->render(); }
Here's what the object oriented method would look like. We create a base game state class with virtual functions for each part of the game loop. Then we have game state classes that inherit from the base class and override the functions to work as the game state needs them to. To
switch the game state, we simply change the game state object as needed.
Now, for these tutorials I assume you were where I was when I was starting game programming. My computer science program taught C++ over two semesters with the first semester being an intro to programming that focused mostly on C concepts with object oriented programming being taught in the second semester. These days it's much more common for students to start out with Java/C#/Python so you probably learned about inheritance and polymorphism earlier than I did. If you don't know what I mean by base class or overriding functions, we'll cover that in a bit.
Now, for these tutorials I assume you were where I was when I was starting game programming. My computer science program taught C++ over two semesters with the first semester being an intro to programming that focused mostly on C concepts with object oriented programming being taught in the second semester. These days it's much more common for students to start out with Java/C#/Python so you probably learned about inheritance and polymorphism earlier than I did. If you don't know what I mean by base class or overriding functions, we'll cover that in a bit.
class Dot { public: //The dimensions of the dot static constexpr int kDotWidth = 20; static constexpr int kDotHeight = 20; //Maximum axis velocity of the dot static constexpr int kDotVel = 10; //Initializes the variables Dot(); //Set position void setPos( int x, int y ); //Takes key presses and adjusts the dot's velocity void handleEvent( SDL_Event& e ); //Moves the dot void move( int levelWidth, int levelHeight ); //Shows the dot on the screen void render( SDL_Rect camera ); //Collision box accessor SDL_Rect getCollider(); private: //Position/size of the dot SDL_Rect mCollisionBox; //The velocity of the dot int mVelX, mVelY; }; class House { public: //The house dimensions static constexpr int kHouseWidth = 40; static constexpr int kHouseHeight = 40; //Initializes variables House(); //Sets the house's position/graphic void set( int x, int y, LTexture* houseTexture ); //Renders house relative to the camera void render( SDL_Rect camera ); //Gets the collision box SDL_Rect getCollider(); private: //The house graphic LTexture* mHouseTexture; //Position/size of the house SDL_Rect mCollisionBox; }; class Door { public: //The door dimensions static constexpr int kDoorWidth = 20; static constexpr int kDoorHeight = 40; //Initializes variables Door(); //Sets the door position void set( int x, int y ); //Shows the door void render(); //Gets the collision box SDL_Rect getCollider(); private: //Position/size of the door SDL_Rect mCollisionBox; };
Here are our main game objects we're going to be using for our state machine demo.
The dot should look fairly familiar with some adjustments to handle variable level sizes and setting position. The house and door classes render a house graphic and black square respectively and have functions to set their position and/or texture and get their collision boxes.
The dot should look fairly familiar with some adjustments to handle variable level sizes and setting position. The house and door classes render a house graphic and black square respectively and have functions to set their position and/or texture and get their collision boxes.
class GameState { public: //State transitions virtual bool enter() = 0; virtual bool exit() = 0; //Main loop functions virtual void handleEvent( SDL_Event& e ) = 0; virtual void update() = 0; virtual void render() = 0; //Make sure to call child destructors virtual ~GameState() = default; };
Here is our base game state class. All our other game states are going to inherit from it. For those of you who don't know object oriented programming, when a class inherits from another class it gets all its functions and variables.
Every game state has functions to enter/exit the state on state change and main loop functions. These functions are pure virtual as indicated by the
We also have a destructor that is virtual and is set the the default which does nothing. Why do we make the destructor do nothing? Because if the destructor is not set to virtual, the derived class destructor is never called. So even if your base class destructor does nothing, you still want to set it to virtual so you know your derived classes call their destructors.
Every game state has functions to enter/exit the state on state change and main loop functions. These functions are pure virtual as indicated by the
virtual
keyword and ending with a = 0;
and it means these functions are empty and must be overridden by
the classes that derive from them which we'll show how to do that it a bit.We also have a destructor that is virtual and is set the the default which does nothing. Why do we make the destructor do nothing? Because if the destructor is not set to virtual, the derived class destructor is never called. So even if your base class destructor does nothing, you still want to set it to virtual so you know your derived classes call their destructors.
class IntroState : public GameState { public: //Static accessor static IntroState* get(); //Transitions bool enter() override; bool exit() override; //Main loop functions void handleEvent( SDL_Event& e ) override; void update() override; void render() override; private: //Static instance static IntroState sIntroState; //Private constructor IntroState(); //Intro background LTexture mBackgroundTexture; //Intro message LTexture mMessageTexture; };
Here is our intro state which inherits from the game state base class with
It also has a static instance of itself with
This game state just shows a background and some text so we have variables for the textures. The
So why bother with all this class inheritance? Say if we had a game state set up like this:
Say if we wanted to switch over to game state Gamma. All we have to do is this:
: public GameState
. There's also protected and private style inheritance but public the the most common type. As you can see we have overridden the functions from the base class for the main loop
and to enter/exit the state. Even though you don't have to explicitly override functions, it's considered good practice to be explicit about what you are overriding.It also has a static instance of itself with
static IntroState sIntroState;
. We've done static constants before, but this is our first static variable. A static class variable is a variable that is global to the class and there is one instance of it for all instances of
the class. We only want on instance of IntroState to exist, which also why we made the constructor private to make sure that another instance cannot be instantiated. We also created a get
function to access our static instance of the intro state.This game state just shows a background and some text so we have variables for the textures. The
enter
function will load our textures and the exit
destroy them. The event handler will transition to the next state when the user presses enter, the update
function will be empty, and the rendering function will draw our textures.So why bother with all this class inheritance? Say if we had a game state set up like this:
GameStateAlpha gameStateAlpha;
GameStateBeta gameStateBeta;
GameStateGamma gameStateGamma;
GameStateDelta gameStateDelta;
GameStateEpsilon gameStateEpsilon;
GameState* currentGameState = &gameStateAlpha;
//Do GameState stuff
currentGameState->enter();
currentGameState->handleEvent( e );
currentGameState->update();
currentGameState->render();
currentGameState->exit();
We can do this because since GameStateAlpha inherits from GameState, it has all the functionality GameState has. Since GameStateAlpha overrides the base games state functions, it will call GameStateAlpha's versions of update/render/etc. The ability to have functionality change based
on what the pointer is pointing to is polymorphism.Say if we wanted to switch over to game state Gamma. All we have to do is this:
GameStateAlpha gameStateAlpha;
GameStateBeta gameStateBeta;
GameStateGamma gameStateGamma;
GameStateDelta gameStateDelta;
GameStateEpsilon gameStateEpsilon;
GameState* currentGameState = &gameStateGamma;
//Do GameState stuff
currentGameState->enter();
currentGameState->handleEvent( e );
currentGameState->update();
currentGameState->render();
currentGameState->exit();
Now the current game state will do the GameStateGamma functionality. It is much more managable to have each game state be its own class and to switch a pointer than it is having to deal with giant switch statements.
class TitleState : public GameState { public: //Static accessor static TitleState* get(); //Transitions bool enter() override; bool exit() override; //Main loop functions void handleEvent( SDL_Event& e ) override; void update() override; void render() override; private: //Static instance static TitleState sTitleState; //Private constructor TitleState(); //Intro background LTexture mBackgroundTexture; //Intro message LTexture mMessageTexture; }; class OverWorldState : public GameState { public: //Static accessor static OverWorldState* get(); //Transitions bool enter() override; bool exit() override; //Main loop functions void handleEvent( SDL_Event& e ) override; void update() override; void render() override; private: //Level dimensions static constexpr int kLevelWidth = kScreenWidth * 2; static constexpr int kLevelHeight = kScreenHeight * 2; //Static instance static OverWorldState sOverWorldState; //Private constructor OverWorldState(); //Overworld textures LTexture mBackgroundTexture; LTexture mRedHouseTexture; LTexture mBlueHouseTexture; //Game objects House mRedHouse; House mBlueHouse; }; class RedRoomState : public GameState { public: //Static accessor static RedRoomState* get(); //Transitions bool enter() override; bool exit() override; //Main loop functions void handleEvent( SDL_Event& e ) override; void update() override; void render() override; private: //Level dimensions static constexpr int kLevelWidth = kScreenWidth; static constexpr int kLevelHeight = kScreenHeight; //Static instance static RedRoomState sRedRoomState; //Private constructor RedRoomState(); //Room textures LTexture mBackgroundTexture; //Game objects Door mExitDoor; }; class BlueRoomState : public GameState { public: //Static accessor static BlueRoomState* get(); //Transitions bool enter() override; bool exit() override; //Main loop functions void handleEvent( SDL_Event& e ) override; void update() override; void render() override; private: //Level dimensions static constexpr int kLevelWidth = kScreenWidth; static constexpr int kLevelHeight = kScreenHeight; //Static instance static BlueRoomState sBlueRoomState; //Private constructor BlueRoomState(); //Room textures LTexture mBackgroundTexture; //Game objects Door mExitDoor; };
The title state looks a lot like the intro state because it's almost the same only it renders different text and background and it transitions to the overworld state.
The overworld state has more going on. It's a larger scrollable world with two house objects. When you touch the red house you go to the red room state and the blue house goes to the blue room state. The red room and blue room state are single screen rooms with a door that take you back to the overworld state. The difference between the two room states is that the background is different and the door is in a different place.
The overworld state has more going on. It's a larger scrollable world with two house objects. When you touch the red house you go to the red room state and the blue house goes to the blue room state. The red room and blue room state are single screen rooms with a door that take you back to the overworld state. The difference between the two room states is that the background is different and the door is in a different place.
class ExitState : public GameState { public: //Static accessor static ExitState* get(); //Transitions bool enter() override; bool exit() override; //Main loop functions void handleEvent( SDL_Event& e ) override; void update() override; void render() override; private: //Static instance static ExitState sExitState; //Private constructor ExitState(); };
The exit state is a stub state that only exists to handle when the user wants to exit.
/* Function Prototypes */ //Starts up SDL and creates window bool init(); //Loads media bool loadMedia(); //Frees media and shuts down SDL void close(); //Check collision bool checkCollision( SDL_Rect a, SDL_Rect b ); //State managers void setNextState( GameState* nextState ); void changeState(); /* Global Variables */ //The window we'll be rendering to SDL_Window* gWindow{ nullptr }; //The renderer used to draw to the window SDL_Renderer* gRenderer{ nullptr }; //Global assets LTexture gDotTexture; TTF_Font* gFont{ nullptr }; //Global game objects Dot gDot; //Game state object GameState* gCurrentState{ nullptr }; GameState* gNextState{ nullptr };
In terms of new functions, we have
It probably would have been better to have this be part of a state manager class, but for the sake of brevity we'll just have global functions/variables.
setNextState
which sets the state we want to transition to and changeState
where we actually transition states. For new global variables we have gCurrentState
which keeps track of the current running state
and gNextState
which points to the state we want to transition to.It probably would have been better to have this be part of a state manager class, but for the sake of brevity we'll just have global functions/variables.
//Dot Implementation Dot::Dot(): mCollisionBox{ 0, 0, kDotWidth, kDotHeight }, mVelX{ 0 }, mVelY{ 0 } { } void Dot::setPos( int x, int y ) { //Set position mCollisionBox.x = x; mCollisionBox.y = y; } void Dot::handleEvent( SDL_Event& e ) { //If a key was pressed if( e.type == SDL_EVENT_KEY_DOWN && e.key.repeat == 0 ) { //Adjust the velocity switch( e.key.key ) { case SDLK_UP: mVelY -= kDotVel; break; case SDLK_DOWN: mVelY += kDotVel; break; case SDLK_LEFT: mVelX -= kDotVel; break; case SDLK_RIGHT: mVelX += kDotVel; break; } } //If a key was released else if( e.type == SDL_EVENT_KEY_UP && e.key.repeat == 0 ) { //Adjust the velocity switch( e.key.key ) { case SDLK_UP: mVelY += kDotVel; break; case SDLK_DOWN: mVelY -= kDotVel; break; case SDLK_LEFT: mVelX += kDotVel; break; case SDLK_RIGHT: mVelX -= kDotVel; break; } } } void Dot::move( int levelWidth, int levelHeight ) { //Move the dot left or right mCollisionBox.x += mVelX; //If the dot went too far to the left or right if( ( mCollisionBox.x < 0 ) || ( mCollisionBox.x + kDotWidth > levelWidth ) ) { //Move back mCollisionBox.x -= mVelX; } //Move the dot up or down mCollisionBox.y += mVelY; //If the dot went too far up or down if( ( mCollisionBox.y < 0 ) || ( mCollisionBox.y + kDotHeight > levelHeight ) ) { //Move back mCollisionBox.y -= mVelY; } } void Dot::render( SDL_Rect camera ) { //Show the dot gDotTexture.render( static_cast( mCollisionBox.x ) - camera.x, static_cast ( mCollisionBox.y ) - camera.y ); } SDL_Rect Dot::getCollider() { return mCollisionBox; } //House Implementation House::House(): mCollisionBox{ 0, 0, kHouseWidth, kHouseHeight }, mHouseTexture{ nullptr } { } void House::set( int x, int y, LTexture* houseTexture ) { //Initialize position mCollisionBox.x = x; mCollisionBox.y = y; //Initialize texture mHouseTexture = houseTexture; } void House::render( SDL_Rect camera ) { //Show the house relative to the camera mHouseTexture->render( static_cast ( mCollisionBox.x ) - camera.x, static_cast ( mCollisionBox.y ) - camera.y ); } SDL_Rect House::getCollider() { return mCollisionBox; } //Door Implementation Door::Door(): mCollisionBox{ 0, 0, kDoorWidth, kDoorHeight } { } void Door::set( int x, int y ) { //Initialize position mCollisionBox.x = x; mCollisionBox.y = y; } void Door::render() { //Draw rectangle for door SDL_SetRenderDrawColor( gRenderer, 0x00, 0x00, 0x00, 0xFF ); SDL_FRect renderRect{ static_cast ( mCollisionBox.x ), static_cast ( mCollisionBox.y ), static_cast ( mCollisionBox.w ), static_cast ( mCollisionBox.h ) }; SDL_RenderFillRect( gRenderer, &renderRect ); } SDL_Rect Door::getCollider() { return mCollisionBox; }
As you can see, the dot class functions mostly the same as from the previous tutorials, only now with the ability to set the position and handling variable level widths. The house and the door classes basically function like dots that don't move. The only real difference
is that the house has a texture it renders and the door just renders itself as a black rectangle.
//IntoState Implementation IntroState* IntroState::get() { //Get static instance return &sIntroState; } bool IntroState::enter() { //Loading success flag bool success = true; //Load background if( success &= mBackgroundTexture.loadFromFile( "19-state-machines/intro-bg.png" ); !success ) { SDL_Log( "Failed to intro background!\n" ); success = false; } //Load text SDL_Color textColor{ 0x00, 0x00, 0x00, 0xFF }; if( success &= mMessageTexture.loadFromRenderedText( "Lazy Foo' Productions Presents...", textColor ); !success ) { SDL_Log( "Failed to render intro text!\n" ); success = false; } return success; } bool IntroState::exit() { //Free background and text mBackgroundTexture.destroy(); mMessageTexture.destroy(); return true; } void IntroState::handleEvent( SDL_Event& e ) { //If the user pressed enter if( ( e.type == SDL_EVENT_KEY_DOWN ) && ( e.key.key == SDLK_RETURN ) ) { //Move onto title state setNextState( TitleState::get() ); } } void IntroState::update() { } void IntroState::render() { //Show the background mBackgroundTexture.render( 0, 0 ); //Show the message mMessageTexture.render( ( kScreenWidth - mMessageTexture.getWidth() ) / 2.f, ( kScreenHeight - mMessageTexture.getHeight() ) / 2.f ); }
As mentioned before the intro state doesn't do much, just renders some text and background and transitions to the title state when the user presses enter.
First we have
First we have
get
function which just gets a pointer to the static class instance. The enter
function loads state assets and exit
frees them. The event handler sets the transition state to the title screen state with setNextState
when they hit enter. The update does nothing because everything is a static image and rendering draws the textures to the screen.
//Declare static instance IntroState IntroState::sIntroState; IntroState::IntroState() { //No public instantiation }
I wanted to call attention to these to bit of the intro state implementation. When you have a static class variable, you have to instantiate it somewhere or you'll get a linker error. Also, notice how the constructor doesn't do anything. It's generally good practice to make sure the
constructor does next to nothing when you have a global or static because global/static objects are initialized in a hard to determine order. At the most they should initialize variables but you shouldn't even assume you entered the
main
function before the constructor
is called.
//TitleState Implementation TitleState* TitleState::get() { //Get static instance return &sTitleState; } bool TitleState::enter() { //Loading success flag bool success = true; //Load background if( success &= mBackgroundTexture.loadFromFile( "19-state-machines/title-bg.png" ); !success ) { printf( "Failed to title background!\n" ); success = false; } //Load text SDL_Color textColor{ 0x00, 0x00, 0x00, 0xFF }; if( success &= mMessageTexture.loadFromRenderedText( "A State Machine Demo", textColor ); !success ) { printf( "Failed to render title text!\n" ); success = false; } return success; } bool TitleState::exit() { //Free background and text mBackgroundTexture.destroy(); mMessageTexture.destroy(); return true; } void TitleState::handleEvent( SDL_Event& e ) { //If the user pressed enter if( ( e.type == SDL_EVENT_KEY_DOWN ) && ( e.key.key == SDLK_RETURN ) ) { //Move to overworld setNextState( OverWorldState::get() ); } } void TitleState::update() { } void TitleState::render() { //Show the background mBackgroundTexture.render( 0, 0 ); //Show the message mMessageTexture.render( ( kScreenWidth - mMessageTexture.getWidth() ) / 2.f, ( kScreenHeight - mMessageTexture.getHeight() ) / 2.f ); } //Declare static instance TitleState TitleState::sTitleState; TitleState::TitleState() { //No public instantiation }
As you can see, the title state is almost the same as the intro state, just loads different textures and it transitions to the overworld state.
//OverWorldState Implementation OverWorldState* OverWorldState::get() { //Get static instance return &sOverWorldState; } bool OverWorldState::enter() { //Loading success flag bool success = true; //Load background if( success &= mBackgroundTexture.loadFromFile( "19-state-machines/green-overworld.png" ); !success ) { SDL_Log( "Failed to load overworld background!\n" ); success = false; } //Load house texture if( success &= mBlueHouseTexture.loadFromFile( "19-state-machines/blue-house.png" ); !success ) { SDL_Log( "Failed to load blue house texture!\n" ); success = false; } //Load house texture if( success &= mRedHouseTexture.loadFromFile( "19-state-machines/red-house.png" ); !success ) { SDL_Log( "Failed to load red house texture!\n" ); success = false; } //Position houses with graphics mRedHouse.set( 0, 0, &mRedHouseTexture ); mBlueHouse.set( kLevelWidth - House::kHouseWidth, kLevelHeight - House::kHouseHeight, &mBlueHouseTexture ); //Came from red room state if( gCurrentState == RedRoomState::get() ) { //Position below red house gDot.setPos( mRedHouse.getCollider().x + ( House::kHouseWidth - Dot::kDotWidth ) / 2, mRedHouse.getCollider().y + mRedHouse.getCollider().h + Dot::kDotHeight ); } //Came from blue room state else if( gCurrentState == BlueRoomState::get() ) { //Position above blue house gDot.setPos( mBlueHouse.getCollider().x + ( House::kHouseWidth - Dot::kDotWidth ) / 2, mBlueHouse.getCollider().y - Dot::kDotHeight * 2 ); } //Came from other state else { //Position middle of overworld gDot.setPos( ( kLevelWidth - Dot::kDotWidth ) / 2, ( kLevelWidth - Dot::kDotHeight ) / 2 ); } return success; } bool OverWorldState::exit() { //Free textures mBackgroundTexture.destroy(); mRedHouseTexture.destroy(); mBlueHouseTexture.destroy(); return true; }
Now the over world state has more going on. When entering the state we load the state assets and then place the game objects. When we come from the red room state we put the dot next to the red house, when we come from the blue room state we put the dot next to the blue house,
and when we come from any other state (like the title screen), we put the dot in the center of the level.
A quirk of the implementation if that the currentState doesn't change until after the transition is complete, so
A quirk of the implementation if that the currentState doesn't change until after the transition is complete, so
gCurrentState
actually tells us what state we're transitioning from.
void OverWorldState::handleEvent( SDL_Event& e ) { //Handle dot input gDot.handleEvent( e ); } void OverWorldState::update() { //Move dot gDot.move( kLevelWidth, kLevelHeight ); //On red house collision if( checkCollision( gDot.getCollider(), mRedHouse.getCollider() ) ) { //Got to red room setNextState( RedRoomState::get() ); } //On blue house collision else if( checkCollision( gDot.getCollider(), mBlueHouse.getCollider() ) ) { //Go to blue room setNextState( BlueRoomState::get() ); } }
In our event handler we handle the dot's events. In the update we move the dot and if we collide with one of the houses, we transition to the state associated with the house.
void OverWorldState::render() { //Center the camera over the dot SDL_Rect camera{ ( gDot.getCollider().x + Dot::kDotWidth / 2 ) - kScreenWidth / 2, ( gDot.getCollider().y + Dot::kDotHeight / 2 ) - kScreenHeight / 2, kScreenWidth, kScreenHeight }; //Keep the camera in bounds if( camera.x < 0 ) { camera.x = 0; } if( camera.y < 0 ) { camera.y = 0; } if( camera.x > kLevelWidth - camera.w ) { camera.x = kLevelWidth - camera.w; } if( camera.y > kLevelHeight - camera.h ) { camera.y = kLevelHeight - camera.h; } //Render background SDL_FRect bgClip{ static_cast( camera.x ), static_cast ( camera.y ), static_cast ( camera.w ), static_cast ( camera.h ) }; mBackgroundTexture.render( 0, 0, &bgClip ); //Render objects mRedHouse.render( camera ); mBlueHouse.render( camera ); gDot.render( camera ); } //Declare static instance OverWorldState OverWorldState::sOverWorldState; OverWorldState::OverWorldState() { //No public instantiation }
For our rendering, we center the camera over the dot, bound the camera, then render the background and the game objects.
//RedRoomState Implementation RedRoomState* RedRoomState::get() { //Get static instance return &sRedRoomState; } bool RedRoomState::enter() { //Loading success flag bool success = true; //Load background if( success &= mBackgroundTexture.loadFromFile( "19-state-machines/red-room.png" ); !success ) { SDL_Log( "Failed to load blue room background!\n" ); success = false; } //Place game objects mExitDoor.set( ( kLevelWidth - Door::kDoorWidth ) / 2, kLevelHeight - Door::kDoorHeight ); gDot.setPos( ( kLevelWidth - Dot::kDotWidth ) / 2, kLevelHeight - Door::kDoorHeight - Dot::kDotHeight * 2 ); return success; } bool RedRoomState::exit() { //Free background mBackgroundTexture.destroy(); return true; } void RedRoomState::handleEvent( SDL_Event& e ) { //Handle dot input gDot.handleEvent( e ); } void RedRoomState::update() { //Move dot gDot.move( kLevelWidth, kLevelHeight ); //On exit collision if( checkCollision( gDot.getCollider(), mExitDoor.getCollider() ) ) { //Go back to overworld setNextState( OverWorldState::get() ); } } void RedRoomState::render() { //Center the camera over the dot SDL_Rect camera{ 0, 0, kScreenWidth, kScreenHeight }; //Render background mBackgroundTexture.render( 0, 0 ); //Render objects mExitDoor.render(); gDot.render( camera ); } //Declare static instance RedRoomState RedRoomState::sRedRoomState; RedRoomState::RedRoomState() { //No public instantiation } //BlueRoomState Implementation BlueRoomState* BlueRoomState::get() { //Get static instance return &sBlueRoomState; } bool BlueRoomState::enter() { //Loading success flag bool success = true; //Load background if( success &= mBackgroundTexture.loadFromFile( "19-state-machines/blue-room.png" ); !success ) { printf( "Failed to load blue room background!\n" ); success = false; } //Position game objects mExitDoor.set( ( kLevelWidth - Door::kDoorWidth ) / 2, 0 ); gDot.setPos( ( kLevelWidth - Dot::kDotWidth ) / 2, Door::kDoorHeight + Dot::kDotHeight * 2 ); return success; } bool BlueRoomState::exit() { //Free background mBackgroundTexture.destroy(); return true; } void BlueRoomState::handleEvent( SDL_Event& e ) { //Handle dot input gDot.handleEvent( e ); } void BlueRoomState::update() { //Move dot gDot.move( kLevelWidth, kLevelHeight ); //On exit collision if( checkCollision( gDot.getCollider(), mExitDoor.getCollider() ) ) { //Back to overworld setNextState( OverWorldState::get() ); } } void BlueRoomState::render() { //Center the camera over the dot SDL_Rect camera{ 0, 0, kScreenWidth, kScreenHeight }; //Render background mBackgroundTexture.render( 0, 0 ); //Render objects mExitDoor.render(); gDot.render( camera ); } //Declare static instance BlueRoomState BlueRoomState::sBlueRoomState; BlueRoomState::BlueRoomState() { //No public instantiation }
The red room and blue room state function very similarly. They both load the background and place the dot next to the door on entry. They both transition back to the over world when the dot collides with the door. The only real difference is which background they load and where
they place the door.
//Hollow exit state ExitState* ExitState::get() { return &sExitState; } bool ExitState::enter() { return true; } bool ExitState::exit() { return true; } void ExitState::handleEvent( SDL_Event& e ) { } void ExitState::update() { } void ExitState::render() { } ExitState ExitState::sExitState; ExitState::ExitState() { }
As mentioned before, the exit state is just a stub state. In a real application, this would probably do some sort of clean up before exit.
bool loadMedia() { //File loading flag bool success{ true }; //Load glocal assets if( success &= gDotTexture.loadFromFile( "19-state-machines/dot.png" ); !success ) { SDL_Log( "Unable to dot image!\n"); } //Load scene font std::string fontPath = "19-state-machines/lazy.ttf"; if( gFont = TTF_OpenFont( fontPath.c_str(), 28 ); gFont == nullptr ) { SDL_Log( "Could not load %s! SDL_ttf Error: %s\n", fontPath.c_str(), SDL_GetError() ); success = false; } return success; } void close() { //Clean up textures gDotTexture.destroy(); //Free font TTF_CloseFont( gFont ); gFont = nullptr; //Destroy window SDL_DestroyRenderer( gRenderer ); gRenderer = nullptr; SDL_DestroyWindow( gWindow ); gWindow = nullptr; //Quit SDL subsystems TTF_Quit(); IMG_Quit(); SDL_Quit(); }
Just wanted to point out that we do have some global assets used across different states.
void setNextState( GameState* newState ) { //If the user doesn't want to exit if( gNextState != ExitState::get() ) { //Set the next state gNextState = newState; } } void changeState() { //If the state needs to be changed if( gNextState != nullptr ) { gCurrentState->exit(); gNextState->enter(); //Change the current state ID gCurrentState = gNextState; gNextState = nullptr; } }
As you can see,
When changing states we check of there is state to change to. We exit the old state, enter the new state, then update the global state pointers.
setNextState
just sets the pointer to the state we want to change to. We set priority to the quit state for the edge case that the user quits the application but then in the same frame triggers another state change.When changing states we check of there is state to change to. We exit the old state, enter the new state, then update the global state pointers.
//The event data SDL_Event e; SDL_zero( e ); //Timer to cap frame rate LTimer capTimer; //Set the current game state object and start state machine gCurrentState = IntroState::get(); gCurrentState->enter();
Before entering the main loop, we set the first state as the intro state and enter it.
//The main loop while( gCurrentState != ExitState::get() ) { //Start frame time capTimer.start(); //Get event data while( SDL_PollEvent( &e ) ) { //Handle state events gCurrentState->handleEvent( e ); //Exit on quit if( e.type == SDL_EVENT_QUIT ) { setNextState( ExitState::get() ); } } //Do state logic gCurrentState->update(); //Change state if needed changeState(); //Fill the background SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF ); SDL_RenderClear( gRenderer ); //Do state rendering gCurrentState->render(); //Update screen SDL_RenderPresent( gRenderer ); //Cap frame rate constexpr Uint64 nsPerFrame = 1000000000 / kScreenFps; Uint64 frameNs = capTimer.getTicksNS(); if( frameNs < nsPerFrame ) { SDL_DelayNS( nsPerFrame - frameNs ); } }
As you can see we plug in our game state calls in the corresponding parts of the main loop. We do have a special case for the exit state both for keeping the main loop going and for handling the quit event.
Notice how the
If you have completed every tutorial up to this point, you are ready to take on your Nasty Tetris Project. There are more tutorials to come but at this point you have completed what I consider to be the core tutorials. There's plenty to learn in the upcoming tutorials but there is no better teacher than getting your hands dirty. If you haven't dove into your Nasty Tetris Project (or even a Nasty Tic Tac Toe Project if you need a warm up), I would say go for it.
Notice how the
changeState
game state happens outside of the update. The reason the setting of the state transition is separate from the actual transition is because you don't want to transition from a game state while in the middle of game state code.If you have completed every tutorial up to this point, you are ready to take on your Nasty Tetris Project. There are more tutorials to come but at this point you have completed what I consider to be the core tutorials. There's plenty to learn in the upcoming tutorials but there is no better teacher than getting your hands dirty. If you haven't dove into your Nasty Tetris Project (or even a Nasty Tic Tac Toe Project if you need a warm up), I would say go for it.
Addendum: Physics world
In this demo, we at most have 3 objects interacting in a world. In real applications you are probably are going to have much more. In real application, you'll probably want to have some sort of physics world or physics manager class that you add physic objects to. That physics
world would then handle motion and collision so you are not having to put code to update object motion in every game state. Your physics world would then update physic objects after each game state object
For a Nasty Tetris Project, you're only going to have a single piece the user is moving against of a bunch of still block so you could probably get away with not having a physics manager. Anything more complicated like a 2D platformer, you probably will.
update
call.For a Nasty Tetris Project, you're only going to have a single piece the user is moving against of a bunch of still block so you could probably get away with not having a physics manager. Anything more complicated like a 2D platformer, you probably will.