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:
- Declare an enum class to give a fixed, named set of states a single type
- Use a switch statement to run the correct behaviour for whichever state a variable currently holds
- Transition between states in response to user input, including a state that ends the program
- Explain what a switch statement does when a case reaches its end without hitting break
- Diagnose a switch fallthrough bug from its symptom alone and correct it
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.
- Declare
enum class GameStatewith four values,MENU,PLAYING,PAUSED,GAME_OVER. - Complete
gameStateMachine()so itswhile (running)loop'sswitchhandles onlyGameState::MENUfor now, callingshowMenu(), reading anintintouserInput, settingcurrentStatetoGameState::PLAYINGand breaking if the player entered1, or settingrunningtofalseand 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.
#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.
#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.
#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.
#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.
#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.
- At the menu, enter
1to reach the playing state. - At the playing prompt, instead of entering
1or2, enter a number that is neither, for example9. - 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.
#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.