Editor Undo API Works but Corrupts Scene on Scene Reload

I’ve been having trouble implementing UndoRedo for a custom editor tool. I’ve managed to get it to work perfectly, however if I perform a undo and then a redo and then save the scene and reload it or hit the play button, the scene heirarchy is broken and stays broken (as if the scene file was incorrectly modified).

I have reduced my problem to a simple reproduce-able test case.

I have the following Scene Hierarchy with a “TestObject” that has a TestMap component attached which only has a public List exposed:
8865501--1209945--upload_2023-3-9_16-57-15.png

Here are the two monobehaviours I am using in the example:

public class MapPiece : MonoBehaviour
{
    public TestMap testMap;
    public Vector3 vec;
    public int number = 0;
}

public class TestMap : MonoBehaviour
{
    public List<MapPiece> pieces = new List<MapPiece>();
}

TestMap is attached to the “TestObject” gameobject (as shown above).
MapPiece is attached to the root of a simple prefab created by creating a simple “primitive Cube” gameobject using the built-in gameobject context menu (Right Click > 3D Object > Cube). This prefab has a “primitive Sphere” gameobject as a child object.
8865501--1209951--upload_2023-3-9_17-9-35.png

Now, for the test code :

public class ExampleMenuItem : MonoBehaviour
{
    [MenuItem("Test/ClearUndoRedoStack")]
    static void ClearUndoRedoStack()
    {
        Undo.ClearAll();
    }

    [MenuItem("Test/UndoExample")]
    static void UndoExample()
    {
        GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/gitignore/UndoTest/cube.prefab");
        GameObject testContainer = GameObject.Find("TestContainer");
        TestMap testMap = testContainer.transform.parent.GetComponent<TestMap>();

        for (int i = 0; i < 3; i++)
        {
            GameObject prefabInstance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, testContainer.transform);

            Undo.RegisterCreatedObjectUndo(prefabInstance, "");

            PrefabUtility.UnpackPrefabInstance(prefabInstance, PrefabUnpackMode.Completely, InteractionMode.UserAction);

            MapPiece mapPiece = prefabInstance.GetComponent<MapPiece>();

            Undo.RecordObjects(new UnityEngine.Object[] { mapPiece, testMap }, "");

            mapPiece.testMap = testMap;

            testMap.pieces.Add(mapPiece);

            mapPiece.transform.position = new Vector3(i, mapPiece.transform.position.y, mapPiece.transform.position.z);

            mapPiece.vec = new Vector3(i, i, i);
            mapPiece.number = i;

            EditorUtility.SetDirty(testMap);

            Undo.FlushUndoRecordObjects();
            Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
        }
    }
}

Reproduction steps:

  1. Click Test > UndoExample from the top menu bar.
    Result :

  2. Press Ctrl + Z to trigger Undo.
    Result : Scene returns to normal.

  3. Press Ctrl + Y to trigger Redo.
    Result: Scene returns to the exact same state as in step 1 (as expected!) but don’t celebrate just yet…

  4. Save the scene. Then either press the play button (reloads the scene) or reload the current scene in the editor.
    Result : Scene is broken if reloaded in the editor. Unity crashes if you press the play button (you will have to manually delete the scene file using your file explorer in order to open Unity again without crashing).


Why does this happen? Am I not using the Undo API correctly?
Should I file a bug report with Unity?
Any help is much appreciated here. Thanks.

Unity version: 2021.3.14f1 Personal
Platform: Windows 10 64bit

I am going to file a bug report for this since I’ve had no response. I’ll post any updates from the bug report here.

Yeah, I’ve got nothing… you might have a Unity bug, but on the other hand you are in some pretty sticky complicated code with the unpacking prefabs stuff.

It appears you are undo-saving the right stuff, but I might suggest recording the mapPiece.gameObject instead, or otherwise trying combinations of other objects.

I would need to kinda diagram the object creation and mutation you got going to reason about if it is capturing everything it needs to or not.

1 Like

