What are the limitations of Undo.RecordObject?

So I’m having trouble understanding exactly how Undo works in custom editor windows. Here is the progress I’ve made:

I’ve discovered that although I can register undo events for objects that change, the undo command will not always revert those changes. For example, any serialized field of a member that belongs to an instance of a class does not seem to revert.

EDIT: The first post was pseudocode off of the top of my head. I’ve rebuilt this to provide as a compilable example:

using System;
using UnityEngine;
using UnityEditor;

public class FooWindow : EditorWindow {

	[SerializeField]
	Foo fooClass;

	[MenuItem("Window/Test Editor")]
	static void ShowEditor(){
		FooWindow editor = EditorWindow.GetWindow<FooWindow>();
		editor.title = "Test Editor";
		editor.Init();
	}

	void Init(){
		fooClass = CreateInstance<Foo>();
	}

	void OnGUI(){
		EditorGUI.BeginChangeCheck();
		bool toggle = GUILayout.Toggle(fooClass.SClass.Bar, "Bar");
		if(EditorGUI.EndChangeCheck()){
			Debug.LogWarning("Saved");
			Undo.RecordObject(fooClass, "Set bar");
			fooClass.SClass.Bar = toggle;
			EditorUtility.SetDirty(fooClass);
		}
	}
	
}

using UnityEngine;
using UnityEditor;
using System.Collections;

public class Foo : Editor {

	[SerializeField]
	SerializedClass sClass;
	public SerializedClass SClass {
		get { return sClass; }
		set { sClass = value; }
	}

	public Foo(){
		sClass = new SerializedClass();
	}
}


using UnityEngine;
using System.Collections;

[System.Serializable]
public class SerializedClass {
	
	[SerializeField]
	bool bar = false;
	public bool Bar {
		get { return bar; }
		set { bar = value; }
	}
}

This will record the undo action, but will not revert it. Instead, If I move the undo code into the Foo class and then create another serialized variable within the Foo class and change that value, which I then would force the SerializedClass’s Bar variable to, it will correctly undo the change. EDIT: I put together a small example here and am able to get both a boolean and an int array tracked through undo.

using System;
using UnityEngine;
using UnityEditor;

public class FooWindow : EditorWindow {

	[SerializeField]
	Foo fooClass;

	[MenuItem("Window/Test Editor")]
	static void ShowEditor(){
		FooWindow editor = EditorWindow.GetWindow<FooWindow>();
		editor.title = "Test Editor";
		editor.Init();
	}

	void Init(){
		GameObject go = new GameObject();
		SerializedClass sClass = go.AddComponent<SerializedClass>();
		fooClass = ScriptableObject.CreateInstance<Foo>();
		fooClass.Init(sClass);
	}

	void OnGUI(){
		fooClass.UpdateValues();
	}
	
}

using UnityEngine;
using UnityEditor;
using System.Collections;

public class Foo : Editor {
	
	SerializedClass sClass;
	public SerializedClass SClass {
		get { return sClass; }
		set { sClass = value; }
	}

	public void Init(SerializedClass newSClass){
		sClass = newSClass;
	}

	public void UpdateValues(){
		EditorGUI.BeginChangeCheck();
		bool toggle = GUILayout.Toggle(sClass.Bar, "Bar");
		if(EditorGUI.EndChangeCheck()){
			Debug.LogWarning("Saved");
			Undo.RecordObject(sClass, "Set bar");
			sClass.Bar = toggle;
			EditorUtility.SetDirty(sClass);
		}

		if(GUILayout.Button("Array")){
			Debug.LogWarning("adding...");
			Undo.RecordObject(sClass, "Add Int");
			int[] intArray = sClass.IntArray;
			System.Array.Resize<int>(ref intArray, sClass.IntArray.Length + 1);
			sClass.IntArray = intArray;
			EditorUtility.SetDirty(sClass);
		}

	}
}


using UnityEngine;
using UnityEditor;
using System.Collections;

public class SerializedClass : MonoBehaviour {
	
	[SerializeField]
	bool bar = false;
	public bool Bar {
		get { return bar; }
		set { bar = value; }
	}

	[SerializeField]
	int[] intArray = new int[0];
	public int[] IntArray{
		get { return intArray; }
		set { intArray = value; }
	}
}

So, there is something about the Undo class that I don’t understand. Why doesn’t it restore properties of serialized values within a class? EDIT: in setting up this example I was able to get the int array working. But a similar setup doesn’t work in my project. So there is something about what will actually be recorded and what won’t that I’m not understanding. So my question is: how exactly does Undo.RecordObject() determine whether it will record and restore a change with the undo command?

I found that if I used RecordObject on the gameObject instead of the component instance, it worked just fine. Dumb…but I guess it works :confused:

I just found out that Undo.RecordObject doesn’t work on ScriptableObject, whereas EditorUtility.SetDirty does.

I found this post by complete accident:

quoting it in case it’s deleted:

Undo.RegisterCompleteObjectUndo(Object objectToUndo, string name) will record a copy of the full state of the object that it will keep, unlike Undo.RecordObject(Object objectToUndo, string name) that will only keep a
copy of the state until the end of the
frame to compute a diff.
.
By the way, I just got the following
error that drove me to find more on
this method:

Generating diff of this object for
undo because the type tree changed.
This happens if you have used
Undo.RecordObject when changing the
script property. Please use
Undo.RegisterCompleteObjectUndo
So RegisterCompleteObjectUndo seems important in complex situations were a diff is not possible.

Couple of words from myself:

Undo.RegisterCompleteObjectUndo seems to work with Event and editor-GUI as well every time. And RecordObject seemed only to work sometimes (only worked occasionally)

Additionally, if you are working with Event in editor script, and actually have code that Use()s the GUI event, chances are the event might not get through to unity’s undo system.
You might need to call Undo.FlushUndoRecordObjects(); after RegisterCompleteObjectUndo()

.

Don’t forget to use EditorUtility.SetDirty() on the object, AFTER you’ve recoreded into Undo

Here is an example, allowing me to offset 2D viewport up/down left/right:

void DragViewingArea() {
            int id = GUIUtility.GetControlID(6, FocusType.Passive);
 
            //prepare for drag:
            if (_currentEvent.type == EventType.MouseDown && _currentEvent.button == 2) {
                Undo.RegisterCompleteObjectUndo(_fsm, "begin drag FSM viewport on " + _fsm.gameObject.name);
                Undo.FlushUndoRecordObjects();
             
                GUIUtility.hotControl = id;
                _currentEvent.Use();
                return;
            }
 
            //drag:
            if(_currentEvent.type == EventType.MouseDrag && GUIUtility.hotControl == id) {
                _fsm._fsmEditorWindow_TableOffset += _currentEvent.delta;
                _currentEvent.Use();
                EditorUtility.SetDirty(_fsm); //set dirty as soon as we actually drag
 
                return;
            }
 
            //finished dragging
            if (_currentEvent.type == EventType.mouseUp  &&  _currentEvent.button == 2  &&  GUIUtility.hotControl == id) {
                GUIUtility.hotControl = 0;
                _currentEvent.Use();
                return;
            }
 
        }

Since working on this I’ve learned about the command pattern, which, in this example (Command · Design Patterns Revisited · Game Programming Patterns), talks about a method to implement undo with a lot more control.