In Unity development, ScriptableObject is an essential feature for managing configuration data, master tables, asset reference lists, etc. However, have you ever faced the following troubles when rewriting ScriptableObject data from your own "EditorWindow", "Custom Editor", or batch processing C# script executed from the editor menu?
- "The values changed in the inspector, but when I restarted Unity, they returned to the original values."
- "The moment I pressed the play button (Play Mode), all the values were emptied and reset."
- “The game is working correctly in the editor, but the changes are not reflected in the built game.”
In this article, we will explain the internal mechanism of the “data not saving problem” that beginners in editor development always face, and We will explain the correct usage and implementation code of ng>EditorUtility.SetDirty, Undo.RecordObject, and AssetDatabase.SaveAssets.
1. Root cause: "Out of synchronization" between memory and disk.
The biggest reason for this problem is the specification that ``Unity editor does not synchronize the rewriting of assets in memory and the writing to disk (.asset file).''
Assets in the Unity editor (ScriptableObjects, Prefabs, Materials, etc.) are read from disk when Unity starts and are expanded as C# objects in memory. If you change the field value from a C# script or editor extension, the value in memory will be rewritten. Therefore, the inspector display will also be updated with the new value.
However, in order to minimize the disk access load, Unity does not automatically write assets that have been modified in memory back to disk.
If the following events occur in this state, the "unsaved data" in memory will be discarded and overwritten (initialized) with old data on disk.
- Entering replay mode: Domain Reload runs, reloads C# assemblies, and rebuilds objects in memory from serialized data on disk.
- Restart editor: Memory is freed and loaded from disk (.asset) on next startup.
- Building the project: Unity packages the asset files on disk as is, so the latest data in memory is ignored.
To prevent this problem, you need to explicitly notify the Unity editor that ``This asset has been modified (became dirty), please mark it for saving'' and serialize it to disk.
2. Solution steps: Three saving processes and how to write them in C# code
To save data reliably, use a combination of APIs that perform the following three roles.
| API name | Main role | When to use it |
|---|---|---|
Undo.RecordObject |
Records the previous state, supports Ctrl+Z undo, and starts tracking changes to the asset. | When the user interacts with values in the editor, such as the Inspector or EditorWindow. |
EditorUtility.SetDirty |
Tells the editor that the asset has been "modified (became dirty)". As a result, it will be automatically saved when Unity closes or when saving a scene. | On any asset change (required even for automatic processing scripts that do not use Undo). |
AssetDatabase.SaveAssets |
Immediately writes all changed assets marked as dirty to disk (.asset). | Scripts that do not rely on the editor's UI automatic updates, such as import processing, pre-build processing, and batch processing from the command line. |
Pattern A: When changing manually from a self-made EditorWindow or GUI (recommended)
When a human changes a value by operating an EditorWindow or a custom inspector, be sure to call Undo.RecordObject before changing the value, and call EditorUtility.SetDirty after changing it.
using UnityEngine;
using UnityEditor;
public class MyDataEditorWindow : EditorWindow
{
public MyGameConfig config; // Target ScriptableObject
[MenuItem("Window/Config Editor")]
public static void ShowWindow()
{
GetWindow("Config Editor");
}
void OnGUI()
{
if (config == null)
{
config = EditorGUILayout.ObjectField("Config", config, typeof(MyGameConfig), false) as MyGameConfig;
return;
}
// 1. Record the value before change in the editor's undo system (target object in the first argument, operation name in the second argument)
// *By calling this method, the object will be automatically marked for serialization
EditorGUI.BeginChangeCheck();
string newName = EditorGUILayout.TextField("Player Name", config.playerName);
int newHp = EditorGUILayout.IntField("Max HP", config.maxHp);
if (EditorGUI.EndChangeCheck())
{
// 2. If a change occurs, record it so that it can be reverted before actually applying the value.
Undo.RecordObject(config, "Modify Game Config");
// Reflect the value
config.playerName = newName;
config.maxHp = newHp;
// 3. Mark the editor as changed (add to disk export queue)
EditorUtility.SetDirty(config);
Debug.Log("Config updated and marked dirty.");
}
}
}
Undo.RecordObject? Simply calling
SetDirty will save the value change, but you will not be able to undo it with Ctrl+Z. Additionally, Undo.RecordObject internally tracks that the current object is scheduled to be modified, which provides a double safeguard to prevent serialization from being leaked from memory.
Pattern B: When automatically changing from a batch script such as during build preprocessing or importing.
Undo.RecordObject is not necessary for automatic build scripts or batch scripts that do not require interactive operations (Undo). However, since it must be reflected on disk immediately after processing, AssetDatabase.SaveAssets must be called explicitly.
using UnityEngine;
using UnityEditor;
public class BuildAutomation
{
[MenuItem("Tools/Auto Update Config Before Build")]
public static void UpdateConfig()
{
// Load ScriptableObject from asset path
MyGameConfig config = AssetDatabase.LoadAssetAtPath("Assets/Data/GameConfig.asset");
if (config != null)
{
// 1. Assign value directly
config.buildVersion = "1.2.3b";
config.isReleaseBuild = true;
// 2. Mark dirty (notify editor of changes)
EditorUtility.SetDirty(config);
// 3. Immediately write all dirty marked assets to disk (.asset)
AssetDatabase.SaveAssets();
Debug.Log("GameConfig saved successfully for build.");
}
}
}
3. "The smartest way to write" in the Custom Inspector (Editor)
When customizing the Inspector screen for MonoBehaviour or ScriptableObject (CustomEditor), the safest and smartest way is to use SerializedObject and SerializedProperty.
With this method, instead of assigning values directly to the fields of the C# object, you rewrite the values via Unity's serialized properties. As a result, Undo.RecordObject and EditorUtility.SetDirty are automatically executed behind the scenes, 100% preventing data loss due to forgetting to call them.
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(MyGameConfig))]
public class MyGameConfigEditor : Editor
{
SerializedProperty playerNameProp;
SerializedProperty maxHpProp;
void OnEnable()
{
// Get property when inspector drawing starts
playerNameProp = serializedObject.FindProperty("playerName");
maxHpProp = serializedObject.FindProperty("maxHp");
}
public override void OnInspectorGUI()
{
// 1. Load the latest serialized data into memory and synchronize
serializedObject.Update();
// 2. Draw a GUI field according to the property
EditorGUILayout.PropertyField(playerNameProp);
EditorGUILayout.PropertyField(maxHpProp);
// 3. If there is a change in the GUI, automatically register Undo and update the asset with a dirty mark
serializedObject.ApplyModifiedProperties();
}
}
This serializedObject.ApplyModifiedProperties() approach is also the most robust option when changing the number of elements in a list (Array) or safely rewriting a complex nested structure (Serializable class). As a general rule, use this approach for inspector extensions.