Skip to main content

Game State Management

This document explains how the R-Type client manages different application states using a finite state machine.


🎯 State Machine Overview​

The client uses an enum class GameState to represent different screens/modes:

enum class GameState {
Menu, // Main menu
Lobby, // Lobby menu (create/join)
LobbyConfig, // Configure game rules
LobbyWaiting, // Waiting room
JoinLobbyDialog, // Join lobby dialog
Settings, // Settings menu
Playing, // Active gameplay
ReplayBrowser, // Replay selection
WatchingReplay, // Watching a replay
GameOver // Game over screen
};

πŸ”„ State Transition Diagram​

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Menu β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ β”‚ β”‚
β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”
β”‚ Lobby β”‚ β”‚Settings β”‚ β”‚ Replay β”‚ β”‚ Exit β”‚
β”‚ Menu β”‚ β”‚ β”‚ β”‚ Browser β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚
β”Œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”
β”‚ β”‚ β”‚ β”‚ β”‚Watching β”‚
β”‚ β”‚ β”‚ └──────────>β”‚ Replay β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚ β”‚
β”Œβ”€β”€β–Όβ”€β”€β”€β”€β–Όβ”β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β” β”‚
β”‚Lobby β”‚β”‚Join β”‚ β”‚
β”‚Config β”‚β”‚Dialog β”‚ β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”‚
└───>β”‚ Lobby β”‚ β”‚
β”‚ Waiting β”‚ β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”‚
β”‚ Playing β”‚ β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”‚
β”‚ Game β”‚ β”‚
β”‚ Over β”‚ β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”
β”‚ Menu β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“‹ State Details​

1. Menu State​

Purpose: Main menu entry point

Available Actions:

  • Play β†’ Navigate to Lobby
  • Settings β†’ Open Settings
  • Replays β†’ Open Replay Browser
  • Exit β†’ Close application

Transition Conditions:

case GameState::Menu: {
MenuAction action = menu->update(deltaTime);
switch (action) {
case MenuAction::Play:
state = GameState::Lobby;
break;
case MenuAction::Settings:
state = GameState::Settings;
break;
case MenuAction::Replays:
state = GameState::ReplayBrowser;
break;
case MenuAction::Exit:
window->close();
break;
}
break;
}

2. Lobby State​

Purpose: Choose between creating or joining a lobby

Available Actions:

  • Create Lobby β†’ Go to Lobby Config
  • Join Lobby β†’ Open Join Dialog
  • Back β†’ Return to Menu

Transition Logic:

case GameState::Lobby: {
if (!lobbyMenu) {
lobbyMenu = std::make_unique<LobbyMenu>(...);
}

LobbyMenuAction action = lobbyMenu->update(deltaTime);
switch (action) {
case LobbyMenuAction::CreateLobby:
state = GameState::LobbyConfig;
break;
case LobbyMenuAction::JoinLobby:
state = GameState::JoinLobbyDialog;
break;
case LobbyMenuAction::Back:
state = GameState::Menu;
lobbyMenu.reset();
break;
}
break;
}

3. LobbyConfig State​

Purpose: Configure game rules before creating lobby

Available Actions:

  • Create β†’ Create lobby and go to Waiting Room
  • Back β†’ Return to Lobby Menu

Transition Logic:

case GameState::LobbyConfig: {
if (!lobbyConfigMenu) {
lobbyConfigMenu = std::make_unique<LobbyConfigMenu>(...);
}

LobbyConfigAction action = lobbyConfigMenu->update(deltaTime);
switch (action) {
case LobbyConfigAction::CreateLobby: {
// 1. Get configured rules
GameRules rules = lobbyConfigMenu->getGameRules();

// 2. Connect to server
if (networkClient->connect(serverIp, serverPort)) {
// 3. Send login
networkClient->sendLogin(username);

// 4. Send rules (create lobby)
networkClient->sendRulesUpdate(rules, username, "");

// 5. Create waiting room
lobbyWaitingRoom = std::make_unique<LobbyWaitingRoom>(...);
lobbyWaitingRoom->setIsLeader(true);

state = GameState::LobbyWaiting;
}
break;
}
case LobbyConfigAction::Back:
state = GameState::Lobby;
lobbyConfigMenu.reset();
break;
}
break;
}

