Skip to main content

Configuration System

This document explains how the R-Type client manages user settings and configuration persistence.


🎯 Overview

The configuration system uses a singleton pattern with JSON-based persistence to manage all user settings.

Key Features:

  • ✅ Persistent storage (config.json)
  • ✅ Type-safe getters/setters
  • ✅ Default values
  • ✅ Runtime configuration updates
  • ✅ Automatic saving

📦 Architecture

┌─────────────────────────────────────────┐
│ Config (Singleton) │
├─────────────────────────────────────────┤
│ - _settings : map<string, Setting> │
│ - CONFIG_FILE : "config.json" │
├─────────────────────────────────────────┤
│ + getInstance() : Config& │
│ + load() : void │
│ + save() : void │
│ + getInt(key, default) : int │
│ + getFloat(key, default) : float │
│ + getString(key, default) : string │
│ + setInt(key, value) : void │
│ + setFloat(key, value) : void │
│ + setString(key, value) : void │
└─────────────────────────────────────────┘

📄 Configuration File Format

Location: config.json (working directory)

Format:

{
"resolutionWidth": 1920,
"resolutionHeight": 1080,
"fullscreen": 0,
"sfxVolume": 100.0,
"musicVolume": 100.0,
"colorBlindMode": "None",
"keyBindings": {
"moveUp": "Z",
"moveDown": "S",
"moveLeft": "Q",
"moveRight": "D",
"shoot": "Space"
}
}

🔑 Configuration Keys

Graphics Settings

KeyTypeDefaultDescription
resolutionWidthint1920Window width in pixels
resolutionHeightint1080Window height in pixels
fullscreenint00=windowed, 1=fullscreen

Audio Settings

KeyTypeDefaultDescription
sfxVolumefloat100.0SFX volume (0.0-100.0)
musicVolumefloat100.0Music volume (0.0-100.0)

Accessibility Settings

KeyTypeDefaultDescription
colorBlindModestring"None"Color blind filter type

Color Blind Modes:

  • "None" - No filter (default)
  • "Protanopia" - Red-blind
  • "Deuteranopia" - Green-blind
  • "Tritanopia" - Blue-blind

Key Bindings

KeyTypeDefaultDescription
keyBindings.moveUpstring"Z"Move up key
keyBindings.moveDownstring"S"Move down key
keyBindings.moveLeftstring"Q"Move left key
keyBindings.moveRightstring"D"Move right key
keyBindings.shootstring"Space"Shoot key

💻 Usage Examples

Basic Usage

#include "Config.hpp"

int main() {
// Get singleton instance
Config& config = Config::getInstance();

// Load configuration from disk
config.load();

// Read settings
int width = config.getInt("resolutionWidth", 1920);
int height = config.getInt("resolutionHeight", 1080);
float volume = config.getFloat("sfxVolume", 100.0f);
std::string colorMode = config.getString("colorBlindMode", "None");

// Use settings
window->create(width, height, "R-Type");
soundManager->setVolume(volume);

return 0;
}

Updating Settings

// Change settings
config.setInt("resolutionWidth", 2560);
config.setInt("resolutionHeight", 1440);
config.setInt("fullscreen", 1);
config.setFloat("sfxVolume", 75.0f);

// Save to disk
config.save();

// Apply changes
window->recreate(2560, 1440, "R-Type", true);
soundManager->setVolume(75.0f);

Settings Menu Integration

class SettingsMenu {
private:
Slider _sfxVolumeSlider;
Slider _musicVolumeSlider;
ToggleButton _fullscreenToggle;
SelectionButton _resolutionButton;

public:
void loadSettings() {
Config& config = Config::getInstance();

// Load current settings into UI
float sfxVol = config.getFloat("sfxVolume", 100.0f);
float musicVol = config.getFloat("musicVolume", 100.0f);
bool fullscreen = config.getInt("fullscreen", 0) == 1;

_sfxVolumeSlider.setValue(sfxVol);
_musicVolumeSlider.setValue(musicVol);
_fullscreenToggle.setState(fullscreen);
}

void saveSettings() {
Config& config = Config::getInstance();

// Save UI values to config
config.setFloat("sfxVolume", _sfxVolumeSlider.getValue());
config.setFloat("musicVolume", _musicVolumeSlider.getValue());
config.setInt("fullscreen", _fullscreenToggle.getState() ? 1 : 0);

// Persist to disk
config.save();
}
};

