Quick answer: Unlike MonoBehaviour components on scene objects, ScriptableObjects are project-level assets. When you modify a ScriptableObject during Play Mode, you are modifying the actual asset on disk. Unity does not revert asset changes when exiting Play Mode — only scene object changes are reverted.
Here is how to fix Unity ScriptableObject data lost play mode. You set up a ScriptableObject to hold your game's inventory data, enemy stats, or player progression. During Play Mode in the editor, everything works — values update as expected. But then something unexpected happens: either the values persist after you exit Play Mode (corrupting your design-time data), or they vanish completely in a build. ScriptableObjects have fundamentally different serialization behavior than MonoBehaviours, and understanding this distinction is the key to using them correctly.
The Symptom
You modify a ScriptableObject's fields during Play Mode — for example, decrementing a health value or adding items to an inventory list. When you exit Play Mode, you expect the values to revert to their original state, just like MonoBehaviour fields on scene objects. Instead, the modified values persist. Your enemy now has 0 health in the Inspector. Your inventory asset is full of items that were added during testing.
The opposite problem appears in builds: you modify ScriptableObject data during gameplay, quit the application, and relaunch — all changes are gone. The ScriptableObject resets to whatever values it had when the build was made. Data that seemed to save in the editor does not save at all in a standalone build.
What Causes This
The root cause is that ScriptableObjects are project assets, not scene objects. Unity's Play Mode serialization system only backs up and restores scene data. This creates two problems:
- Editor Play Mode: changes persist — When you modify a ScriptableObject at runtime in the editor, you are directly modifying the asset file. Unity does not snapshot or restore asset data when entering and exiting Play Mode. This is by design — it allows tools like level editors to save data during Play Mode — but it catches most developers off guard.
- Builds: changes are lost — In a compiled build, ScriptableObjects are serialized into the game data and are effectively read-only. You can modify them in memory during a session, but those changes are never written back to disk. When the application restarts, the original baked-in data is loaded.
- Instantiate vs direct reference — If multiple MonoBehaviours reference the same ScriptableObject asset and one of them modifies it, all of them see the change immediately (because they all point to the same object in memory). This is useful for shared state, but dangerous if you did not intend to share mutations.
- Collections and nested references — Lists, arrays, and nested objects inside ScriptableObjects follow the same rules. Adding or removing items from a List field during Play Mode permanently modifies the asset in the editor.
The Fix
Step 1: Understand when to use ScriptableObjects as read-only data. The safest pattern is to treat ScriptableObjects as immutable configuration data — define values at design time and never modify them at runtime. Use them for things like weapon definitions, enemy stat templates, level configuration, and dialogue trees.
using UnityEngine;
// A read-only data definition — never modify at runtime
[CreateAssetMenu(fileName = "NewWeapon", menuName = "Game/Weapon Definition")]
public class WeaponDefinition : ScriptableObject
{
public string weaponName;
public int baseDamage;
public float attackSpeed;
public float range;
public Sprite icon;
}
// Runtime weapon instance that can be modified safely
public class WeaponInstance
{
public WeaponDefinition definition;
public int currentDamage;
public int upgradeLevel;
public WeaponInstance(WeaponDefinition def)
{
definition = def;
currentDamage = def.baseDamage;
upgradeLevel = 0;
}
public void Upgrade()
{
upgradeLevel++;
currentDamage = definition.baseDamage + (upgradeLevel * 5);
}
}
Step 2: Use runtime copies to protect original data. If you need to modify ScriptableObject data during gameplay, clone the asset at startup using Instantiate(). The clone lives only in memory and will not affect the original asset. This works identically in the editor and in builds.
using UnityEngine;
[CreateAssetMenu(fileName = "NewPlayerStats", menuName = "Game/Player Stats")]
public class PlayerStats : ScriptableObject
{
public int maxHealth = 100;
public int currentHealth = 100;
public int attackPower = 10;
public int gold = 0;
}
public class PlayerController : MonoBehaviour
{
[SerializeField] private PlayerStats baseStats;
// Runtime copy — safe to modify
private PlayerStats _runtimeStats;
public PlayerStats Stats => _runtimeStats;
private void Awake()
{
// Clone the asset so we never modify the original
_runtimeStats = Instantiate(baseStats);
_runtimeStats.name = baseStats.name + " (Runtime)";
}
public void TakeDamage(int damage)
{
// Modifies the clone, not the asset
_runtimeStats.currentHealth -= damage;
if (_runtimeStats.currentHealth < 0)
_runtimeStats.currentHealth = 0;
Debug.Log("Health: " + _runtimeStats.currentHealth);
}
private void OnDestroy()
{
// Clean up the runtime clone
if (_runtimeStats != null)
Destroy(_runtimeStats);
}
}
Step 3: Persist data in builds with JSON serialization. Since ScriptableObjects are read-only in builds, you need an external save system for any data that should survive between sessions. The simplest approach is to serialize to JSON and write to Application.persistentDataPath.
using UnityEngine;
using System.IO;
public class SaveManager : MonoBehaviour
{
[SerializeField] private PlayerStats playerStats;
private string SavePath =>
Path.Combine(Application.persistentDataPath, "player_save.json");
public void SaveGame()
{
// Serialize the ScriptableObject to JSON
string json = JsonUtility.ToJson(playerStats, prettyPrint: true);
File.WriteAllText(SavePath, json);
Debug.Log("Game saved to: " + SavePath);
}
public void LoadGame()
{
if (!File.Exists(SavePath))
{
Debug.Log("No save file found. Using default values.");
return;
}
string json = File.ReadAllText(SavePath);
// Overwrite the ScriptableObject fields with saved data
JsonUtility.FromJsonOverwrite(json, playerStats);
Debug.Log("Game loaded. Health: " + playerStats.currentHealth);
}
public void DeleteSave()
{
if (File.Exists(SavePath))
{
File.Delete(SavePath);
Debug.Log("Save file deleted.");
}
}
}
For a more robust approach, combine the runtime copy pattern with JSON persistence. Clone the ScriptableObject on startup, load any saved data into the clone, modify the clone during gameplay, and save the clone's data on quit or at checkpoints. This gives you the best of both worlds: the original asset stays clean, and player progress persists across sessions.
using UnityEngine;
using System.IO;
public class PersistentStatsManager : MonoBehaviour
{
[SerializeField] private PlayerStats defaultStats;
private PlayerStats _runtimeStats;
public PlayerStats Stats => _runtimeStats;
private string SavePath =>
Path.Combine(Application.persistentDataPath, "stats.json");
private void Awake()
{
// 1. Clone the default asset
_runtimeStats = Instantiate(defaultStats);
// 2. Overwrite with saved data if available
if (File.Exists(SavePath))
{
string json = File.ReadAllText(SavePath);
JsonUtility.FromJsonOverwrite(json, _runtimeStats);
}
}
private void OnApplicationQuit()
{
// 3. Save runtime data on quit
string json = JsonUtility.ToJson(_runtimeStats);
File.WriteAllText(SavePath, json);
}
}
Related Issues
See also: Fix: Unity TextMeshPro Text Not Showing or Rendering.
See also: Fix: Unity Raycast Not Hitting Any Colliders.
Clone ScriptableObjects at runtime; serialize to JSON for build persistence.