State Initialization:

  • Creates lobby with configured rules
  • Connects to server
  • Sends login and rules packets
  • Player becomes lobby leader

4. JoinLobbyDialog State​

Purpose: Enter lobby ID to join existing lobby

Available Actions:

  • Join β†’ Attempt to join lobby
  • Cancel β†’ Return to Lobby Menu

Transition Logic:

case GameState::JoinLobbyDialog: {
if (!joinLobbyDialog) {
joinLobbyDialog = std::make_unique<JoinLobbyDialog>(...);
}

JoinDialogAction action = joinLobbyDialog->update(deltaTime);
switch (action) {
case JoinDialogAction::Join: {
std::string lobbyId = joinLobbyDialog->getLobbyId();

// Connect and send join request
if (networkClient->connect(serverIp, serverPort)) {
networkClient->sendLogin(username);
networkClient->sendLobbyUpdate(0, username, lobbyId); // 0 = Join

waitingForJoinResponse = true;
pendingJoinLobbyId = lobbyId;
}
break;
}
case JoinDialogAction::Cancel:
state = GameState::Lobby;
joinLobbyDialog.reset();
break;
}

// Check for join response
if (waitingForJoinResponse && joinLobbyStatus != -1) {
if (joinLobbyStatus == 1) { // Success
lobbyWaitingRoom = std::make_unique<LobbyWaitingRoom>(...);
lobbyWaitingRoom->setIsLeader(false);
state = GameState::LobbyWaiting;
} else {
// Handle errors (full, not found, etc.)
joinLobbyDialog->setErrorMessage(getErrorMessage(joinLobbyStatus));
}
waitingForJoinResponse = false;
joinLobbyStatus = -1;
}
break;
}

Asynchronous Flow:

  1. User enters lobby ID
  2. Client sends join request to server
  3. Client waits for response (async)
  4. On success β†’ Transition to LobbyWaiting
  5. On failure β†’ Show error message

5. LobbyWaiting State​

Purpose: Wait for all players to join and become ready

Available Actions:

  • Ready/Not Ready β†’ Toggle ready status
  • Start Game β†’ (Leader only, when all ready)
  • Back β†’ Leave lobby

Transition Logic:

case GameState::LobbyWaiting: {
// Process network updates
networkClient->update();

WaitingRoomAction action = lobbyWaitingRoom->update(deltaTime);
switch (action) {
case WaitingRoomAction::ToggleReady:
// Toggle ready and notify server
networkClient->sendLobbyUpdate(2, username, lobbyId); // 2 = Toggle Ready
break;

case WaitingRoomAction::StartGame:
// Leader starts game
if (lobbyWaitingRoom->areAllPlayersReady()) {
networkClient->sendStartGame();
}
break;

case WaitingRoomAction::Back:
// Leave lobby
networkClient->sendLobbyUpdate(1, username, lobbyId); // 1 = Leave
networkClient->disconnect();
state = GameState::Lobby;
lobbyWaitingRoom.reset();
break;
}

// Check for game start event from server
if (gameStartRequested) {
// Create game state
auto gameState = std::make_unique<ClientGameState>(...);

state = GameState::Playing;
gameStartRequested = false;
}
break;
}

Network Synchronization:

  • Server broadcasts lobby state updates
  • Client receives player join/leave/ready events
  • Leader receives permission to start game when all ready

6. Playing State​

Purpose: Active gameplay

Transition Logic:

case GameState::Playing: {
// Process network updates
networkClient->update();

// Update game state
gameState->update(deltaTime);

// Send player input to server
uint16_t inputMask = 0;
if (input->isKeyPressed(Key::Z)) inputMask |= (1 << 0); // Up
if (input->isKeyPressed(Key::S)) inputMask |= (1 << 1); // Down
// ... other inputs
networkClient->sendInput(inputMask);

// Check for game over
if (gameState->isGameOver()) {
gameOverScreen->setScore(gameState->getScore());
gameOverScreen->setWinner(gameState->isWinner());
state = GameState::GameOver;
}

// Check for ESC key (return to menu)
if (input->isKeyPressed(Key::Escape)) {
networkClient->disconnect();
state = GameState::Menu;
gameState.reset();
}
break;
}