🔄 Lifecycle

Application Startup

int main() {
// 1. Get config instance
Config& config = Config::getInstance();

// 2. Load from disk (creates with defaults if missing)
config.load();

// 3. Apply settings
int width = config.getInt("resolutionWidth", 1920);
int height = config.getInt("resolutionHeight", 1080);
auto window = std::make_unique<WindowSFML>(width, height, "R-Type");

// ... rest of initialization
}

Runtime Updates

// Settings changed in UI
void onVolumeChanged(float newVolume) {
// 1. Update config
Config::getInstance().setFloat("sfxVolume", newVolume);

// 2. Apply immediately
SoundManager::getInstance().setVolume(newVolume);

// 3. No need to save yet (save on settings menu exit)
}

Application Shutdown

void onSettingsMenuExit() {
// Save all changes to disk
Config::getInstance().save();
}

🔍 Implementation Details

File Structure

// Config.hpp
#pragma once

#include <map>
#include <string>
#include <variant>

namespace rtype {

class Config {
public:
static Config& getInstance();

void load();
void save();

int getInt(const std::string& key, int defaultValue) const;
float getFloat(const std::string& key, float defaultValue) const;
std::string getString(const std::string& key, const std::string& defaultValue) const;

void setInt(const std::string& key, int value);
void setFloat(const std::string& key, float value);
void setString(const std::string& key, const std::string& value);

private:
Config() = default;
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;

using SettingValue = std::variant<int, float, std::string>;
std::map<std::string, SettingValue> _settings;

static constexpr const char* CONFIG_FILE = "config.json";

void loadDefaults();
void parseJsonFile(const std::string& filepath);
void writeJsonFile(const std::string& filepath);
};

} // namespace rtype

Load Implementation

void Config::load() {
// Try to load from file
if (std::filesystem::exists(CONFIG_FILE)) {
try {
parseJsonFile(CONFIG_FILE);
return;
} catch (const std::exception& e) {
std::cerr << "Failed to load config: " << e.what() << std::endl;
}
}

// Fall back to defaults
loadDefaults();
}

void Config::loadDefaults() {
// Graphics
_settings["resolutionWidth"] = 1920;
_settings["resolutionHeight"] = 1080;
_settings["fullscreen"] = 0;

// Audio
_settings["sfxVolume"] = 100.0f;
_settings["musicVolume"] = 100.0f;

// Accessibility
_settings["colorBlindMode"] = std::string("None");

// Key bindings (stored as separate keys)
_settings["keyBindings.moveUp"] = std::string("Z");
_settings["keyBindings.moveDown"] = std::string("S");
_settings["keyBindings.moveLeft"] = std::string("Q");
_settings["keyBindings.moveRight"] = std::string("D");
_settings["keyBindings.shoot"] = std::string("Space");
}

Save Implementation

void Config::save() {
try {
writeJsonFile(CONFIG_FILE);
} catch (const std::exception& e) {
std::cerr << "Failed to save config: " << e.what() << std::endl;
}
}

void Config::writeJsonFile(const std::string& filepath) {
// Build JSON object
nlohmann::json j;

for (const auto& [key, value] : _settings) {
if (std::holds_alternative<int>(value)) {
j[key] = std::get<int>(value);
} else if (std::holds_alternative<float>(value)) {
j[key] = std::get<float>(value);
} else if (std::holds_alternative<std::string>(value)) {
j[key] = std::get<std::string>(value);
}
}

// Write to file
std::ofstream file(filepath);
file << j.dump(2); // Pretty print with 2-space indent
}

Getters Implementation

int Config::getInt(const std::string& key, int defaultValue) const {
auto it = _settings.find(key);
if (it != _settings.end() && std::holds_alternative<int>(it->second)) {
return std::get<int>(it->second);
}
return defaultValue;
}

float Config::getFloat(const std::string& key, float defaultValue) const {
auto it = _settings.find(key);
if (it != _settings.end() && std::holds_alternative<float>(it->second)) {
return std::get<float>(it->second);
}
return defaultValue;
}

std::string Config::getString(const std::string& key, const std::string& defaultValue) const {
auto it = _settings.find(key);
if (it != _settings.end() && std::holds_alternative<std::string>(it->second)) {
return std::get<std::string>(it->second);
}
return defaultValue;
}

