Supporting Undo in Unity Editor scripts

date
Aug 23, 2023
type
KnowledgeBase
year
slug
serializedobject
status
Published
tags
Unity
Undo
Serialization
summary
There’s 2 ways to do this: Via Serialization or via the Undo class. Let’s have a look at both!
ℹ️
There's 2 ways to do this: Via Serialization or via the Undo class.
  • The 🏆 recommended way is to work with Serialization, then Undo is more or less automatically handled for you whenever you apply changes to a SerializedObject. In addition this ensures that serialized fields are automatically dirtied and styled correctly for Prefab overrides.
  • The other option is to use the Undo class and create Undo-steps manually before you change an Object. For newly created objects you create a creation-undo-step after you created the new object. - Only use this in emergencies.

Undo via Serialization

Undo will be handled almost automatically for you if you work with SerializedObjects and SerializedPropertys.
💬
What’s a SerializedObject? A class for editing serialized fields on Unity objects in a completely generic way.
Usually all you have to do is this:
  1. ⬇️ Call Update() on the SerializedObject to make sure it’s got all the latest values from the actual object
  1. 🔄 Edit the SerializedPropertys of the SerializedObject
  1. ⬆️ Call ApplyModifiedProperties() on the SerializedObject, so that the changes get written back to the actual object.
Looks like this in code:
serializedObject.Update(); // Update SerializedObject // Do stuff with the serialized object EditorGUILayout.PropertyField(serializedObject.FindProperty("m_MyField")); // Maybe disaplay a PropertyField for the user to edit serializedObject.FindProperty("m_Value").floatValue = 666f; // Or change something directly serializedObject.ApplyModifiedProperties(); // Apply modifications

Working with SerializedObject and SerializedProperty

Getting a SerializeObject

If you make a custom Inspector for an object you will automatically get a serializedObject to work with.
[CanEditMultipleObjects] [CustomEditor(typeof( Spaceship ), true)] public class SpaceshipEditor : Editor { public override void OnInspectorGUI() { serializedObject.Update(); // <--- Ready to use } }
Otherwise you can create a SerializedObject for any Object like this:
SerializedObject so = new SerializedObject( transform );

Getting SerializedPropertys from a SerializedObject

To change values on a SerializedObject we need to get them as SerializedPropertys. - This is a generic way to deal with serialized objects, remember. - So what we have to do is call FindProperty:
SerializedProperty sProp = serializedObject.FindProperty( "m_Value" ); SerializedProperty sPropStr = serializedObject.FindProperty( "m_TargetName" );
As you see we need to know the field name for this. You can Shift+RMB any field in the inspector and click on “Print Property Path” to log that field’s name into the console.
 
Or you can switch the inspector to Debug mode and hold the Alt key for a while - then all field names will show up.
 
notion imagenotion image
notion imagenotion image
To change the value of a SerializedProperty you kinda need to know it’s type. Every SerializedProperty has fields for stringValue, boolValue, floatValue, intValue, rectValue, vector3Value, animationCurveValue, etc., etc.
If you don’t know the type for some reason you can look it up via sProp.type (a string) or sProp.propertyType (to compare against the SerializedPropertyType enum)
Further Reading: 🌐 → SerializedProperty Docs
 
And one more cool thing: You can change multiple things at once! If you turn an Array into a SerializedObject and change one of its properties it will update all the objects. Even for different kinds of objects (at least as long as they all have that same field that you’re changing…)
Here’s the example from the Unity Docs:
var transforms = Selection.gameObjects.Select( go => go.transform ).ToArray(); var so = new SerializedObject( transforms ); so.FindProperty( "m_LocalPosition" ).vector3Value = Vector3.zero; so.ApplyModifiedProperties();

Undo via Undo Class

💬
Lets you register undo operations on specific objects before you change them.
Change some properties:
Undo.RecordObject( transform, "Change some stuff" ); transform.position = Vector3.zero; transform.eulerAngles = new Vector3( 0f, 90f, 0f );
Add a component:
Undo.AddComponent<Rigidbody>( gameObject );
Reparent Transform:
Undo.SetTransformParent( transform1, parentToTransform, "Set new parent" );
Create GameObject:
var go = new GameObject( "Buster" ); Undo.RegisterCreatedObjectUndo( go, "Create GameObject" );
Destroy GameObject (or component):
Undo.DestroyObjectImmediate( target );
You can also use this class to Clear the Undo and Redo stacks or to perform Undo and Redo operations via script.
Undo.PerformUndo(); Undo.PerformRedo(); Undo.ClearUndo(); Undo.ClearAll(); // Clear Undo and Redo
There’s also stuff in there for grouping undo operations together under a single name, etc. 🌐 → Undo Docs
 
Random Sidenote - I’ve had some fun in the past trying to get it to register changes to a ScriptableObject. I had to Undo + SetDirty + AssetDatabase.SaveAssets before it would actually show up as changed…
Undo.RegisterCompleteObjectUndo( Style, "Adjust Style" ); EditorGUI.BeginChangeCheck(); Style.Style.border.left=EditorGUILayout.IntField("Left",Style.Style.border.left); Style.Style.border.right=EditorGUILayout.IntField("Right",Style.Style.border.right); Style.Style.border.top=EditorGUILayout.IntField("Top",Style.Style.border.top); Style.Style.border.bottom=EditorGUILayout.IntField("Bottom",Style.Style.border.bottom); if(EditorGUI.EndChangeCheck()){ EditorUtility.SetDirty(Style); AssetDatabase.SaveAssets(); }

Leave a comment