Lazy Foo' Productions

SDL Forums external link SDL Tutorials 🐰SDL3 Tutorials🥚 Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

State Machines

State Machines screenshot

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.

We're going to have a intro screen and title screen:
intro screen

title screen

Then we're going to have an overworld you can move around in:
overworld

From that overworld you can enter a red room or a blue room:
red room

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.
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.
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 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 : 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.
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 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 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 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, 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 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 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.