How to properly Add/Remove sub-assets (Scriptable Objects) using Undo/Redo ?

Hi, I’m creating a custom editor window for a tool I’m developing. The “main asset” consist of a ScriptableObject called “Main Asset”, inside this asset are sub-assets or “elements”, also ScriptableObjects (added with the “AddObjectToAsset” method). The main asset looks like this:

In order to add a new element i create the instantiate of the ScriptableObject, add the asset to the main asset and then add the reference to a list. This is the code for the AddElement (an extension method for ScriptableObjects), this works fine with Undo/Redo (i think):

public static void AddElement<T>( this ScriptableObject scriptableObject , SerializedProperty listProperty , string name = "Element" , HideFlags hideFlags = HideFlags.None ) where T : ScriptableObject
 {
          if( !listProperty.isArray )
               throw new System.Exception( "\"listProperty\" is not a List." );

          T element = ScriptableObject.CreateInstance<T>();

          element.name = name;
          element.hideFlags = hideFlags;

          string scriptableObjectPath = AssetDatabase.GetAssetPath( scriptableObject );
                 
          AssetDatabase.AddObjectToAsset( element , scriptableObjectPath );
          AssetDatabase.SaveAssets();

          Undo.RegisterCreatedObjectUndo( element , "Add element to ScriptableObject" );                  

          listProperty.InsertArrayElementAtIndex( listProperty.arraySize );
          SerializedProperty lastElement = listProperty.GetArrayElementAtIndex( listProperty.arraySize - 1 );
          lastElement.objectReferenceValue = element;

 }

The problem happens with the RemoveElement method, here when i run the “Undo” the asset is recovered just fine, but the reference to the list is still missing. I’m doing two things, first Destroying the subAsset, like the docs says with Undo.DestroyObjectImmediate, then recording the state of the list using Undo.RecordObject, these two methods are wrapped inside an Undo group. Here is the code:

public static void RemoveElement<T>( this ScriptableObject scriptableObject , int index , SerializedProperty listProperty ) where T : ScriptableObject
    {
        if( !listProperty.isArray )
            throw new System.Exception( "\"listProperty\" is not a List." );
      
        if( !Utilities.isBetween( index , 0 , listProperty.arraySize - 1 , true ) )
            throw new System.Exception( "\"index\" out of range." );
      
        if( listProperty.arraySize == 0 )
            return;
      
        SerializedProperty elementProperty = listProperty.GetArrayElementAtIndex( index );

        //Undo
        Undo.SetCurrentGroupName( "Remove element from ScriptableObject" );
        int group = Undo.GetCurrentGroup();
               
        Undo.DestroyObjectImmediate( elementProperty.objectReferenceValue );

        Undo.RecordObject( listProperty.objectReferenceValue , "");

        listProperty.DeleteArrayElementAtIndex( index );
        listProperty.DeleteArrayElementAtIndex( index );
      
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        Undo.CollapseUndoOperations( group );             
      
      
    }

Here is a gif showing the problem:

So, what am i doing wrong? maybe i misunderstood something. I’m too green with Unity’s Undo functionality, any tooltip or suggestion is welcome.

Thanks in advance.

Edit: I forgot to mention, the line “Undo.RecordObject( listProperty.objectReferenceValue , “”);” gives me an error, i asume that’s because the list is not a “Unity.Object”.

There’s a few other options rather than .RecordObject… there’s a plural, as well as a few hierarchy-recording things. You might find some combination of things that do what you need perhaps. Maybe you could create a ghost object with a custom script with OnDestroy() method in it, and record that side-by-side with what you are currently undoing. When the second ghost object is undone, you would call a delegate that undoes your extra list entry above.

Here’s the Undo API reference:

Hi, thanks for the reply, yes that was the next thing to do, create sort of a “ghost” and record or save the state somehow. I wonder if that’s what everyone does, or maybe i’m using this functionality wrong (of course i’m, the Object Unity is expecting is not there). For now I’m using plain C# classes instead of Scriptable Objects (of course, not the same thing) and everything works really well (specifically for this). if I came up with something i will return to the scriptableObjects (and update this thread).

Thanks again.

Did you find a good solution for this?

A bit late to the party and I hope I understood the issue correctly.

The correct order would have to be:

  1. Undo.Record and remove the element from the list;
  2. Destroy the Scriptable object using Undo.DestroyObjectImmediate;

My guess is that when you undo, the scriptable object has to first be restored and then stored back in the list. If you first destroy the SO and then remove from list, it will attempt to deserialize the list with a null value (because you destroyed the SO previously).

I am using the same strategy in my own Editor extensions. Sometimes for example, I have to destroy a large group of SOs which are also stored inside a list. The right way to do this would be:

  1. Undo.Record to save the list state.
  2. remove the SOs from the list
  3. Undo.DestroyObjectImmediate in a separate pass for each SO that was removed from the list.
1 Like