When I do an Undoable sequence of multiple steps, I follow very clear steps as if they were independently-invokable commands. Your code doesn’t really read like there’s a clear bracketing of stuff to be done, at least not like I’ve seen before.

        Undo.IncrementCurrentGroup();
        Undo.SetCurrentGroupName("Do Things");
        int undoID = Undo.GetCurrentGroup();
        foreach (var gobj in gobjsToBeDone)
        {
            MyComp comp = gobj.GetComponent<MyComp>();
            Undo.RecordObject(comp, "Do One Thing");
            comp.DoThing();
        }
        Undo.CollapseUndoOperations(undoID);
1 Like

@halley1 Thanks for the reply. I want to make multiple changes to multiple things and collapse it into a single action. I record all steps for each action individually and collapse them into the current group. I should then increment the group index but I didn’t bother as this is a minimal test example that should run once and work (plus I have a test context menu action that clears the undoredo stack).

As for my logic not having a “bracketing of stuff” (I’m not quite sure what you mean by this) but I think the logic makes sense and follows a simple set of steps:

  1. Do the following 3 times:
    A. Instantiate prefab
    B. Since I created a gameobject in the scene, I register its creation via RegisterCreatedObjectUndo
    C. Unpack the prefab, passing InteractionMode.UserAction as a parameter which registers this action with the undo system.
    D. Start recording MapPiece object attached to the newly created instance and the testMap object that is in the scene
    E. Make changes to the MapPiece object (assign testMap reference)
    F. Make changes to the testMap object (assign mapPiece reference)
    G. Make changes to the MapPiece object (modify various values)
    H. Mark Map as dirty so the user has to save the scene
    I. Ensure objects recorded using RecordObjects are registered as an undoable action.
    F. Collapse all undo operation up to group index together into one step

Perhaps I have to add the mapPiece Transform component to the list of objects being recorded?
Edit: nope, I added mapPiece.transform to the array of Objects passed to recordObject() but I still get the same result.

So after some experimentation it seems that commenting out this line causes no errors:

PrefabUtility.UnpackPrefabInstance(prefabInstance, PrefabUnpackMode.Completely, InteractionMode.UserAction);

However, I want to unpack the prefab instance. What should I do to have the unpacking of the prefab registered with the same undo command along with all the other modifications? Any Ideas?

I feel like my logic is correct and that this is a bug with the Undo system which is why it would be great if a Unity dev can take a look at this and tell me if I did things correctly and this is indeed a bug or I messed up and in this case what are the correct steps to achieve the desired result?

Just to note, I’ve tried passing InteractionMode.AutomatedAction instead which doesn’t break the scene file but I get incorrect behavior now when doing ctrl+z then ctrl+y which is expected since the unpacking of the prefab is not being recorded if I flag the action as “Automated”.

For reference, here is the source for UnpackPrefabInstance : UnityCsReference/Editor/Mono/Prefabs/PrefabUtility.cs at e7d9de5f09767c3320b6dab51bc2c2dc90447786 · Unity-Technologies/UnityCsReference · GitHub

which is slightly different than my version that I see in VS:

public static void UnpackPrefabInstance(GameObject instanceRoot, PrefabUnpackMode unpackMode, InteractionMode action)
{
    if (!IsPartOfNonAssetPrefabInstance(instanceRoot))
    {
        throw new ArgumentException("UnpackPrefabInstance must be called with a Prefab instance.");
    }

    if (!IsOutermostPrefabInstanceRoot(instanceRoot))
    {
        throw new ArgumentException("UnpackPrefabInstance must be called with a root Prefab instance GameObject.");
    }

    if (action == InteractionMode.UserAction)
    {
        string name = "Unpack Prefab instance";
        Undo.RegisterFullObjectHierarchyUndo(instanceRoot, name);
        GameObject[] array = UnpackPrefabInstanceAndReturnNewOutermostRoots(instanceRoot, unpackMode);
        GameObject[] array2 = array;
        foreach (GameObject instanceComponentOrGameObject in array2)
        {
            UnityEngine.Object prefabInstanceHandle = GetPrefabInstanceHandle(instanceComponentOrGameObject);
            if ((bool)prefabInstanceHandle)
            {
                Undo.RegisterCreatedObjectUndo(prefabInstanceHandle, name);
            }
        }
    }
    else
    {
        UnpackPrefabInstanceAndReturnNewOutermostRoots(instanceRoot, unpackMode);
    }

    PrefabUtility.prefabInstanceUnpacked?.Invoke(instanceRoot);
}