- Home /
Catch a unity undo/redo event.
Say I have a script like this :
[ExecuteInEditMode]
public class RandomScript : Monobehaviour
{
//Private array.
[SerializeField]
private float[] _numbers;
//Public property so I'm sure that DoSomething() is executed when someone sets the float array.
public float[] Numbers
{
get { return _numbers; }
set
{
_numbers = value;
DoSomething();
}
}
//Some heavy calculations that need to be done after adjusting the float array.
DoSomething()
{
//Initialize stuff from _numbers;
}
}
}
I put the field private, but serializable, so unity remembers the values between sessions, and a public property to make sure stuff is done after the float array is adjusted. In my custom inspector script I implemented an undo state (Undo.RegisterUndo(targetObject, "Description")). Now, whenever the user selects the undo operation in unity, unity puts the old float array back in the private field (so it doesn't use the public property), so the DoSomething() method isn't called and my other data isn't updated after the undo. Same for redo.
The only solution is see atm is calling DoSomething() in every update, but that's a heavy operation (and bad code design in the first place).
Is there any way to catch an undo or redo event from unity? Some event to subscribe to or a method to implement that unity will call (like Awake(), Start(), OnEnable(), ...)?
Answer by MrPhil · Jun 24, 2013 at 11:53 PM
I see two approaches you can take.
First, is the a no CustomEditor approach which I think is better because it involves less code:
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class RandomScript : MonoBehaviour
{
//Private array.
[SerializeField]
public float[] _numbers;
//Public property so I'm sure that DoSomething() is executed when someone
//sets the float array.
public float[] Numbers
{
get { return _numbers; }
set
{
_numbers = value;
DoSomething();
}
}
void Update()
{
EditorUpdate();
}
//Some heavy calculations that need to be done after adjusting
//the float array.
public void DoSomething()
{
//Initialize stuff from _numbers;
Debug.Log("DoSomething() called!");
}
public void EditorUpdate()
{
#if UNITY_EDITOR // Only when in the editor
if (!Application.isPlaying)
{
// It is possible my numbers changed
DoSomething();
}
#endif
}
}
By using the Preprocessor Directive #if UNITY_EDITOR you make sure the code is removed from builds. The check for Application.isPlaying make sure the code is run only due to an Update call by the Editor:
Update is only called when something in the scene changed. - ScriptReference: ExecuteInEditMode
So, it isn't as heavy handed as it looks.
Second approach uses the Event types to look for the special Undo/Redo command, see bottom of the OnInspectorGUI() method:
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(RandomScript))]
public class RandomScriptEditor : Editor
{
// Our serialized array of floats on the RandomScript
public SerializedProperty _numbers;
// This is for formatting in the Editor
private static GUILayoutOption[] noneGUILayoutOption =
new GUILayoutOption[] { };
// This function is called when the object is loaded.
void OnEnable()
{
// Get the property from the editing object
_numbers = serializedObject.FindProperty("_numbers");
}
// Implement this function to make a custom inspector.
public override void OnInspectorGUI()
{
// Update the serializedProperty
// - always do this in the beginning of OnInspectorGUI.
serializedObject.Update();
// How you change the size of the array
_numbers.arraySize =
EditorGUILayout.IntField("Size", _numbers.arraySize);
// Display each element/number in the array
foreach (SerializedProperty numberField in _numbers)
{
Rect rect = GUILayoutUtility.GetRect(0f, 16f);
EditorGUI.PropertyField(rect, numberField);
}
// Apply changes to the serializedProperty
// - always do this in the end of OnInspectorGUI.
// Checking the Event type lets us update after Undo and Redo commands.
if (serializedObject.ApplyModifiedProperties() ||
(Event.current.type == EventType.ValidateCommand &&
Event.current.commandName == "UndoRedoPerformed"))
{
// Tell our target to update because it's properties have changed
((RandomScript)target).DoSomething();
}
}
}
For a nice detailed tutorial on Custom Editor's check out Catlike Coding's Star tutorial.
That second method looks great. Same for the tutorial. Gonna play around with it a bit.
Thanks!
Note in Unity 5 it is EventType.ExecuteCommand
not Event.current.type == EventType.ValidateCommand
, just for those who get here and wonder why its not working.
Second method deprecated as of some time in Unity 5 -- Undo and Redo no longer seem to be stored by the Event class. The current way to do it seems to be to add a method of yours to the Undo.undoRedoPerformed
callback. Note that it seems to throw an exception if the method added belongs to the GameObject -- make a wrapper for it in your editor class instead. (This also keeps all the editor-only stuff conveniently in your editor class, letting you keep the GameObject purely for game logic.)
Answer by lPVDl · Sep 13, 2018 at 06:38 PM
If you are still interested in different solution (which seems to be added in latest versions of Unity), then here is an another approach: Implement ISerializationCallbackReceiver to receive callback whenever component is serialized or deserialized:
[ExecuteInEditMode]
public class RandomScript : MonoBehaviour, ISerializationCallbackReceiver
{
//Private array.
[SerializeField]
private float[] _numbers;
//Public property so I'm sure that DoSomething() is executed when someone sets the float array.
public float[] Numbers
{
get { return _numbers; }
set
{
_numbers = value;
DoSomething();
}
}
// Whenever object is deserialized.
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
DoSomething();
}
//Some heavy calculations that need to be done after adjusting the float array.
private void DoSomething()
{
//Initialize stuff from _numbers;
}
void ISerializationCallbackReceiver.OnBeforeSerialize() { }
}
The sad thing that it will not work in some situations, for example if you need to update object's transform position after deserialization, this will not work, since it is restricted to do. In this case you can create flag something like "RequireUpdate" and call you method in Update() after checking this flag, the disadvantage of this solution is that you will need to create Update(), which will allow game designer to disable your component, which is not awesome. Recently I've used this trick: Create class which will implement "Lazy Actions", which will be called next frame and only in the editor:
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public static class EditorLazyAction
{
private static List<Action> Actions = new List<Action>();
static EditorLazyAction()
{
#if UNITY_EDITOR
EditorApplication.update -= Update;
EditorApplication.update += Update;
#endif
}
private static void Update()
{
foreach (var action in Actions)
{
try
{
action.Invoke();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
Actions.Clear();
}
public static void Add(Action action)
{
if (action != null)
Actions.Add(action);
}
}
Than you can use it this way:
[ExecuteInEditMode]
public class RandomScript : MonoBehaviour, ISerializationCallbackReceiver
{
//Private array.
[SerializeField]
private float[] _numbers;
//Public property so I'm sure that DoSomething() is executed when someone sets the float array.
public float[] Numbers
{
get { return _numbers; }
set
{
_numbers = value;
DoSomething();
}
}
// Whenever object is deserialized.
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
EditorLazyAction.Add(DoSomething);
}
//Some heavy calculations that need to be done after adjusting the float array.
private void DoSomething()
{
//Initialize stuff from _numbers;
}
void ISerializationCallbackReceiver.OnBeforeSerialize() { }
}
No flags, no updates in custom class - less code and no way to "disable" initialization by disabling component.