University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

State Machines

Overview

This lab builds a small console program that tracks which mode a game is currently in, the main menu, playing, paused, or game over, and moves between those modes as the player types choices. You will build the gameStateMachine function one state at a time using an enum class and a switch statement, then work through a bug where a later edit quietly deletes two lines a switch statement depends on, tracing the resulting misbehaviour back to its cause and fixing it.

Learning Objectives

By the end of this lab you will be able to:

Concept

A game usually needs to behave completely differently depending on what is currently happening, showing a menu is nothing like updating gameplay, and updating gameplay is nothing like showing a paused overlay. Tracking this with a handful of separate bool flags, isPaused, isPlaying, isGameOver, gets error-prone fast, since nothing stops two of them from being true at once, and every new mode means another flag to keep in sync everywhere.

A finite state machine solves this by giving the program exactly one variable that holds exactly one named state at a time. In C++ the natural way to name that fixed set of states is an enum class, enum class GameState { MENU, PLAYING, PAUSED, GAME_OVER };, which gives each state its own type-safe name instead of a raw number. A switch statement then reads that one variable and jumps straight to the block of code written for whichever state it currently holds, the case matching GameState::PLAYING only runs while the machine is actually in the playing state. Moving to a different state is nothing more than assigning a new value to that same variable, for example setting it to GameState::PAUSED partway through the PLAYING case.

Each case in a switch must end with a break statement. Without one, execution does not stop at the end of a case, it falls straight through into the next case label and keeps running, whether that was intended or not. Reading input with std::cin and comparing it against the choices offered at each state is what decides which state the machine assigns next.

Tasks

Task 1 - The GameState enum and the Menu state

Create a new Console App (C++) project in Visual Studio the same way as in the Environment Setup lab, naming it StateMachineDemo. Use Add > New Item to add a new C++ source file named GameStateMachine.cpp, and write the code below.

  1. Declare enum class GameState with four values, MENU, PLAYING, PAUSED, GAME_OVER.
  2. Complete gameStateMachine() so its while (running) loop's switch handles only GameState::MENU for now, calling showMenu(), reading an int into userInput, setting currentState to GameState::PLAYING and breaking if the player entered 1, or setting running to false and breaking for anything else.

Success condition: building and running the project prints the menu, and entering any number other than 1 ends the program immediately.

GameStateMachine.cpp
#include <iostream>

enum class GameState
{
    // TODO: declare four values, MENU, PLAYING, PAUSED, GAME_OVER
};

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "Anything else. Exit" << std::endl;
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            // TODO: call showMenu(), read userInput, and either move to
            // GameState::PLAYING on 1 or set running to false otherwise.
            // Both branches must end with break.
            break;
        }
    }
}

int main()
{
    gameStateMachine();
    return 0;
}

Task 2 - Adding the Playing state

Open GameStateMachine.cpp and add a playGame() function above gameStateMachine() that prints a message saying the game is playing. Then add a case GameState::PLAYING to the switch below the existing MENU case, calling playGame(), reading userInput, and setting running to false and breaking if the player enters 2. GameState::PAUSED is not wired up yet, so for now leave any other input to fall through to the bottom of the case with a plain break.

Success condition: entering 1 at the menu moves to the playing state and prints its message, and entering 2 there ends the program.

GameStateMachine.cpp
#include <iostream>

enum class GameState
{
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
};

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "Anything else. Exit" << std::endl;
}

void playGame()
{
    // TODO: print a message saying the game is playing
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            showMenu();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            running = false;
            break;

        case GameState::PLAYING:
            // TODO: call playGame(), read userInput, and print the
            // options (2 to end, anything else does nothing yet since
            // Paused is not wired up). Set running to false and break
            // if userInput is 2, otherwise just break.
            break;
        }
    }
}

int main()
{
    gameStateMachine();
    return 0;
}

Task 3 - Adding the Paused state

Open GameStateMachine.cpp and add a pauseGame() function that prints a message saying the game is paused. In the PLAYING case, change the branch for input 1 so it sets currentState to GameState::PAUSED and breaks, instead of doing nothing. Then add a case GameState::PAUSED below PLAYING, calling pauseGame(), reading userInput, returning to GameState::PLAYING on 1, and ending the program on 2, each branch ending with break.

Success condition: from the menu, entering 1 then 1 then 1 moves menu to playing to paused and back to playing again, and entering 2 while paused ends the program.

GameStateMachine.cpp
#include <iostream>

enum class GameState
{
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
};

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "Anything else. Exit" << std::endl;
}

void playGame()
{
    std::cout << "Playing..." << std::endl;
    std::cout << "1. Pause" << std::endl;
    std::cout << "2. End the game" << std::endl;
}

void pauseGame()
{
    // TODO: print a message saying the game is paused
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            showMenu();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            running = false;
            break;

        case GameState::PLAYING:
            playGame();
            std::cin >> userInput;
            if (userInput == 1)
            {
                // TODO: set currentState to GameState::PAUSED and break
            }
            if (userInput == 2)
            {
                running = false;
                break;
            }
            break;

        case GameState::PAUSED:
            // TODO: call pauseGame(), read userInput, and return to
            // GameState::PLAYING on 1 or set running to false on 2.
            // Each branch must end with break.
            break;
        }
    }
}

int main()
{
    gameStateMachine();
    return 0;
}

Task 4 - Adding the Game Over state

