⚙️ 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=...)andUPROPERTY(Config) - Explain where config values are read from and the
.inihierarchy - Save changed config back to disk with
SaveConfig - Choose config vs a DataAsset vs SaveGame for a value
- Recognize
UDeveloperSettingsfor 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:
| Tool | Holds | Lives in |
|---|---|---|
| DataTable / DataAsset (7.1) | Gameplay content authored by designers | Assets in Content/ |
| Config (this lesson) | Settings & tunables, defaults & overrides | .ini files in Config/ & Saved/ |
| SaveGame (7.3) | A player's progress | Save 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.
(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)"]
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.
- Create
UPlayerOptions : public UObjectwithUCLASS(Config = Game). - Add
Configproperties:float SfxVolume,float MusicVolume,bool bInvertY,int32 GraphicsQuality. - Write setters that clamp and call
SaveConfig(). - Explain why
GraphicsQualitybelongs 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 fieldUPROPERTY(Config)— both are required. - Config is a layered hierarchy: committed
Config/DefaultGame.inidefaults, overridden by the generated per-userSaved/ini. - Reading is automatic at load; call
SaveConfig()to persist a runtime change (to the per-user file).GConfiggives ad-hoc access. UDeveloperSettingssurfaces project-wide settings in the Project Settings UI, accessible viaGetDefault<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.