How to know if an object was affected by UNDO operation?

I really need help on this one guys.

I have multiple objects on a scene that captures UNDO operation in the editor through OnUndoPerformed event, and does heavy tasks in the editor when that event is triggered.

// Catch UNDO operation
void OnValidate()
{
    Undo.undoRedoPerformed += OnUndoPerformed;
}
 
void OnUndoPerformed()
{
    <Do Heavy Operations>
}

The thing is, every time you hit Ctrl+z the performance of the scene is very low because of these tasks. I’m trying to optimize the code doing those heavy operations only when UNDO operation changes the object with this script. Something like this:

void OnUndoPerformed()
{
    If (<this object was changed by UNDO>)
        <Do Heavy Operations>
}

Is there any way to know what objects where affected by UNDO operation?

1 Like

I know not this event… which event are you referring to?

Undo.undoRedoPerformed event?

Can you show us an example of your code for what you’re attempting to do?

Thanks for your answer. I’ve updated the very first post to be more clear on what I need.

Up!

We cant help you optimise your heavy task if we dont know what it is.
Also OnValidate could get called multiple times adding the method again no? So you might be running that function multiple times.

Oh, I’m not trying to optimize those heavy tasks. I have to do them anyway. I’m just trying to do them only if UNDO operation changed the object with the script, not every time UNDO is performed.

Again, I changed the very first post to explain the whole thing better.

One last try. I’m really stuck on this one. Up again!

You’re not the only one. Up

Gonna revive this old thread. I’m trying to work around it with version numbers and stuff, but this would be very helpful

encountered this thread while solving it for my case.
in my case I used Time.frameCount, OnValidate, and Undo.undoRedoPerformed to match the undo to the target object.
because it appeared that OnValidate and Undo.undoRedoPerfomed happened on the same editor frame. I will update you here if this does not hold up.

as a side note, it appears that in Unity 2022 there is some progress made to make this easier but this appears to not directly address which object had Redo/Undo done on but just give you slightly more information as in groups and the name of the record but I am currently using 2021 so I did not inspect it further.

1 Like

@FishToast A code example would be appreciated!

Though on first thought I think this solution may be very brittle because Time.frameCount will be identical for multiple (dozens) EditorApplication.update calls, so when a user is quickly hitting undo/redo you may actually stack multiple Undo/Redo operations onto the same frameCount number.

The improvement in Unity 2022 relates to undoRedoEvent which gives you UndoRedoInfo as a parameter and that ends up giving you the group (a number), the name (what’s seen in the menu) and whether it is a redo operation. Unfortunately no info about the affected objects.

It should be possible with the Undo postprocessing event. This gives you access to a target property but you risk generally slowing down undo/redo operations if you’re not super-careful.

You ever find yourself working on something, then run into a wall, so you google it and find a thread that you were part of years prior???

Well that was me today.

Thank you @CodeSmile for pointing out the new version of underRedoEvent that includes the groupid and groupname (the thing I specifically was looking for), so not exactly OP’s needs, but close enough.

But also I’m unfortunately in Unity 2021, and we’re not moving off of it, especially not for this little thing.

So I wrote up my own version that emulates it, and did so in a way that will just automatically transition to the 2022.2 way if/when we move to that version for future projects.

It can be found here:

Or the current state of it as of today:

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace com.spacepuppyeditor
{

    [InitializeOnLoad]
    public static class SPUndo
    {

        public struct UndoRedoInfo
        {
            public string groupName;
            public int groupId;
            public bool isRedo;
        }

        public static event System.Action<UndoRedoInfo> undoRedoEvent;

#if UNITY_2022_2
        static SPUndo()
        {
            Undo.undoRedoEvent += UndoRedoEventHandler;
        }

        static void UndoRedoEventHandler(in UnityEditor.UndoRedoInfo info)
        {
            undoRedoEvent?.Invoke(new UndoRedoInfo()
            {
                groupId = info.undoGroup,
                groupName = info.undoName,
                isRedo = info.isRedo,
            });
        }
#else
        delegate void GetRecordsDelegate(List<string> records, out int cursor);

        static GroupInfo _lastGroup;

        static List<string> _records = new List<string>();
        static int _cursorPos;
        static GetRecordsDelegate _getRecordsCallback;

        static SPUndo()
        {
            unsafe
            {
                var tp = typeof(Undo);
                var methinfo = tp.GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Where(o => o.Name == "GetRecords").FirstOrDefault(o =>
                {
                    var parr = o.GetParameters();
                    return parr.Length == 2 && parr[0].ParameterType == typeof(List<string>) && parr[1].IsOut;
                });
                if (methinfo != null)
                {
                    _getRecordsCallback = methinfo.CreateDelegate(typeof(GetRecordsDelegate)) as GetRecordsDelegate;
                }
                if (_getRecordsCallback == null)
                {
                    Debug.Log("SPUndo - failed to locate Undo.GetRecords, this version of Unity is not supported.");
                }
            }
            if (_getRecordsCallback == null) return;

            Undo.undoRedoPerformed += UndoRedoPerformed;
            Undo.willFlushUndoRecord += WillFlushUndoRecord;
           
            //initialization in editor sometimes happens outside of the main loop and 'GetRecords' call only works on main loop
            EditorHelper.Invoke(() =>
            {
                _lastGroup = GroupInfo.GetCurrent();
            }, 0f);
        }

        static void UndoRedoPerformed()
        {
            if (_getRecordsCallback == null) return;

            var cur = GroupInfo.GetCurrent();

            UndoRedoInfo outinfo;
            if (cur.cursor > _lastGroup.cursor)
            {
                //redo
                outinfo = new UndoRedoInfo()
                {
                    groupName = cur.name,
                    groupId = cur.id,
                    isRedo = true,
                };
            }
            else
            {
                //undo
                outinfo = new UndoRedoInfo()
                {
                    groupName = _lastGroup.name,
                    groupId = _lastGroup.id,
                    isRedo = false,
                };
            }

            _lastGroup = cur;
            undoRedoEvent?.Invoke(outinfo);
        }

        static void WillFlushUndoRecord()
        {
            if (_getRecordsCallback == null) return;

            var cur = GroupInfo.GetCurrent();
            if (cur.id != _lastGroup.id)
            {
                _lastGroup = cur;
            }
        }


        struct GroupInfo
        {
            public string name;
            public int id;
            public int cursor;

            public static GroupInfo GetCurrent()
            {
                _getRecordsCallback?.Invoke(_records, out _cursorPos);
                return new GroupInfo()
                {
                    name = Undo.GetCurrentGroupName(),
                    id = Undo.GetCurrentGroup(),
                    cursor = _cursorPos,
                };
            }

        }

#endif

    }

}

This is “mostly” self-contained aside from lines 72-75:

            //initialization in editor sometimes happens outside of the main loop and 'GetRecords' call only works on main loop
            EditorHelper.Invoke(() =>
            {
                _lastGroup = GroupInfo.GetCurrent();
            }, 0f);

I need to get the “initial” state on initialize, but because that may not always occur on the main loop in editor we need to ensure it does so. This is what my custom ‘EditorHelper.Invoke’ does, it just calls the delegate the next time EditorApplication.update fires.

If someone wants this you could replace it with an ‘EditorCoroutine’ (unity package that can be added through the package manager), or just inline the update callback:

            EditorApplication.CallbackFunction callback = null;
            callback = () =>
            {
                EditorApplication.update -= callback;
                _lastGroup = GroupInfo.GetCurrent();
            };
            EditorApplication.update += callback;
1 Like