UI Systems
This document covers all user interface systems in the R-Type client, including menus, dialogs, and UI components.
🎯 UI Architecture Overview
The R-Type UI follows a component-based architecture with reusable elements:
┌──────────────────────────────────────────┐
│ UI Screen (Menu, etc.) │
│ ┌────────────┐ ┌────────────┐ │
│ │ Button │ │ Slider │ │
│ └────────────┘ └────────────┘ │
│ ┌────────────┐ ┌────────────┐ │
│ │ InputField │ │ Toggle │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────────────┘
📦 Reusable UI Components
1. Button (Button.hpp/cpp)
Standard clickable button with hover states.
Constructor:
Button(float x, float y, float width, float height, const std::string& text);
Key Methods:
bool update(int mouseX, int mouseY, bool isMousePressed);
bool isHovered(int mouseX, int mouseY) const;
void setFocused(bool focused); // For keyboard navigation
const std::string& getText() const;
Usage Example:
Button playButton(400.0f, 200.0f, 300.0f, 60.0f, "PLAY");
// In update loop
if (playButton.update(mouseX, mouseY, isMousePressed)) {
// Button was clicked
startGame();
}
// Rendering
if (playButton.getIsHovered() || playButton.getIsFocused()) {
graphics.drawRectangle(x, y, width, height, 0, 200, 255, 255); // Blue
} else {
graphics.drawRectangle(x, y, width, height, 30, 30, 100, 255); // Gray
}
graphics.drawText(playButton.getText(), textX, textY, 24, 255, 255, 255, 255);
Features:
- ✅ Mouse hover detection
- ✅ Click handling
- ✅ Keyboard focus support
- ✅ Custom text labels
2. InputField (InputField.hpp/cpp)
Text input field for user data entry.
Constructor:
InputField(float x, float y, float width, float height,
const std::string& label,
const std::string& initialValue = "",
InputFieldType type = InputFieldType::Default);
Input Types:
enum class InputFieldType {
Default, // Alphanumeric + dots + colons
ServerIP, // Digits and dots only (IP address)
ServerPort, // Digits only (port number)
Filename // Alphanumeric + common filename characters
};
Key Methods:
bool update(int mouseX, int mouseY, bool isMousePressed);
void handleTextInput(char character);
void handleBackspace();
void handleEnter();
bool isActive() const;
const std::string& getValue() const;
void setValue(const std::string& value);
void setOnChangeCallback(std::function<void(const std::string&)> callback);
Usage Example:
InputField usernameField(300.0f, 200.0f, 400.0f, 50.0f, "Username:", "Player1");
// Activation
usernameField.update(mouseX, mouseY, isMousePressed);
// Text input handling
if (usernameField.isActive()) {
if (/* character input */) {
usernameField.handleTextInput(character);
}
if (/* backspace pressed */) {
usernameField.handleBackspace();
}
if (/* enter pressed */) {
usernameField.handleEnter();
}
}
// Get value
std::string username = usernameField.getValue();
Features:
- ✅ Input validation by type
- ✅ Character limit (20 chars)
- ✅ Cursor display when active
- ✅ Change callbacks
- ✅ Backspace/Enter handling
3. Slider (Slider.hpp/cpp)
Adjustable slider for numeric values (e.g., volume).
Constructor:
Slider(float x, float y, float width, float height,
const std::string& label,
float minValue, float maxValue, float currentValue);
Key Methods:
bool update(int mouseX, int mouseY, bool isMousePressed);
float getValue() const;
void setValue(float value);
void setOnChangeCallback(std::function<void(float)> callback);
Usage Example:
Slider volumeSlider(300.0f, 300.0f, 400.0f, 40.0f, "Volume", 0.0f, 100.0f, 75.0f);
volumeSlider.setOnChangeCallback([](float value) {
SoundManager::getInstance().setVolume(value);
});
// Update
volumeSlider.update(mouseX, mouseY, isMousePressed);
// Render slider bar and handle
Features:
- ✅ Drag to adjust value
- ✅ Value range constraints
- ✅ Real-time callbacks
- ✅ Visual feedback
4. ToggleButton (ToggleButton.hpp/cpp)
On/off toggle button (e.g., fullscreen).
Constructor:
ToggleButton(float x, float y, float width, float height,
const std::string& label, bool initialState);
Key Methods:
bool update(int mouseX, int mouseY, bool isMousePressed);
bool getState() const;
void setState(bool state);
void setOnChangeCallback(std::function<void(bool)> callback);
Usage Example:
ToggleButton fullscreenToggle(300.0f, 400.0f, 200.0f, 50.0f, "Fullscreen", false);
fullscreenToggle.setOnChangeCallback([&window](bool enabled) {
window->recreate(width, height, "R-Type", enabled);
});
if (fullscreenToggle.update(mouseX, mouseY, isMousePressed)) {
// Toggle was clicked
}
Features:
- ✅ Binary state (on/off)
- ✅ Toggle on click
- ✅ State callbacks
- ✅ Visual state indication
5. SelectionButton (SelectionButton.hpp/cpp)
Button for selecting from multiple options (e.g., resolution).
Constructor:
SelectionButton(float x, float y, float width, float height,
const std::string& label,
const std::vector<std::string>& options,
int selectedIndex);
Key Methods:
bool update(int mouseX, int mouseY, bool isMousePressed);
int getSelectedIndex() const;
std::string getSelectedOption() const;
void selectNext();
void selectPrevious();
void setOnChangeCallback(std::function<void(int)> callback);
Usage Example:
std::vector<std::string> resolutions = {"1280x720", "1920x1080", "2560x1440"};
SelectionButton resButton(300.0f, 250.0f, 400.0f, 50.0f, "Resolution", resolutions, 1);
resButton.setOnChangeCallback([](int index) {
// Apply selected resolution
});
// Cycle through options with left/right arrows
Features:
- ✅ Multiple option support
- ✅ Cycle through options
- ✅ Selected index tracking
- ✅ Change callbacks
🖥️ Main Screens
1. Main Menu (Menu.hpp/cpp)
Entry point of the application.
Structure:
class Menu {
public:
Menu(WindowSFML& window, GraphicsSFML& graphics, InputSFML& input);
MenuAction update(float deltaTime);
void render();
void updateLayout();
void reset();
private:
std::vector<Button> _buttons;
std::shared_ptr<Background> _background;
int _selectedButtonIndex;
};
enum class MenuAction {
None,
Play,
Settings,
Replays,
Exit
};
Button Layout:
- PLAY → Navigate to lobby
- SETTINGS → Open settings menu
- REPLAYS → Open replay browser
- EXIT → Close application
Features:
- ✅ Keyboard navigation (Up/Down/Enter)
- ✅ Mouse hover and click
- ✅ Animated background
- ✅ Sound effects on selection
Rendering:
┌─────────────────────────────────┐
│ R-TYPE │
│ [Animated Background] │
│ │
│ [ PLAY ] │
│ [ SETTINGS ] │
│ [ REPLAYS ] │
│ [ EXIT ] │
└─────────────────────────────────┘
2. Lobby System
The lobby system consists of multiple interconnected screens:
2.1 Lobby Menu (LobbyMenu.hpp/cpp)
Choose between creating or joining a lobby.
Structure:
class LobbyMenu {
public:
LobbyMenu(WindowSFML& window, GraphicsSFML& graphics, InputSFML& input,
std::shared_ptr<Background> background);
LobbyMenuAction update(float deltaTime);
void render();
enum class LobbyMenuAction {
None,
CreateLobby,
JoinLobby,
Back
};
Button Layout:
- CREATE LOBBY → Go to lobby config
- JOIN LOBBY → Open join dialog
- BACK → Return to main menu
2.2 Lobby Config Menu (LobbyConfigMenu.hpp/cpp)
Configure game rules before creating a lobby.
Structure:
struct GameRules {
uint8_t requiredPlayers; // 1-4 players
uint8_t maxRespawn; // 0-255 (255 = unlimited)
uint8_t enablePowerUps; // 0 or 1
uint8_t enableFriendlyFire; // 0 or 1
};
class LobbyConfigMenu {
public:
LobbyConfigAction update(float deltaTime);
void render();
const GameRules& getGameRules() const;
Configuration Options:
- Number of Players: 1, 2, 3, or 4
- Max Respawns: 0, 1, 3, 5, Unlimited
- Power-Ups: Enabled/Disabled
- Friendly Fire: Enabled/Disabled
Button Layout:
- CREATE → Create lobby with configured rules
- BACK → Return to lobby menu
Rendering:
┌─────────────────────────────────────┐
│ LOBBY CONFIGURATION │
│ │
│ Players: [ < 2 > ] │
│ Respawns: [ < 3 > ] │
│ Power-Ups: [ < ON > ] │
│ Friendly Fire: [ < OFF > ] │
│ │
│ [ CREATE ] [ BACK ] │
└─────────────────────────────────────┘
2.3 Lobby Waiting Room (LobbyWaitingRoom.hpp/cpp)
Wait for all players to join and become ready.
Structure:
struct PlayerSlot {
uint32_t playerId;
std::string username;
bool isConnected;
bool isReady;
bool isLeader;
};
class LobbyWaitingRoom {
public:
WaitingRoomAction update(float deltaTime);
void render();
void updatePlayer(uint32_t playerId, const std::string& username,
bool isReady, bool isLeader);
void removePlayer(uint32_t playerId);
bool areAllPlayersReady() const;
enum class WaitingRoomAction {
None,
StartGame,
ToggleReady,
Back
};
Features:
- Lobby ID Display: Shows unique 6-character lobby code
- Player Slots: Shows connected players (max 4)
- Ready Status: Green when ready, orange when not ready
- Leader Badge: First player is lobby leader
- Game Rules Display: Shows configured game settings
Button Layout:
- READY / NOT READY → Toggle ready status
- START GAME → (Leader only, when all ready)
- BACK → Leave lobby
Slot Rendering:
┌───────────────────────────────────────────────────┐
│ LOBBY ID: UAKA052G │
│ │
│ WAITING FOR PLAYERS │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Player 1 [LEADER] READY│ │
│ │ Player1 │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Player 2 NOT READY│ │
│ │ Player2 │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Player 3 │ │
│ │ Waiting for player... │ │
│ └─────────────────────────────────────┘ │
│ │
│ [ READY ] [ BACK ] │
└───────────────────────────────────────────────────┘
Slot Dimensions (Updated):
- Width: 600px (was 400px)
- Height: 110px (was 80px)
- Font Size: 36pt (was 28pt)
2.4 Join Lobby Dialog (JoinLobbyDialog.hpp/cpp)
Enter lobby ID to join an existing lobby.
Structure:
class JoinLobbyDialog {
public:
JoinDialogAction update(float deltaTime);
void render();
std::string getLobbyId() const;
void setErrorMessage(const std::string& message);
enum class JoinDialogAction {
None,
Join,
Cancel
};
UI Elements:
- Input Field: Enter 6-character lobby ID
- Error Message Display: Shows join errors
- Buttons: JOIN, CANCEL
Rendering:
┌─────────────────────────────────┐
│ JOIN LOBBY │
│ │
│ Lobby ID: [______] │
│ │
│ [Error: Lobby not found!] │
│ │
│ [ JOIN ] [ CANCEL ] │
└─────────────────────────────────┘
Error Messages:
- "Lobby is full!"
- "Lobby not found!"
- "Already in game!"
- "Connection failed!"
3. Settings Menu (SettingsMenu.hpp/cpp)
Comprehensive settings and configuration.
Structure:
class SettingsMenu {
public:
SettingsMenuAction update(float deltaTime);
void render();
void saveSettings();
void loadSettings();
enum class SettingsMenuAction {
None,
Back
};
Configuration Sections:
Graphics Settings:
- Resolution: Dropdown selector
- 1280x720
- 1920x1080 (default)
- 2560x1440
- Fullscreen: Toggle button
Audio Settings:
- SFX Volume: Slider (0-100%)
- Music Volume: Slider (0-100%)
Accessibility:
- Color Blind Mode: Selection button
- None (default)
- Protanopia (red-blind)
- Deuteranopia (green-blind)
- Tritanopia (blue-blind)
Controls:
- Key Bindings: Customizable keys
- Move Up (default: Z)
- Move Down (default: S)
- Move Left (default: Q)
- Move Right (default: D)
- Shoot (default: Space)
Rendering:
┌───────────────────────────────────────────┐
│ SETTINGS │
│ │
│ GRAPHICS │
│ Resolution: [ < 1920x1080 > ] │
│ Fullscreen: [ ON ] │
│ │
│ AUDIO │
│ SFX Volume: [━━━━━━━━━─] 75% │
│ Music Volume: [━━━━━━━━──] 65% │
│ │
│ ACCESSIBILITY │
│ Color Blind: [ < None > ] │
│ │
│ CONTROLS │
│ Move Up: [ Z ] │
│ Move Down: [ S ] │
│ Shoot: [ Space ] │
│ │
│ [ BACK ] │
└───────────────────────────────────────────┘
Persistence:
All settings are saved to config.json and loaded on startup.
4. Game Over Screen (GameOverScreen.hpp/cpp)
Displayed after game ends.
Structure:
class GameOverScreen {
public:
GameOverAction update(float deltaTime);
void render();
void setScore(uint32_t score);
void setWinner(bool isWinner);
enum class GameOverAction {
None,
PlayAgain,
MainMenu
};
Display Elements:
- Title: "VICTORY!" or "DEFEAT!"
- Final Score: Player's score
- Buttons: PLAY AGAIN, MAIN MENU
Rendering:
┌─────────────────────────────────┐
│ VICTORY! │
│ │
│ Final Score: 15,420 │
│ │
│ │
│ [ PLAY AGAIN ] │
│ [ MAIN MENU ] │
└─────────────────────────────────┘
5. Replay Browser (ReplayBrowser.hpp/cpp)
Browse and select saved replays.
Structure:
class ReplayBrowser {
public:
ReplayBrowserAction update(float deltaTime);
void render();
std::string getSelectedReplayPath() const;
enum class ReplayBrowserAction {
None,
WatchReplay,
Back
};
Features:
- File List: All
.rtrfiles inreplays/directory - Sorting: By date (newest first)
- Selection: Click to select, double-click to watch
- Scrolling: Scroll through replay list
Rendering:
┌─────────────────────────────────────────┐
│ REPLAYS │
│ │
│ game_20260118_143022.rtr ◀ │
│ game_20260118_140515.rtr │
│ game_20260117_210338.rtr │
│ game_20260117_195422.rtr │
│ │
│ [ WATCH ] [ BACK ] │
└─────────────────────────────────────────┘
6. Replay Controls (ReplayControls.hpp/cpp)
Playback controls during replay viewing.
Structure:
class ReplayControls {
public:
void update(float deltaTime, bool isPaused, float currentTime,
float totalTime, float speed);
void render(GraphicsSFML& graphics);
bool isRewindClicked(int mouseX, int mouseY, bool isMousePressed);
bool isForwardClicked(int mouseX, int mouseY, bool isMousePressed);
bool isSpeedClicked(int mouseX, int mouseY, bool isMousePressed);
bool isPauseClicked(int mouseX, int mouseY, bool isMousePressed);
Controls:
- ⏸️ Pause/Play: Toggle playback
- ⏪ Rewind: Jump back 10 seconds
- ⏩ Forward: Jump forward 10 seconds
- 🔄 Speed: Cycle speed (0.5x, 1x, 2x)
- ❌ Exit: Return to replay browser
Progress Bar:
- Current time / Total time (MM:SS format)
- Visual progress indicator
- Click to seek
Rendering:
┌─────────────────────────────────────────────────┐
│ [Game Playback] │
│ ┌─────────────────────────────────────────┐ │
│ │ [⏸️] [⏪] [⏩] [0.5x] [❌] │ │
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │
│ │ 01:23 / 03:45 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
🎨 UI Rendering Guidelines
Layout Calculations
All UI uses responsive scaling based on window size:
float windowWidth = static_cast<float>(_window.getWidth());
float windowHeight = static_cast<float>(_window.getHeight());
float scaleW = windowWidth / 1920.0f; // Base: 1920x1080
float scaleH = windowHeight / 1080.0f;
// Scale button dimensions
float btnWidth = BUTTON_WIDTH * scaleW;
float btnHeight = BUTTON_HEIGHT * scaleH;
// Center horizontally
float centerX = windowWidth / 2.0f;
float buttonX = centerX - (btnWidth / 2.0f);
Colors
Standard color palette:
// Button colors
const Color BUTTON_DEFAULT = {30, 30, 100, 255}; // Dark blue
const Color BUTTON_HOVER = {0, 200, 255, 255}; // Bright blue
const Color BUTTON_FOCUSED = {0, 200, 255, 255}; // Bright blue
// Slot colors
const Color SLOT_READY = {0, 150, 50, 255}; // Green
const Color SLOT_NOT_READY = {150, 100, 0, 255}; // Orange
const Color SLOT_EMPTY = {50, 50, 50, 255}; // Gray
// Text colors
const Color TEXT_WHITE = {255, 255, 255, 255};
const Color TEXT_GRAY = {150, 150, 150, 255};
const Color TEXT_YELLOW = {255, 255, 0, 255}; // Leader badge
const Color TEXT_GOLD = {255, 215, 0, 255}; // Leader badge
Font Sizes
const unsigned int TITLE_FONT_SIZE = 64;
const unsigned int BUTTON_FONT_SIZE = 24;
const unsigned int SLOT_FONT_SIZE = 36;
const unsigned int INFO_FONT_SIZE = 20;
🔄 Update/Render Pattern
All UI screens follow this pattern:
class MyScreen {
public:
MyScreenAction update(float deltaTime) {
// 1. Update background
if (_background) {
_background->update(deltaTime);
}
// 2. Get input
int mouseX = _input.getMouseX();
int mouseY = _input.getMouseY();
bool isMousePressed = _input.isMouseButtonPressed(MouseButton::Left);
// 3. Handle keyboard navigation
if (isUpPressed && !_wasUpPressed) {
selectPreviousButton();
}
if (isDownPressed && !_wasDownPressed) {
selectNextButton();
}
// 4. Handle button clicks
if (isEnterPressed || isMousePressed) {
return handleButtonClick();
}
return MyScreenAction::None;
}
void render() {
// 1. Draw background
if (_background) {
_background->draw(_graphics);
}
// 2. Draw title
drawTitle();
// 3. Draw UI elements
for (const auto& button : _buttons) {
drawButton(button);
}
// 4. Draw text/overlays
drawInfoText();
}
};
🔍 Common UI Patterns
1. Keyboard Navigation
// Button selection with Up/Down
if (isUpPressed && !_wasUpPressed) {
_buttons[_selectedButtonIndex].setFocused(false);
_selectedButtonIndex--;
if (_selectedButtonIndex < 0) {
_selectedButtonIndex = _buttons.size() - 1;
}
_buttons[_selectedButtonIndex].setFocused(true);
SoundManager::getInstance().playSound("UI_MENU_SELECT");
}
2. Mouse Hover
// Update hover state
for (size_t i = 0; i < _buttons.size(); ++i) {
if (_buttons[i].isHovered(mouseX, mouseY)) {
if (_selectedButtonIndex != i) {
_buttons[_selectedButtonIndex].setFocused(false);
_selectedButtonIndex = i;
_buttons[_selectedButtonIndex].setFocused(true);
SoundManager::getInstance().playSound("UI_MENU_SELECT");
}
}
}
3. Button Click Handling
if ((isEnterPressed && !_wasEnterPressed) ||
(isMousePressed && _buttons[_selectedButtonIndex].isHovered(mouseX, mouseY))) {
SoundManager::getInstance().playSound("UI_MENU_VALIDATE");
switch (_selectedButtonIndex) {
case 0: return ScreenAction::FirstButton;
case 1: return ScreenAction::SecondButton;
// ...
}
}
🚀 Performance Tips
- Minimize Redraws: Only update UI when state changes
- Cache Text Width: Calculate once, reuse for centering
- Limit Input Polling: Check input once per frame
- Background Caching: Reuse background sprite
- Event Debouncing: Use
_wasPressedflags to prevent double-triggers
📚 Further Reading
- Game State Management - State transitions
- Configuration System - Persisting UI settings
- Audio System - UI sound effects
- Tutorials - Creating custom UI components