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:
- Add a new value to an existing enum class and see it flow into a switch statement
- Give a state its own data by declaring a local variable and passing it into the function that state calls
- Wire a transition into a new state from an existing state without disturbing that state's other transitions
- Give a new case its own transitions in and out, matching the pattern every other case already follows
- Recognise that adding any state touches the same four places no matter how many states already exist
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.
- Add a fifth value,
SETTINGS, toenum class GameState. - In
showMenu(), add a line printing2. Settingsbetween the existingPlayline and theExitline. - In the
MENUcase, add a branch that setscurrentStatetoGameState::SETTINGSand breaks whenuserInputis2, placed before the existing fallback that ends the program on anything else. - Add a
showSettings()function, abovegameStateMachine(), that printsSettingsfollowed by1. Back to menu. - Add a
case GameState::SETTINGSbelowGAME_OVERthat callsshowSettings(), readsuserInput, and setscurrentStatetoGameState::MENUand breaks whenuserInputis1. Any other input should justbreak, 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.
#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.
- In
gameStateMachine(), declareint difficulty = 2;alongsidecurrentState,runninganduserInput. - Change
showSettings()to take anint difficultyparameter, printDifficulty:followed by its value and(1=Easy, 2=Normal, 3=Hard), and add a line printing2. Change difficultybelow the existing1. Back to menuline. - In the
SETTINGScase, passdifficultyintoshowSettings(), and add a branch that runs whenuserInputis2, settingdifficultyto(difficulty % 3) + 1and breaking, so it cycles1 -> 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.
#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.
- In
gameStateMachine(), declareint volume = 50;alongsidedifficulty. - Change
showSettings()to also take anint volumeparameter, printVolume:followed by its value and a%sign, and add a line printing3. Change volumebelow the existing2. Change difficultyline. - In the
SETTINGScase, passvolumeintoshowSettings()alongsidedifficulty, and add a branch that runs whenuserInputis3, settingvolumeto(volume + 10) % 110and breaking, so it climbs in steps of10from0up to100and then wraps back to0.
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.
#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.