Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question Save and Undo ScriptableObject from Editor.OnSceneGUI()

Discussion in 'Scripting' started by nikita-v-fedorov, Aug 4, 2023.

  1. nikita-v-fedorov

    nikita-v-fedorov

    Joined:
    Jun 15, 2022
    Posts:
    3
    My goal is to update a 2D array from an editor script and being able to save it and undo the changes. I use a ScriptableObject and an intermediate object to encapsulate the array:

    Code (CSharp):
    1.  
    2. // Array wrapper
    3. [Serializable]
    4. public class CityMapArray<T> : IIndexed<T>
    5. {
    6.     [SerializeField] private T[,] _array;
    7.  
    8.     public CityMapArray()
    9.     {
    10.         _array = new T[1, 1];
    11.     }
    12.  
    13.     public CityMapArray(int width, int height)
    14.     {
    15.         _array = new T[width, height];
    16.     }
    17.  
    18.     public CityMapArray(T[,] array)
    19.     {
    20.         UpdateMap(array);
    21.     }
    22.  
    23.     public void UpdateMap(T[,] array)
    24.     {
    25.         _array = new T[array.GetLength(0), array.GetLength(1)];
    26.         if (array.Rank != 2)
    27.             throw new ArgumentException($"{nameof(array)} must be a 2D array.");
    28.  
    29.         for (var x = 0; x < array.GetLength(0); x++)
    30.         {
    31.             for (var z = 0; z < array.GetLength(1); z++)
    32.             {
    33.                 this[x, z] = array[x, z];
    34.             }
    35.         }
    36.     }
    37.  
    38.     public T this[int x, int z]
    39.     {
    40.         get
    41.         {
    42.             return _array[x, z];
    43.         }
    44.         set
    45.         {
    46.             _array[x, z] = value;
    47.         }
    48.     }
    49.  
    50.     public int GetLength(int dimension)
    51.     {
    52.         return _array.GetLength(dimension);
    53.     }
    54. }
    Code (CSharp):
    1. [Serializable]
    2. public class CityMapData : ScriptableObject
    3. {
    4.     // ...
    5.     // I'm using a complex object instead of nullable int. The code doesn't work even with nullable int.
    6.     [SerializeField] private CityMapArray<int?> _cityMap;
    7.  
    8.     public CityMapArray<int?> CityMap => _cityMap;
    9. }
    And I'm using it this way:

    Code (CSharp):
    1.  
    2. [ExecuteInEditMode]
    3. public class City : MonoBehaviour
    4. {
    5.     [SerializeField] private CityMapData _cityMapData;
    6.  
    7.     public CityMapData CityMapData => _cityMapData;
    8.     public CityMapArray<int?> CityMap => CityMapData == null ? null : CityMapData.CityMap;
    9.  
    10.     // ...
    11.  
    12.     public void ChangeMapSize(/* ... */)
    13.     {
    14.         // ...
    15.         var newMap = new int?[newWidth, newHeight];
    16.         CityMap.UpdateMap(newMap);
    17.  
    18.         // ...
    19.     }
    20. }
    21.  
    22.  
    This is the custom editor:
    Code (CSharp):
    1.  
    2. [CustomEditor(typeof(City)), CanEditMultipleObjects]
    3. public class CityEditor : Editor
    4. {
    5.     // ...
    6.  
    7.     public override void OnInspectorGUI()
    8.     {
    9.         var myTarget = (City)target;
    10.  
    11.         if (GUILayout.Button("Save map"))
    12.         {
    13.             // doesn't work
    14.             EditorUtility.SetDirty(myTarget.CityMapData);
    15.             AssetDatabase.SaveAssets();
    16.             AssetDatabase.Refresh();
    17.         }
    18.     }
    19.  
    20.     public void OnSceneGUI()
    21.     {
    22.         var t = target as City;
    23.         if (Event.current.type == EventType.MouseUp && Event.current.button == 0)
    24.         {
    25.             EditorUtility.SetDirty(myTarget.CityMapData);
    26.             // throws an error
    27.             AssetDatabase.SaveAssets();
    28.             AssetDatabase.Refresh();
    29.         }
    30.  
    31.         EditorGUI.BeginChangeCheck();
    32.  
    33.         // changing the array in some ways
    34.         if (EditorGUI.EndChangeCheck())
    35.         {
    36.            
    37.             if (changed)
    38.             {
    39.                 // registers simple properties of City, but the CityMapData stays the same
    40.                 Undo.RegisterCompleteObjectUndo(new UnityEngine.Object[] { t, t.CityMapData }, "Change Map size");
    41.                 t.ChangeMapSize(/* ... */);
    42.             }
    43.         }
    44.     }
    45. }
    As you can see when I change the data, a new array is being created.

    Not a single save method works: when I save the object from OnSceneGUI() it throws an error, from OnInspectorGUI() nothing happens (I reopen the editor and nothing has been saved). The Undo operation works only for simple properties of City class, but not for the CityMapData.

    How should it be done here?
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,015
    Unity can't serialise 2d arrays, so it's never going to work. Nor can it serialise nullable types.

    You need to use something like ISerializationCallbackReciever to convert the data to and from a Unity friendly form and the non-serialisable 2d array. Or use Odin Serialization.
     
    nikita-v-fedorov likes this.
  3. nikita-v-fedorov

    nikita-v-fedorov

    Joined:
    Jun 15, 2022
    Posts:
    3
    Thanks for the tip,
    I ended up with an array of a structs to make it serialized:
    Code (CSharp):
    1. [NonSerialized] private T[,] _array;
    2. [SerializeField] private CityMapArrayElement<T>[] _elements;
    3.  
    4. ///...
    5.  
    6. [Serializable]
    7. public struct CityMapArrayElement<T>
    8. {
    9.     [SerializeField] private int _x;
    10.     [SerializeField] private int _z;
    11.     [SerializeField] private T _element;
    12.  
    13.     public CityMapArrayElement(int x, int z, T element)
    14.     {
    15.         _x = x;
    16.         _z = z;
    17.         _element = element;
    18.     }
    19.  
    20.     public int X => _x;
    21.     public int Z => _z;
    22.     public T Element => _element;
    23. }
    24.  
    Each time I update the array I update the _elements as well. In the OnAfterDeserialize method I restore the array:

    Code (CSharp):
    1. public void Restore()
    2. {
    3.     var maxX = -1;
    4.     var maxZ = -1;
    5.  
    6.     foreach(var el in _elements)
    7.     {
    8.         if (maxX < el.X) maxX = el.X;
    9.         if (maxZ < el.Z) maxZ = el.Z;
    10.     }
    11.  
    12.     _array = new T[maxX + 1, maxZ + 1];
    13.  
    14.     foreach (var el in _elements)
    15.         _array[el.X, el.Z] = el.Element;
    16. }
     
    spiney199 likes this.