Open GameStateMachine.cpp and add a gameOver() function that prints a message saying the game has ended. Change the PLAYING case's branch for input 2 so it sets currentState to GameState::GAME_OVER and breaks, instead of ending the program directly. Do the same for the PAUSED case's branch for input 2. Then add a case GameState::GAME_OVER, calling gameOver(), reading userInput, returning to GameState::MENU on 1, and ending the program on 2, each branch ending with break.

Success condition: from the menu, entering 1 then 2 reaches the game over state and prints its message, entering 1 there returns all the way to the menu, and entering 2 there ends the program.

GameStateMachine.cpp
#include <iostream>

enum class GameState
{
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
};

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "Anything else. Exit" << std::endl;
}

void playGame()
{
    std::cout << "Playing..." << std::endl;
    std::cout << "1. Pause" << std::endl;
    std::cout << "2. End the game" << std::endl;
}

void pauseGame()
{
    std::cout << "Game paused." << std::endl;
    std::cout << "1. Continue" << std::endl;
    std::cout << "2. End the game" << std::endl;
}

void gameOver()
{
    // TODO: print a message saying the game has ended
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            showMenu();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            running = false;
            break;

        case GameState::PLAYING:
            playGame();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PAUSED;
                break;
            }
            if (userInput == 2)
            {
                // TODO: set currentState to GameState::GAME_OVER and break
            }
            break;

        case GameState::PAUSED:
            pauseGame();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            if (userInput == 2)
            {
                // TODO: set currentState to GameState::GAME_OVER and break
            }
            break;

        case GameState::GAME_OVER:
            // TODO: call gameOver(), read userInput, and return to
            // GameState::MENU on 1 or set running to false on 2.
            // Each branch must end with break.
            break;
        }
    }
}

int main()
{
    gameStateMachine();
    return 0;
}

Task 5 - A refactor removes two lines

The state machine now cycles cleanly between all four states. Open GameStateMachine.cpp and replace gameStateMachine() with the version below, which someone tidying up the file has just finished saving. It still builds without a single warning.

Do not run it yet, just read it carefully first, particularly the PLAYING and PAUSED cases, and compare them against the version you finished in Task 4.

GameStateMachine.cpp
#include <iostream>

enum class GameState
{
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
};

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "Anything else. Exit" << std::endl;
}

void playGame()
{
    std::cout << "Playing..." << std::endl;
    std::cout << "1. Pause" << std::endl;
    std::cout << "2. End the game" << std::endl;
}

void pauseGame()
{
    std::cout << "Game paused." << std::endl;
    std::cout << "1. Continue" << std::endl;
    std::cout << "2. End the game" << std::endl;
}

void gameOver()
{
    std::cout << "Game over." << std::endl;
    std::cout << "1. Return to the menu" << std::endl;
    std::cout << "2. Exit" << std::endl;
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            showMenu();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            running = false;
            break;

        case GameState::PLAYING:
            playGame();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PAUSED;
                break;
            }
            if (userInput == 2)
            {
                currentState = GameState::GAME_OVER;
                break;
            }

        case GameState::PAUSED:
            pauseGame();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            if (userInput == 2)
            {
                currentState = GameState::GAME_OVER;
                break;
            }

        case GameState::GAME_OVER:
            gameOver();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::MENU;
                break;
            }
            running = false;
            break;
        }
    }
}

int main()
{
    gameStateMachine();
    return 0;
}

Task 6 - Triggering the misbehaviour

Build and run the project with the version from Task 5. Nothing needs editing in this task, just try it and read the output carefully.

  1. At the menu, enter 1 to reach the playing state.
  2. At the playing prompt, instead of entering 1 or 2, enter a number that is neither, for example 9.
  3. Read every line the program prints immediately after that 9, before it waits for input again.

Success condition: you can point at the extra output that appears straight after entering 9, a full paused-state prompt, even though nothing in the code ever set currentState to GameState::PAUSED, and describe what the program does differently now compared with the version from Task 4.

Task 7 - Fixing the fallthrough

Open GameStateMachine.cpp. In the PLAYING case, after the two if blocks, add a plain break; so the case always ends there when neither 1 nor 2 was entered instead of continuing into whatever case label follows it. Make the same change to the PAUSED case.

Success condition: repeating the same steps as Task 6, entering 9 at the playing prompt now only reprints the playing prompt and asks again, with no paused-state output appearing uninvited, matching the behaviour of the version you built in Task 4.

GameStateMachine.cpp
#include <iostream>

enum class GameState
{
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
};

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "Anything else. Exit" << std::endl;
}

void playGame()
{
    std::cout << "Playing..." << std::endl;
    std::cout << "1. Pause" << std::endl;
    std::cout << "2. End the game" << std::endl;
}

void pauseGame()
{
    std::cout << "Game paused." << std::endl;
    std::cout << "1. Continue" << std::endl;
    std::cout << "2. End the game" << std::endl;
}

void gameOver()
{
    std::cout << "Game over." << std::endl;
    std::cout << "1. Return to the menu" << std::endl;
    std::cout << "2. Exit" << std::endl;
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            showMenu();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            running = false;
            break;

        case GameState::PLAYING:
            playGame();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PAUSED;
                break;
            }
            if (userInput == 2)
            {
                currentState = GameState::GAME_OVER;
                break;
            }
            // TODO: add the missing break here

        case GameState::PAUSED:
            pauseGame();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            if (userInput == 2)
            {
                currentState = GameState::GAME_OVER;
                break;
            }
            // TODO: add the missing break here

        case GameState::GAME_OVER:
            gameOver();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::MENU;
                break;
            }
            running = false;
            break;
        }
    }
}

int main()
{
    gameStateMachine();
    return 0;
}

Solution

Try the tasks above before revealing the solution.