Quick answer: Wrap edits in Undo.RecordObject + EditorUtility.SetDirty, then AssetDatabase.SaveAssetIfDirty when you want bytes on disk. For Play Mode mutations you intend to keep, use a custom inspector button that copies the current state back to the asset.

You wired up a tool that edits values on a ScriptableObject. The edits look right. Then a script recompiles, or you exit Play Mode, and every change is gone. Unity is doing exactly what it is supposed to do; the asset was never marked dirty.

The Symptom

You hit a button on a custom inspector or run a tool script. Fields visibly update in the Inspector. Five minutes later, after you click somewhere else, the values are back to what they were before. Or you mutate the asset from a runtime script in Play Mode and the changes vanish on stop.

What Causes This

Unity serializes assets only when the Editor decides they are dirty. Setting a field via reflection, SerializedProperty, or direct assignment does not by itself flip that flag — you have to do it explicitly. On a domain reload (script recompile, assembly definition change, exit Play Mode with reload enabled), Unity reloads the asset from disk and any unsaved in-memory mutations are discarded.

The Fix in Editor Tools

using UnityEditor;
using UnityEngine;

public static class EnemyConfigEditor
{
    public static void SetHealth(EnemyConfig cfg, int hp)
    {
        Undo.RecordObject(cfg, "Set Enemy Health");
        cfg.health = hp;
        EditorUtility.SetDirty(cfg);
        AssetDatabase.SaveAssetIfDirty(cfg);
    }
}

The four lines do four different jobs:

If you skip SaveAssetIfDirty, the change persists across script recompiles but only writes to disk when Unity next saves the project. Usually fine; for tools that operate on many assets, prefer a single AssetDatabase.SaveAssets() at the end.

The Fix for SerializedProperty Edits

If you are editing through a SerializedObject in a custom inspector, the right calls are serializedObject.ApplyModifiedProperties(). ApplyModifiedProperties calls SetDirty internally, so you do not need a separate SetDirty.

SerializedObject so = new SerializedObject(target);
so.FindProperty("health").intValue = 100;
so.ApplyModifiedProperties();   // dirties + records undo

Play Mode Is Different

In Play Mode, runtime mutations to a ScriptableObject persist until the domain reloads. Exiting Play Mode reloads the domain by default, which reverts the asset. To capture state from a Play Mode session:

  1. Add an inspector button: “Capture Runtime Values”.
  2. While in Play Mode, click it. The button reads the live fields and calls SetDirty + SaveAssetIfDirty so the asset is serialized before the reload.

Don’t Mutate ScriptableObjects in Builds

Once shipped, ScriptableObjects are read-only data. The asset bytes live in a binary blob; writing to them in a player has no effect across sessions and may quietly corrupt other instances that share the same backing memory. For runtime state, use a separate save system.

“SetDirty marks. SaveAssetIfDirty writes. ApplyModifiedProperties does both. Without one of these, every edit is in-memory only.”

Related Issues

For lost prefab edits, see prefab changes not saving. For undo behavior, see undo in custom inspector.

RecordObject. SetDirty. SaveAssetIfDirty. The bytes hit disk.