🎨 Key Binding System

KeyBinding Class

class KeyBinding {
public:
enum class Action {
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
Shoot
};

static std::string getKeyForAction(Action action);
static void setKeyForAction(Action action, const std::string& key);
static Key getKeyCode(const std::string& keyName);
static std::string getKeyName(Key keyCode);
};

Usage in Game

// Check if move up key is pressed
std::string moveUpKey = KeyBinding::getKeyForAction(KeyBinding::Action::MoveUp);
Key moveUpCode = KeyBinding::getKeyCode(moveUpKey);

if (input->isKeyPressed(moveUpCode)) {
player->moveUp();
}

Key Name Mapping

std::map<std::string, Key> keyNameToCode = {
{"Z", Key::Z},
{"S", Key::S},
{"Q", Key::Q},
{"D", Key::D},
{"Space", Key::Space},
{"Escape", Key::Escape},
{"Enter", Key::Enter},
{"Up", Key::Up},
{"Down", Key::Down},
{"Left", Key::Left},
{"Right", Key::Right}
};

⚠️ Error Handling

Missing Config File

void Config::load() {
if (!std::filesystem::exists(CONFIG_FILE)) {
std::cout << "Config file not found, using defaults" << std::endl;
loadDefaults();
save(); // Create file with defaults
return;
}

// ... load from file
}

Corrupted Config File

void Config::parseJsonFile(const std::string& filepath) {
try {
std::ifstream file(filepath);
nlohmann::json j;
file >> j;

// Parse JSON into settings map
// ...
} catch (const nlohmann::json::exception& e) {
std::cerr << "JSON parse error: " << e.what() << std::endl;
loadDefaults(); // Fall back to defaults
}
}

Invalid Setting Values

void Config::setInt(const std::string& key, int value) {
// Validate ranges
if (key == "resolutionWidth" && (value < 640 || value > 7680)) {
std::cerr << "Invalid resolution width: " << value << std::endl;
return;
}

if (key == "fullscreen" && (value != 0 && value != 1)) {
std::cerr << "Invalid fullscreen value: " << value << std::endl;
return;
}

_settings[key] = value;
}

🔒 Thread Safety

The Config singleton is not thread-safe by design because:

  • All config access happens on the main thread only
  • Settings are only loaded at startup and saved when exiting settings menu
  • No concurrent modification from multiple threads

If thread safety is needed:

class Config {
private:
mutable std::mutex _mutex;

public:
int getInt(const std::string& key, int defaultValue) const {
std::lock_guard<std::mutex> lock(_mutex);
// ... get value
}

void setInt(const std::string& key, int value) {
std::lock_guard<std::mutex> lock(_mutex);
// ... set value
}
};

🚀 Best Practices

  1. Always provide defaults in getter calls
  2. Validate values before setting
  3. Save settings when exiting settings menu, not on every change
  4. Apply settings immediately for real-time feedback
  5. Handle missing/corrupted files gracefully
  6. Use constants for key names to avoid typos
  7. Document valid ranges for numeric settings

📊 Performance Considerations

  • Loading: O(n) where n = number of settings (~10-20)
  • Saving: O(n) JSON serialization
  • Getting: O(log n) map lookup
  • Setting: O(log n) map insertion

Optimization Tips:

  • Load config once at startup
  • Cache frequently accessed values
  • Batch save operations
  • Use defaults for optional settings

🔧 Extending the System

Adding New Settings

// 1. Add default value
void Config::loadDefaults() {
// ... existing defaults
_settings["myNewSetting"] = 42;
}

// 2. Add getter/setter if needed (or use existing)
int myValue = config.getInt("myNewSetting", 42);

// 3. Use in application
applyMyNewSetting(myValue);

Custom Setting Types

// For complex types, use JSON strings
struct Resolution {
int width;
int height;
};

// Serialize
std::string resJson = "{\"width\":" + std::to_string(res.width) +
",\"height\":" + std::to_string(res.height) + "}";
config.setString("resolution", resJson);

// Deserialize
std::string resJson = config.getString("resolution", "{\"width\":1920,\"height\":1080}");
auto j = nlohmann::json::parse(resJson);
Resolution res{j["width"], j["height"]};

📚 Further Reading