- Home /
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?
What version of Unity are you using?
I tested with both 4.6 and 5, and the method Undo.RegisterObject does not exist in any of them. It's not in the API. Undo.RecordObject is a thing, though.
Are those typos, or are you using a seriously old version of Unity? I'm suspecting typos, as your code isn't even close to compiling ("Class" ins$$anonymous$$d of "class", properties lacking types, etc.), but I want to make sure.
So I wrote pseudocode off the top of my head. I totally had all of the typos in there. I have updated the question to use actual code. I'm using Unity 4.3. Doing so because of console publishing.
Although I was able to get the int array recording with my example, I still don't know exactly what's going on with this. Why does RecordObject require me to be within one object? Why am I able to create scenarios where it will record an undo step, but not restore the changes on Undo? I want to know exactly how this function works.
I've been tweaking things and I've made some progress. It seems that a large part of my issue is that I'm editing prefabs, and not instances in the scene. These seem to provide different results. In my case, I want to edit prefabs. $$anonymous$$aybe there is a command that saves prefab changes to the Undo stack?
You're probably right in that this is a prefab issue. It might also be a bug.
I'll see if I can look into it - we might run into this issue later ourselves. Do you have some working example code?
Here's an example to reproduce the error. Create these three classes, with Foo and TestEditor in your Editor folder and SerializedClass outside.
using System;
using UnityEngine;
using UnityEditor;
public class FooWindow : EditorWindow {
Foo fooClass;
[$$anonymous$$enuItem("Window/Test Editor")]
static void ShowEditor(){
FooWindow editor = EditorWindow.GetWindow<FooWindow>();
editor.title = "Test Editor";
}
void OnSelectionChange(){
if(Selection.activeGameObject.GetComponent<SerializedClass>() != null){
SerializedClass sClass = Selection.activeGameObject.GetComponent<SerializedClass>();
fooClass = ScriptableObject.CreateInstance<Foo>();
fooClass.Init(sClass);
} else {
fooClass = null;
}
}
void OnGUI(){
if(fooClass != null){
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;
// TODO: Find out how to save the prefab
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 : $$anonymous$$onoBehaviour {
[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; }
}
}
When this is done create a new gameobject in the scene. Then copy this to your project as a prefab. Open up the Test Editor from the Window menu. If you set Bar to true on the instance in the scene, it will undo properly. If you select the prefab, it will not undo. I'm going to keep looking into saving prefabs. Alternatively, a workaround might be to instantiate the objects while working on this, but that's not great for my program's intended flow.
Answer by SquarePieStudios · Jul 15, 2015 at 09:38 AM
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 :/
Answer by oferei · Nov 02, 2016 at 01:13 PM
I just found out that Undo.RecordObject doesn't work on ScriptableObject, whereas EditorUtility.SetDirty does.
I am also having problems getting Undo.RecordObject to work on a ScriptableObject. Problem is that EditorUtility.SetDirty doesn't record undo states.
Answer by IgorAherne · Jul 19, 2017 at 10:13 PM
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, unlikeUndo.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;
}
}
Answer by PicklesIIDX · Nov 02, 2016 at 03:32 PM
Since working on this I've learned about the command pattern, which, in this example (http://gameprogrammingpatterns.com/command.html), talks about a method to implement undo with a lot more control.