Scriptable Objects for Game Config Without the Mess
ScriptableObjects are the cleanest way to store game config in Unity, until they are not. Here is how to organise them so balance data stays editable and version control stays sane.

ScriptableObjects are the right tool for game configuration in Unity when you follow three rules: one asset per concern, no runtime mutation, and a flat structure that version control can merge. Break any of the three and the system that felt clean at ten assets becomes unmanageable at a hundred.
Why ScriptableObjects for config
The alternatives are worse in specific, predictable ways.
Hardcoded constants cannot be changed without a code change and a build. Designers cannot touch them.
JSON or CSV files work but lack editor integration. No inspector, no validation, no drag-and-drop references to other assets.
MonoBehaviour fields on prefabs mix configuration with behaviour, and changing a value means checking out a prefab, which causes merge conflicts in teams.
ScriptableObjects sit in the project as assets. The inspector is the editor. Version control tracks them as YAML. Designers can change values without opening a code file.
The goal is a config layer where a designer can change any number, test it immediately, and commit the change without touching code.
One asset per concern
The first rule, and the one most projects violate.
A single "GameConfig" ScriptableObject with every value in the game is easy to create and impossible to maintain. Two people editing the same asset at the same time produces merge conflicts, and a designer looking for one value scrolls past hundreds.
Split by concern.
- PlayerConfig. Health, speed, jump height, damage.
- EconomyConfig. Currency rates, shop prices, reward tables.
- LevelConfig. Per-level assets, one per level or per level group.
- DifficultyConfig. Curves, multipliers, scaling rules.
- AudioConfig. Volume defaults, music crossfade timing.
Each is a small asset that one person edits at a time. Merge conflicts become rare because two designers are rarely changing the same concern on the same day.
Name them consistently. A folder called Config with assets named Config_Player, Config_Economy is searchable and scannable.
No runtime mutation
The second rule, and the one that causes the strangest bugs.
ScriptableObjects in the editor persist changes made at runtime. In a build, they do not. This means a system that modifies a ScriptableObject field during play works perfectly in the editor and breaks silently in production.
The fix is simple: treat config assets as read-only at runtime. Never write to them. If gameplay needs to modify a value, copy it into a runtime data structure at load time and modify the copy.
This pattern also makes reset trivial. Restarting a level means reloading the original values from the asset, which has not changed.
Three enforcement methods, from light to strict.
- Convention. Agree that config assets are read-only. Works for small teams.
- Interface. Expose config through a read-only interface so calling code cannot access setters.
- Runtime copy. Load config into a plain class at startup. The ScriptableObject is never referenced during play.
The third method also improves testability, because you can construct the plain class with test values without needing an asset.
Flat structure for version control
ScriptableObjects serialize as YAML in Unity. Simple fields, arrays of primitives, and references to other assets merge cleanly. Nested structures, polymorphic lists, and serialized dictionaries do not.
Keep the structure flat.
| Structure | Merges well | Merges badly |
|---|---|---|
| Float, int, string fields | Yes | |
| Arrays of primitives | Usually | |
| References to other assets | Yes | |
| Nested serializable classes | Sometimes | When deep |
| Lists of polymorphic types | Yes | |
| Serialized dictionaries | Yes |
If you need a dictionary, consider an array of key-value structs instead. It serializes as a YAML sequence, which merges line by line rather than as a blob.
If you need polymorphic config, split it into separate ScriptableObject types with a shared base class. Each variant is its own asset, and the parent references them. This replaces one asset with complex internals with several simple assets that merge independently.
Editor tooling worth building
Two tools pay for themselves quickly.
A validation pass. A menu item that loads every config asset and checks for invalid values: negative health, prices of zero, missing references. Run it before every commit. The check is a loop over all assets of a type, which Unity makes easy with AssetDatabase queries.
A quick-access window. A custom editor window that lists every config asset as a clickable button. Faster than navigating folders, especially for a designer who changes values dozens of times a day.
Both are under fifty lines of editor code. Neither is essential at the start, but both become essential once the project has more than twenty config assets.
Config references at runtime
How gameplay code accesses config values matters as much as how they are stored.
Three approaches, ordered by complexity.
Direct reference. A MonoBehaviour holds a serialized field pointing to the config asset. Simple, explicit, and easy to trace. Best for components that always need the same config.
Service locator. A static class holds references loaded at startup. Any system can access any config. Less explicit, more flexible. Best when many systems share the same config.
Dependency injection. Config is injected into systems at construction. The most testable and the most setup. Worth it for large projects, overhead for small ones.
For a mobile game with under fifty config assets, direct references cover most needs and service locator covers the rest. Start simple and migrate if complexity demands it.
Common traps
Five mistakes that recur across projects.
- One giant config asset. Causes merge conflicts and is overwhelming to edit.
- Mutating config at runtime. Works in the editor, fails in builds, wastes hours debugging.
- Deeply nested structures. Version control cannot merge them sensibly.
- No validation. A mistyped value in config is silent until a player hits it.
- Configs referencing configs referencing configs. Long reference chains are hard to trace. Keep the graph shallow, usually one level deep.
What we would do
Split config by concern on day one. It takes five minutes longer than a single asset and prevents weeks of merge conflict pain later.
Enforce read-only access through a runtime copy pattern. The copy adds a small amount of code and eliminates an entire class of editor-versus-build bugs.
Build the validation pass as soon as you have ten config assets. The first bug it catches pays for the hour it took to write.
The short version
- One ScriptableObject per concern: player, economy, difficulty, audio.
- Never mutate config assets at runtime. Copy values into a runtime structure at load.
- Keep structures flat so YAML merges work cleanly in version control.
- Build a validation pass that checks for invalid values before each commit.
- Use direct references for simple access and a service locator when many systems share config.
- Avoid one giant config asset, deep nesting, and long reference chains.
Split your current config asset this week. If you want a clean config architecture built into your project, talk to us.
Related reading: Unity Packages Worth Importing, Unity Save System Guide, and Free Tools for Solo Devs.
Got a game idea? We build it.
You bring the concept. We design, build, test and launch it, and you own 100% of the finished game.
Share Your Game Idea →