Skip to main content

⚙️ Lesson 7.2: Config Properties & .ini Files

Some values aren't gameplay content — they're settings: audio volume, difficulty, quality, key bindings, server addresses. These belong in .ini config files, and Unreal can load and save them into your C++ properties automatically. Learn the config system and settings become almost free.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Mark a class and properties for config with UCLASS(Config=...) and UPROPERTY(Config)
  • Explain where config values are read from and the .ini hierarchy
  • Save changed config back to disk with SaveConfig
  • Choose config vs a DataAsset vs SaveGame for a value
  • Recognize UDeveloperSettings for project settings pages

Estimated Time: 45 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

What Config Is For

Config is for settings that persist between runs but aren't part of a player's save game — project- or machine-level values. Think of the three persistence tools you'll meet this module as a spectrum:

ToolHoldsLives in
DataTable / DataAsset (7.1)Gameplay content authored by designersAssets in Content/
Config (this lesson)Settings & tunables, defaults & overrides.ini files in Config/ & Saved/
SaveGame (7.3)A player's progressSave slots on disk

📖 Config vs SaveGame — a real distinction

Master volume and graphics quality are config — they belong to the install/machine and apply across all saves. A player's level, inventory, and quest progress are SaveGame — per playthrough. Don't put progress in config or settings in a save; the confusion causes real bugs.

Declaring Config Properties

Mark the class with Config=<FileName> and each persisted property with Config. Unreal reads the values from the matching .ini at load.

// GameSettings.h
UCLASS(Config = Game)   // reads/writes DefaultGame.ini / Saved Game.ini
class MYPROJECT_API UGameSettings : public UObject
{
    GENERATED_BODY()

public:
    // 'Config' marks this property as loaded from / saved to the ini file.
    UPROPERTY(Config, EditAnywhere, Category = "Audio")
    float MasterVolume = 1.0f;

    UPROPERTY(Config, EditAnywhere, Category = "Gameplay")
    int32 Difficulty = 1;

    UPROPERTY(Config, EditAnywhere, Category = "Gameplay")
    bool bShowTutorials = true;
};

The corresponding .ini section is keyed by [Package.Class]:

# In Config/DefaultGame.ini
[/Script/MyProject.GameSettings]
MasterVolume=0.8
Difficulty=2
bShowTutorials=False

⚠️ The value is only read if the property is Config

A property without the Config specifier is ignored by the ini, even if the class is Config=Game. Both pieces are required: the class chooses the file, each property opts in. The common config value types — numbers, bools, FString, enums, and arrays — all serialize to ini text.

The .ini Hierarchy

Config isn't one file — it's a layered hierarchy, where later layers override earlier ones. This is what lets a project ship defaults while a player's machine keeps personal overrides.

graph TD Base["Engine base config
(engine defaults)"] --> Proj["Config/DefaultGame.ini
(your project defaults — committed)"] Proj --> Plat["Platform overrides
(Windows/Mac/...)"] Plat --> User["Saved/Config/.../Game.ini
(per-user, generated — not committed)"]
Figure 1: Config layers, each overriding the last. You commit Config/DefaultGame.ini (project defaults); the per-user file in Saved/ is generated and holds runtime changes.

✅ Why the split matters for source control

Recall Lesson 1.2/1.3: Config/ is committed, Saved/ is not. Your project's default settings live in committed DefaultGame.ini; each player's changed settings land in the generated Saved/ ini. So shipping a new default doesn't stomp a player's saved override — the hierarchy resolves it. This is the same generated-vs-committed line you set up in your .gitignore.

Saving Config

Reading happens automatically at load. To persist a change the player made (they turned the volume down), call SaveConfig — it writes the Config properties back to the per-user ini.

void UGameSettings::SetMasterVolume(float NewVolume)
{
    MasterVolume = FMath::Clamp(NewVolume, 0.0f, 1.0f);
    SaveConfig();   // persist to the per-user ini so it survives a restart
}

For direct, ad-hoc access to any ini value, the global GConfig object is available — useful for reading a one-off value without a config class:

float Volume = 1.0f;
GConfig->GetFloat(
    TEXT("/Script/MyProject.GameSettings"), TEXT("MasterVolume"),
    Volume, GGameIni);
// GGameIni is the resolved path to the active Game.ini.

⚠️ SaveConfig writes to the per-user file, not your defaults

SaveConfig persists to the generated Saved/ ini — it does not edit your committed DefaultGame.ini. That's correct: runtime changes are the player's, not the project's. If you want to change a shipped default, edit DefaultGame.ini in the editor's Project Settings or by hand.

Developer Settings

For settings you want to appear as a page in Project Settings (or Editor Preferences), subclass UDeveloperSettings. It's a config class that Unreal automatically surfaces in the settings UI and makes globally accessible.

#include "Engine/DeveloperSettings.h"

UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "My Game Settings"))
class MYPROJECT_API UMyGameSettings : public UDeveloperSettings
{
    GENERATED_BODY()

public:
    UPROPERTY(Config, EditAnywhere, Category = "Gameplay")
    float DefaultDifficultyScale = 1.0f;

    // Reach it anywhere:
    static const UMyGameSettings* Get()
    {
        return GetDefault<UMyGameSettings>();
    }
};

✅ When to reach for it

UDeveloperSettings is ideal for project-wide tunables a developer sets in Project Settings — a global difficulty scale, feature flags, default classes. DefaultConfig makes edits save to DefaultGame.ini (committed project defaults). Access the read-only defaults anywhere with GetDefault<T>(). It's the modern replacement for scattering globals and magic numbers through code.

Hands-on Exercise & Quiz

🏋️ Exercise: An options object

Objective: Build a persistable settings class.

  1. Create UPlayerOptions : public UObject with UCLASS(Config = Game).
  2. Add Config properties: float SfxVolume, float MusicVolume, bool bInvertY, int32 GraphicsQuality.
  3. Write setters that clamp and call SaveConfig().
  4. Explain why GraphicsQuality belongs in config, not in a SaveGame.
✅ Answer

Graphics quality is a machine/install setting shared across all playthroughs and players on that PC — it's not part of any single save's progress. Putting it in config means it applies globally and persists independent of which save is loaded. A SaveGame would wrongly tie it to one playthrough.

🎯 Quick Quiz

Question 1: What two things must be marked for a property to persist to an ini?

Question 2: A player lowers the music volume at runtime. To make it stick, call:

Question 3: Which value belongs in config rather than a SaveGame?

Summary

🎉 Key Takeaways

  • Config is for settings that persist between runs but aren't save-game progress (volume, quality, difficulty defaults).
  • Mark the class UCLASS(Config=Game) and each persisted field UPROPERTY(Config) — both are required.
  • Config is a layered hierarchy: committed Config/DefaultGame.ini defaults, overridden by the generated per-user Saved/ ini.
  • Reading is automatic at load; call SaveConfig() to persist a runtime change (to the per-user file). GConfig gives ad-hoc access.
  • UDeveloperSettings surfaces project-wide settings in the Project Settings UI, accessible via GetDefault<T>().

📚 Additional Resources

🚀 What's Next?

Config persists settings. Next we persist the thing players care about most — their progress. SaveGame: writing and reading a save slot so a player can pick up where they left off.

🎉 Lesson complete!

Settings persist. Now let's save the player's whole journey.