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:
- User enters lobby ID
- Client sends join request to server
- Client waits for response (async)
- On success β Transition to LobbyWaiting
- 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:
- Process network updates from server
- Update game state (entities, physics)
- Send player input to server
- Render game world
- 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 State | To State | Trigger |
|---|---|---|
| Menu | Lobby | Click "PLAY" |
| Menu | Settings | Click "SETTINGS" |
| Menu | ReplayBrowser | Click "REPLAYS" |
| Lobby | LobbyConfig | Click "CREATE LOBBY" |
| Lobby | JoinLobbyDialog | Click "JOIN LOBBY" |
| Lobby | Menu | Click "BACK" |
| LobbyConfig | LobbyWaiting | Click "CREATE" |
| LobbyConfig | Lobby | Click "BACK" |
| JoinLobbyDialog | LobbyWaiting | Join success |
| JoinLobbyDialog | Lobby | Click "CANCEL" |
| LobbyWaiting | Playing | Server sends GAME_START |
| LobbyWaiting | Lobby | Click "BACK" |
| Playing | GameOver | Game ends |
| Playing | Menu | Press ESC |
| GameOver | Lobby | Click "PLAY AGAIN" |
| GameOver | Menu | Click "MAIN MENU" |
| Settings | Menu | Click "BACK" |
| ReplayBrowser | WatchingReplay | Click "WATCH" |
| ReplayBrowser | Menu | Click "BACK" |
| WatchingReplay | ReplayBrowser | Press ESC / Replay ends |
π Best Practicesβ
- Always cleanup resources when leaving a state
- Use lazy initialization for expensive state objects
- Validate state before processing network callbacks
- Use flags for deferred transitions in async scenarios
- Reset UI components when re-entering a state
- Log state transitions for debugging
- Handle ESC key consistently across states
π Further Readingβ
- UI Systems - UI component details
- Network Architecture - Network callbacks
- Configuration System - Settings persistence