Gameplay Loop:

  1. Process network updates from server
  2. Update game state (entities, physics)
  3. Send player input to server
  4. Render game world
  5. Check for game over condition

7. GameOver State​

Purpose: Display final score and results

Available Actions:

  • Play Again β†’ Return to Lobby
  • Main Menu β†’ Return to Menu

Transition Logic:

case GameState::GameOver: {
GameOverAction action = gameOverScreen->update(deltaTime);
switch (action) {
case GameOverAction::PlayAgain:
state = GameState::Lobby;
gameOverScreen->reset();
break;

case GameOverAction::MainMenu:
state = GameState::Menu;
gameOverScreen->reset();
break;
}
break;
}

8. Settings State​

Purpose: Configure application settings

Available Actions:

  • Back β†’ Return to Menu (saves settings)

Transition Logic:

case GameState::Settings: {
SettingsMenuAction action = settingsMenu->update(deltaTime);
if (action == SettingsMenuAction::Back) {
// Save settings to disk
config.save();

// Apply settings
window->recreate(width, height, "R-Type", isFullscreen);
SoundManager::getInstance().setVolume(sfxVolume);

state = GameState::Menu;
}
break;
}

Settings Persistence:

  • Settings are loaded on startup
  • Changes are saved when exiting settings menu
  • Some settings apply immediately (volume)
  • Others require restart (resolution, fullscreen)

9. ReplayBrowser State​

Purpose: Select a replay to watch

Available Actions:

  • Watch Replay β†’ Start replay playback
  • Back β†’ Return to Menu

Transition Logic:

case GameState::ReplayBrowser: {
ReplayBrowserAction action = replayBrowser->update(deltaTime);
switch (action) {
case ReplayBrowserAction::WatchReplay: {
selectedReplayPath = replayBrowser->getSelectedReplayPath();

// Create replay viewer
replayViewer = std::make_unique<ReplayViewer>(selectedReplayPath);
if (replayViewer->load()) {
state = GameState::WatchingReplay;
}
break;
}
case ReplayBrowserAction::Back:
state = GameState::Menu;
break;
}
break;
}

10. WatchingReplay State​

Purpose: Watch a recorded game session

Available Actions:

  • Pause/Play β†’ Toggle playback
  • Rewind/Forward β†’ Seek in replay
  • Speed β†’ Change playback speed
  • Exit β†’ Return to Replay Browser

Transition Logic:

case GameState::WatchingReplay: {
bool isPaused = replayViewer->isPaused();
float speed = replayViewer->getSpeed();

// Update replay
if (!isPaused) {
replayViewer->update(deltaTime * speed);
}

// Handle controls
if (input->isKeyPressed(Key::Space)) {
replayViewer->togglePause();
}
if (input->isKeyPressed(Key::Left)) {
replayViewer->seek(-10.0f); // Rewind 10 seconds
}
if (input->isKeyPressed(Key::Right)) {
replayViewer->seek(10.0f); // Forward 10 seconds
}
if (input->isKeyPressed(Key::S)) {
replayViewer->cycleSpeed(); // 0.5x -> 1x -> 2x
}

// Check for exit
if (input->isKeyPressed(Key::Escape) || replayViewer->isFinished()) {
state = GameState::ReplayBrowser;
replayViewer.reset();
}
break;
}

πŸ”§ State Lifecycle Management​

State Creation​

States are created lazily when first needed:

// Example: Lazy initialization of Settings Menu
case GameState::Settings: {
if (!settingsMenu) {
settingsMenu = std::make_unique<SettingsMenu>(...);
settingsMenu->loadSettings();
}

SettingsMenuAction action = settingsMenu->update(deltaTime);
// ...
}

State Cleanup​

