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

Generic FSM

Overview

This lab extends the StateMachineDemo project from the State Machines lab. That project's gameStateMachine function already moves cleanly between MENU, PLAYING, PAUSED and GAME_OVER. Here you will add a fifth state, SETTINGS, reachable from the menu, that lets the player cycle a difficulty level and a volume level before returning to the menu. Adding it will walk through the same four steps every new state needs, regardless of how many states already exist, which is what makes the recipe general enough to reuse on any future state.

Learning Objectives

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

Concept

A finite state machine built from an enum class and a switch statement does not stay a fixed size. Games grow settings screens, inventory screens and credits screens long after the core loop is working, and each of those is just another state bolted onto the same machine. The problem is that "bolting on a state" is easy to do inconsistently: forget to wire an entry point and the new state is unreachable, forget to give it its own exit and it traps the player, forget a break and it falls into whatever case happens to follow it.

The fix is to treat adding a state as a fixed, repeatable recipe rather than something worked out fresh each time. First, add the new name to the enum class so the compiler knows the value exists. Second, decide what data that state alone needs, a difficulty level has no meaning inside PLAYING, so it is declared as its own local variable rather than folded into an existing one. Third, write the function that runs while the machine is in that state, taking whatever data it needs as parameters. Fourth, add exactly two kinds of switch logic, one branch in whichever existing case should be able to reach the new state, setting currentState to the new value, and one new case for the state itself, with its own break-terminated branches for whatever it can transition to next.

Every state already in gameStateMachine, MENU, PLAYING, PAUSED, GAME_OVER, was built by following exactly these four steps. SETTINGS is no different.

Tasks

Task 1 - Reaching Settings and returning to the Menu

Open GameStateMachine.cpp.

  1. Add a fifth value, SETTINGS, to enum class GameState.
  2. In showMenu(), add a line printing 2. Settings between the existing Play line and the Exit line.
  3. In the MENU case, add a branch that sets currentState to GameState::SETTINGS and breaks when userInput is 2, placed before the existing fallback that ends the program on anything else.
  4. Add a showSettings() function, above gameStateMachine(), that prints Settings followed by 1. Back to menu.
  5. Add a case GameState::SETTINGS below GAME_OVER that calls showSettings(), reads userInput, and sets currentState to GameState::MENU and breaks when userInput is 1. Any other input should just break, so the loop shows the Settings screen again.

Success condition: entering 1 at the main menu still reaches Playing exactly as before, entering 2 at the main menu shows the Settings screen, and entering 1 there returns to the main menu.

GameStateMachine.cpp
#include <iostream>

enum class GameState
{
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
    // TODO: add a fifth value, SETTINGS
};

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    // TODO: print "2. Settings" here
    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;
}

// TODO: add a showSettings() function that prints "Settings" then
// "1. Back to menu"

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;
            }
            // TODO: add a branch here, if userInput == 2 set currentState
            // to GameState::SETTINGS and 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;
            }
            break;

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

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

        // TODO: add case GameState::SETTINGS here. Call showSettings(),
        // read userInput, and set currentState to GameState::MENU and
        // break when userInput is 1. Any other input should just break.
        }
    }
}

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

Task 2 - Changing the difficulty from Settings

Open GameStateMachine.cpp.

  1. In gameStateMachine(), declare int difficulty = 2; alongside currentState, running and userInput.
  2. Change showSettings() to take an int difficulty parameter, print Difficulty: followed by its value and (1=Easy, 2=Normal, 3=Hard), and add a line printing 2. Change difficulty below the existing 1. Back to menu line.
  3. In the SETTINGS case, pass difficulty into showSettings(), and add a branch that runs when userInput is 2, setting difficulty to (difficulty % 3) + 1 and breaking, so it cycles 1 -> 2 -> 3 -> 1.

Success condition: the Settings screen now shows the current difficulty, and entering 2 there repeatedly cycles the printed difficulty through 1, 2 and 3 without ever leaving the Settings screen.

GameStateMachine.cpp
#include <iostream>

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

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "2. Settings" << 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;
}

// TODO: add an int difficulty parameter to showSettings(), print
// "Difficulty: " followed by its value and "(1=Easy, 2=Normal, 3=Hard)",
// and print "2. Change difficulty" below the existing menu line
void showSettings()
{
    std::cout << "Settings" << std::endl;
    std::cout << "1. Back to menu" << std::endl;
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;
    // TODO: declare int difficulty = 2;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            showMenu();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            if (userInput == 2)
            {
                currentState = GameState::SETTINGS;
                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;
            }
            break;

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

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

        case GameState::SETTINGS:
            // TODO: pass difficulty into showSettings()
            showSettings();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::MENU;
                break;
            }
            // TODO: add a branch here, if userInput == 2 set difficulty
            // to (difficulty % 3) + 1 and break
            break;
        }
    }
}

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

Task 3 - Changing the volume from Settings

Open GameStateMachine.cpp.

  1. In gameStateMachine(), declare int volume = 50; alongside difficulty.
  2. Change showSettings() to also take an int volume parameter, print Volume: followed by its value and a % sign, and add a line printing 3. Change volume below the existing 2. Change difficulty line.
  3. In the SETTINGS case, pass volume into showSettings() alongside difficulty, and add a branch that runs when userInput is 3, setting volume to (volume + 10) % 110 and breaking, so it climbs in steps of 10 from 0 up to 100 and then wraps back to 0.

Success condition: the Settings screen now shows both the current difficulty and the current volume, entering 2 still cycles the difficulty, entering 3 repeatedly raises the printed volume in steps of 10 and wraps back to 0 after 100, and entering 1 from Settings still returns to a main menu where 1 reaches Playing as it always has.

GameStateMachine.cpp
#include <iostream>

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

void showMenu()
{
    std::cout << "Game Menu:" << std::endl;
    std::cout << "1. Play" << std::endl;
    std::cout << "2. Settings" << 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;
}

// TODO: add an int volume parameter to showSettings(), print
// "Volume: " followed by its value and "%", and print
// "3. Change volume" below the existing menu lines
void showSettings(int difficulty)
{
    std::cout << "Settings" << std::endl;
    std::cout << "Difficulty: " << difficulty << " (1=Easy, 2=Normal, 3=Hard)" << std::endl;
    std::cout << "1. Back to menu" << std::endl;
    std::cout << "2. Change difficulty" << std::endl;
}

void gameStateMachine()
{
    GameState currentState = GameState::MENU;
    bool running = true;
    int userInput = 0;
    int difficulty = 2;
    // TODO: declare int volume = 50;

    while (running)
    {
        switch (currentState)
        {
        case GameState::MENU:
            showMenu();
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::PLAYING;
                break;
            }
            if (userInput == 2)
            {
                currentState = GameState::SETTINGS;
                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;
            }
            break;

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

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

        case GameState::SETTINGS:
            // TODO: pass volume into showSettings() alongside difficulty
            showSettings(difficulty);
            std::cin >> userInput;
            if (userInput == 1)
            {
                currentState = GameState::MENU;
                break;
            }
            if (userInput == 2)
            {
                difficulty = (difficulty % 3) + 1;
                break;
            }
            // TODO: add a branch here, if userInput == 3 set volume to
            // (volume + 10) % 110 and break
            break;
        }
    }
}

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

Solution

Try the tasks above before revealing the solution.