Quick answer: ScriptableObjects persist only in the editor. In builds, runtime writes are in-memory and lost on quit. For user data, use JSON files in Application.persistentDataPath. Use ScriptableObjects only for shared configuration data that ships with your game.
Here is how to fix Unity ScriptableObject not persisting runtime changes. You store player gold in a ScriptableObject called PlayerData. Edit in play mode, the value updates. Close Unity, reopen, the value is still there — great. Build the game, play, earn gold, quit, relaunch: gold is reset to the starting value. The ScriptableObject that seemed like the perfect save system is editor-only. This catches many developers.
The Symptom
A ScriptableObject modified at runtime appears to save correctly in the Unity editor (changes survive entering and exiting play mode, survive editor restarts, visible in the Inspector). The same asset in a built game loses changes on application quit. On next launch the ScriptableObject’s values are whatever you shipped with.
Variant: in the editor, play mode changes sometimes persist and sometimes do not (depends on whether Unity saved before quit). Confusingly inconsistent behavior because the editor autosaves at unpredictable times.
What Causes This
Built player assets are read-only. In a build, ScriptableObject assets are serialized into binary data and loaded at runtime. The binary is not writable back to disk by design. Any runtime modifications live only in memory.
Editor illusion. In the editor, a ScriptableObject IS the actual asset file. Modifying fields via code modifies the asset. Unity’s serialization detects the dirty state and writes on exit. This makes ScriptableObjects feel like a persistent database — but only in the editor.
Domain reload clears static references. Even in the editor, disabling Enter Play Mode Options > Reload Domain can cause static references to persist between play sessions. Combined with ScriptableObject persistence, developers assume “it just saves” when actually they’re seeing editor-only behavior that will break in builds.
The Fix
Step 1: Separate data shape from data persistence. Use ScriptableObjects as the schema and loader, not the storage. The actual runtime state goes in JSON/binary files.
using UnityEngine;
using System.IO;
[CreateAssetMenu(fileName = "PlayerDataConfig", menuName = "Config/Player Data")]
public class PlayerDataConfig : ScriptableObject
{
// Default values shipped with the game (read-only at runtime)
public int StartingGold = 100;
public int StartingHealth = 100;
}
[System.Serializable]
public class PlayerDataSave
{
public int Gold;
public int Health;
}
public class PlayerSaveManager : MonoBehaviour
{
[SerializeField] private PlayerDataConfig defaults;
public PlayerDataSave CurrentData;
private string SavePath => Path.Combine(Application.persistentDataPath, "player.json");
void Awake()
{
if (File.Exists(SavePath))
{
string json = File.ReadAllText(SavePath);
CurrentData = JsonUtility.FromJson<PlayerDataSave>(json);
}
else
{
CurrentData = new PlayerDataSave
{
Gold = defaults.StartingGold,
Health = defaults.StartingHealth
};
}
}
public void Save()
{
string json = JsonUtility.ToJson(CurrentData, true);
File.WriteAllText(SavePath, json);
}
void OnApplicationQuit() => Save();
}
ScriptableObject holds defaults. JSON file holds runtime state. Saves work in both editor and build. Clean separation makes testing easier too.
Step 2: If you need editor-time persistence specifically, call SaveAssetIfDirty. For editor tools that modify ScriptableObjects (level editors, configuration UIs), explicitly save:
#if UNITY_EDITOR
using UnityEditor;
#endif
public void ModifyAndSave()
{
myScriptableObject.SomeField = newValue;
#if UNITY_EDITOR
EditorUtility.SetDirty(myScriptableObject);
AssetDatabase.SaveAssetIfDirty(myScriptableObject);
#endif
}
This only compiles in editor. Runtime build code that touches AssetDatabase fails to compile. The #if UNITY_EDITOR guard is mandatory.
Step 3: Use PlayerPrefs for simple settings. For a handful of settings (volume, resolution, language), PlayerPrefs is simpler than JSON:
PlayerPrefs.SetInt("Gold", currentGold);
PlayerPrefs.Save();
// Later:
int gold = PlayerPrefs.GetInt("Gold", defaultGold);
PlayerPrefs is cross-platform, persists reliably, and survives application quit. Not suitable for large datasets (>1 MB) or complex structures, but perfect for simple values.
Step 4: Use Application.persistentDataPath for writable files. This is the platform-specific path where your game can write data. On Windows it is %APPDATA%\..\LocalLow\CompanyName\GameName, on macOS ~/Library/Application Support, on mobile per-app sandboxed storage. Writes here persist across sessions and are included in OS backups.
Why This Trap Exists
ScriptableObjects are designed for sharing data between scripts and scenes — a shared inventory definition, a list of weapons, audio configuration. They are serialized into builds as read-only data. Unity never intended them as a save system, but their editor behavior makes developers think they work that way.
The community has tools like Odin and ScriptableObject-based architectures (the “Ryan Hipple talk”) that can blur the line, but fundamentally SOs are shipping assets, not user data.
“ScriptableObjects are for what you know at build time. Player state is not known at build time. Use files.”
Related Issues
For ScriptableObject singleton issues, see ScriptableObject Singleton Null After Build. For SO data lost on enter/exit play mode, ScriptableObject Data Lost in Play Mode covers related gotchas.
SO for config, JSON for state. Persist to Application.persistentDataPath.