States are cleaned up when no longer needed:

// Example: Cleanup when leaving lobby
case LobbyMenuAction::Back:
state = GameState::Menu;
lobbyMenu.reset(); // Destroy lobby menu
lobbyConfigMenu.reset(); // Destroy config menu
lobbyWaitingRoom.reset(); // Destroy waiting room
break;

State Reset​

Some states can be reset without destruction:

// Example: Reset menu state
menu->reset(); // Resets selection to first button

🌐 Network State Synchronization​

Some states depend on network events:

Lobby Join Flow​

1. User clicks "Join" in JoinLobbyDialog
↓
2. Client sends C2S_UPD_LOBBY (action=0, Join)
↓
3. Client enters "waiting for response" mode
↓
4. Server validates and responds with S2C_LOBBY_AVAIL
↓
5. Client receives callback with status
↓
6. If success (status=1):
- Create LobbyWaitingRoom
- Transition to LobbyWaiting state
↓
7. If failure (status≠1):
- Show error message
- Stay in JoinLobbyDialog state

Game Start Flow​

1. Leader clicks "Start Game" in LobbyWaitingRoom
↓
2. Client sends C2S_START_GAME
↓
3. Server validates (all players ready?)
↓
4. Server broadcasts S2C_GAME_EVENT (GAME_START)
↓
5. All clients receive event callback
↓
6. gameStartRequested flag set to true
↓
7. Next update cycle: Transition to Playing state

🎨 Rendering State-Specific UI​

Each state has its own rendering logic:

void renderCurrentState(GameState state) {
window->clear();

switch (state) {
case GameState::Menu:
menu->render();
break;

case GameState::LobbyWaiting:
lobbyWaitingRoom->render();
break;

case GameState::Playing:
gameState->render();
break;

// ... other states
}

window->display();
}

πŸ› Common State Transition Issues​

Issue 1: Memory Leaks​

Problem: Forgetting to reset unique_ptrs when changing states

Solution:

// ❌ Bad: Memory leak
state = GameState::Menu;

// βœ… Good: Explicit cleanup
lobbyWaitingRoom.reset();
networkClient->disconnect();
state = GameState::Menu;

Issue 2: State Desynchronization​

Problem: Client and server state don't match

Solution:

  • Always send state change notifications to server
  • Wait for server confirmation before transitioning
  • Use callbacks for asynchronous transitions

Issue 3: Race Conditions​

Problem: Network callback arrives during state transition

Solution:

  • Use flags (gameStartRequested) to defer transitions
  • Process callbacks at safe points in update loop
  • Validate state before processing callbacks

πŸ“Š State Transition Matrix​

From StateTo StateTrigger
MenuLobbyClick "PLAY"
MenuSettingsClick "SETTINGS"
MenuReplayBrowserClick "REPLAYS"
LobbyLobbyConfigClick "CREATE LOBBY"
LobbyJoinLobbyDialogClick "JOIN LOBBY"
LobbyMenuClick "BACK"
LobbyConfigLobbyWaitingClick "CREATE"
LobbyConfigLobbyClick "BACK"
JoinLobbyDialogLobbyWaitingJoin success
JoinLobbyDialogLobbyClick "CANCEL"
LobbyWaitingPlayingServer sends GAME_START
LobbyWaitingLobbyClick "BACK"
PlayingGameOverGame ends
PlayingMenuPress ESC
GameOverLobbyClick "PLAY AGAIN"
GameOverMenuClick "MAIN MENU"
SettingsMenuClick "BACK"
ReplayBrowserWatchingReplayClick "WATCH"
ReplayBrowserMenuClick "BACK"
WatchingReplayReplayBrowserPress ESC / Replay ends

πŸš€ Best Practices​

  1. Always cleanup resources when leaving a state
  2. Use lazy initialization for expensive state objects
  3. Validate state before processing network callbacks
  4. Use flags for deferred transitions in async scenarios
  5. Reset UI components when re-entering a state
  6. Log state transitions for debugging
  7. Handle ESC key consistently across states

πŸ“š Further Reading​