Search Unity

How to correct the size of a serialized array when removing objects via editor script?

Discussion in 'Scripting' started by Edvard-D, Jun 27, 2017.

  1. Edvard-D

    Edvard-D

    Joined:
    Jun 14, 2012
    Posts:
    129
    Hey all!

    I'm playing around with editor scripts for the first time, which seems really, really promising. Right now my focus is automating what happens when I create a new asset, such as getting correct scripts attached to it and adding prefabs to lists in other objects that use them.

    It's that last bit that I'm having a little trouble with. I'm able to add and remove items from it just fine, but when removing items it doesn't fix the size of the array. After adding an item to the array the size is 1, and on deleting that item it gets removed from the array. The problem is that the size stays at 1, so every time I delete an item there's a new blank spot in the array.

    What's the right way to go about fixing this?
     
  2. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    when called for non-null elements, DeleteArrayElementAtIndex clears the value but the element position remains. When you call DeleteArrayElementAtIndex on empty elements then it will actually remove the postion from the array.

    its always been like this and its never made sense. I simply use an extension that pre-clears the element before deleting it and be done with it.

    Note: not the actual methods I use. these are a simplified versions since the one I use would spiral down a rabbit hole of required methods.
    Code (CSharp):
    1.  
    2.         public static void RemoveElement(this SerializedProperty list, int index)
    3.         {
    4.             if (list == null)
    5.                 throw new ArgumentNullException ();
    6.          
    7.             if(!list.isArray)
    8.                 throw new ArgumentException("Property is not an array");
    9.          
    10.             if(index<0||index>=list.arraySize)
    11.                 throw new IndexOutOfRangeException ();
    12.          
    13.             list.GetArrayElementAtIndex(index).SetPropertyValue(null);
    14.             list.DeleteArrayElementAtIndex(index);
    15.          
    16.             list.serializedObject.ApplyModifiedProperties();
    17.         }
    18.  
    19.         public static void SetPropertyValue(this SerializedProperty property,object value)
    20.         {
    21.             switch(property.propertyType)
    22.             {
    23.              
    24.                 case SerializedPropertyType.AnimationCurve:
    25.                     property.animationCurveValue = value as AnimationCurve;
    26.                     break;
    27.  
    28.                 case SerializedPropertyType.ArraySize:
    29.                     property.intValue = Convert.ToInt32(value);
    30.                     break;
    31.  
    32.                 case SerializedPropertyType.Boolean:
    33.                     property.boolValue = Convert.ToBoolean(value);
    34.                     break;
    35.  
    36.                 case SerializedPropertyType.Bounds:
    37.                     property.boundsValue = (value==null)
    38.                             ? new Bounds()
    39.                             : (Bounds)value;
    40.                     break;
    41.  
    42.                 case SerializedPropertyType.Character:
    43.                     property.intValue = Convert.ToInt32(value);
    44.                     break;
    45.  
    46.                 case SerializedPropertyType.Color:
    47.                     property.colorValue = (value==null)
    48.                             ? new Color()
    49.                             : (Color)value;
    50.                     break;
    51.  
    52.                 case SerializedPropertyType.Float:
    53.                     property.floatValue = Convert.ToSingle(value);
    54.                     break;
    55.  
    56.                 case SerializedPropertyType.Integer:
    57.                     property.intValue = Convert.ToInt32(value);
    58.                     break;
    59.  
    60.                 case SerializedPropertyType.LayerMask:
    61.                     property.intValue = (value is LayerMask)?((LayerMask)value).value: Convert.ToInt32(value);
    62.                     break;
    63.  
    64.                 case SerializedPropertyType.ObjectReference:
    65.                     property.objectReferenceValue = value as UnityEngine.Object;
    66.                     break;
    67.  
    68.                 case SerializedPropertyType.Quaternion:
    69.                     property.quaternionValue = (value==null)
    70.                             ? Quaternion.identity
    71.                             :(Quaternion)value;
    72.                     break;
    73.  
    74.                 case SerializedPropertyType.Rect:
    75.                     property.rectValue = (value==null)
    76.                             ? new Rect()
    77.                             :(Rect)value;
    78.                     break;
    79.  
    80.                 case SerializedPropertyType.String:
    81.                     property.stringValue = value as string;
    82.                     break;
    83.  
    84.                 case SerializedPropertyType.Vector2:
    85.                     property.vector2Value = (value==null)
    86.                             ? Vector2.zero
    87.                             :(Vector2)value;
    88.                     break;
    89.  
    90.                 case SerializedPropertyType.Vector3:
    91.                     property.vector3Value = (value==null)
    92.                             ? Vector3.zero
    93.                             :(Vector3)value;
    94.                     break;
    95.  
    96.                 case SerializedPropertyType.Vector4:
    97.                     property.vector4Value = (value==null)
    98.                             ? Vector4.zero
    99.                             :(Vector4)value;
    100.                     break;
    101.  
    102.             }
    103.         }
    104.  
     
    Last edited: Jun 28, 2017
  3. Edvard-D

    Edvard-D

    Joined:
    Jun 14, 2012
    Posts:
    129
    Thanks for the reply!

    I'm not familiar at all with SerializedProperty or the context of how this code would be used. I'm used a serialized List (see below), but it seems like the array I use to store this data would need to be a SerializedProperty? Or can you pass any list into this? In fact, it seems like these methods should be added to a class that derives from SerializedProperty? The part where SetPropertyValue is called is throwing an error.

    It seems like SaveModifiedProperties is now ApplyModifiedProperties, and I think the empty parentheses after GetArrayElementAtIndex isn't necessary?

    Code (CSharp):
    1. [SerializeField]
    2. private List<RoomBase> _roomPrefabs;
    3. private List<RoomBase> RoomPrefabs { get { return _roomPrefabs; } set { _roomPrefabs = value; } }
     
  4. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    First off, your right its ApplyModifiedProperties, not SaveModifiedProperties, I wrote this from memory and I just messed that up. and yes that is extra parenths on the GetArrayElementAtIndex, I'll edit my post and fix that.

    Next up, the methods I posted are called Extension Methods (dead giveaways is that they are public static methods with a "this" modifier on their 1st parameter and will be in public static classes). The primary purpose of Extension methods is that it allows you to add functions for a class, without directly modifying or breaking that class. SerializedProperty is a class written by Unity, but with extension methods I able to safely add new methods to that class, as if they were written directly in the class itself. it still respects encapsulation so you only have access to the stuff thats already public in the class, so no you can't use it to break encapsulation and mess with private variables. Simply write a public static class (give it what ever name you want, but I typically postfix them with "Extensions", like public static class SerializedPropertyExtensions ), and drop those methods in them and all your SerializedProperties will have RemoveElement and SetPropertyValue methods.

    Finally, it sounds like you don't understand what a SerializedProperty is. so before I can explain that its important to understand what the SerializedObject class is. The entire purpose of the SerializedObject (and thus SerializedProperty) is to be a representative for a classes savable data. its a proxy, or ambassador of the true class.

    When you look at a transforms data in the inspector, you're actually looking at a serializedObject which is a representing that transform's publicized state. It walks through each serializable field and copies each value's state into a SerializedProperty. the inspector then iterates over each serializedProperty, and determines how it should draw the data in its panel. So all it is, a picture of the data, put in a format that the inspector can use to draw with.

    thus you do have a List<RoomBase>, it just that in your Editor class you'll have serializedProperty which will be representing your list for the inspector. Unity was nice in that they've written their inspector in such a way to automate loading a serializedProperty and drawing it for you, though if you so wanted you're free to write it yourself.
     
    Edvard-D likes this.
  5. Edvard-D

    Edvard-D

    Joined:
    Jun 14, 2012
    Posts:
    129
    Wow, thanks for such a thorough response! A lot of what you explained is new to me, so it was super helpful. The disconnect for me at this point is understanding how exactly I'm calling RemoveElement. It seems like that would need to be called on a SerializedProperty, but I think that means I can't call it on RoomPrefabs or _roomPrefabs directly, right?
     
  6. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Correct you would use the code on the SerializedProperty. notice the "this SerializedProperty list"? that means the function is written to extend the SerializedProperty class. Thus no you can't call it on RoomPrefabs.

    Then again if you have the reference to the RoomPrefabs List, you don't need RemoveElement(). Simply use Remove or RemoveAt. If you're using an Editor class its pretty easy to get the direct reference by casting from the "target" variable. However, SerializedProperties do have Undo History and they are far more important in Property Drawers.

    What code do you have in your Editor class? pretty much all you have to do is switch out DeleteArrayElementAtIndex with my RemoveElement.
     
    Edvard-D likes this.
  7. Edvard-D

    Edvard-D

    Joined:
    Jun 14, 2012
    Posts:
    129
    So, maybe I'm going about this the wrong way? The code below is in RoomBase, since the goal is for the RoomPrefabs list to be added/removed from automatically when prefabs are created or deleted.

    Code (CSharp):
    1. [ExecuteInEditMode]void OnDestroy()
    2. {
    3.     if (Application.isPlaying == false)
    4.         GetMansionGenService().RemoveRoomPrefab(this);
    5. }
    That calls the following method in MansionGenService, where the RoomPrefabs list is declared. Side note: it's in a method of its own right now since it's within a #IF UNITY_EDTIOR declaration.

    Code (CSharp):
    1. public void RemoveRoomPrefab(RoomBase roomPrefab)
    2. {
    3.     RoomPrefabs.Remove(roomPrefab);
    4. }
    There really isn't an editor class. Maybe there's another way to go about this that would let me make use of SerializedProperty?
     
  8. Edvard-D

    Edvard-D

    Joined:
    Jun 14, 2012
    Posts:
    129
    After taking a step back and digging into custom inspectors a bit more, I realized that the way I was going about this isn't really possible. I learned that custom editors only activate when the inspector is displayed. I wish there was some way to access the values of a SerializedProperty without it having to be active in the inspector, but I haven't come across anything like that. For now, this is the code I have to clear out any empty elements from the array in question.

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. [CustomEditor(typeof(MansionGenService))]
    5. public class MansionGenInspector : Editor
    6. {
    7.     public SerializedProperty RoomPrefabsProperty;
    8.  
    9.  
    10.     void OnEnable()
    11.     {
    12.         RoomPrefabsProperty = this.serializedObject.FindProperty("_roomPrefabs");
    13.         RemoveEmptyRoomPrefabElements();
    14.     }
    15.  
    16.     public void RemoveEmptyRoomPrefabElements()
    17.     {
    18.         if (RoomPrefabsProperty.serializedObject != null)
    19.         {
    20.             for (int i = RoomPrefabsProperty.arraySize - 1; i >= 0; i--)
    21.             {
    22.                 if (RoomPrefabsProperty.GetArrayElementAtIndex(i).objectReferenceValue == null)
    23.                     RoomPrefabsProperty.RemoveElement(i);
    24.             }
    25.         }
    26.     }
    27. }
    28.  
    I'm planning to make the "RemoveEmptyRoomPrefabElements" more generic and add it to the SerializedPropertyExtensions script.

    At this point I'm started another thread to try and figure out how I can automatically select an object in the project asset folder so its inspector displays and the code above can run without me needing to do anything.

    EDIT: Got a response at this Reddit thread. Now that I have the RemoveEmptyArrayElements method (generic version of RemoveEmptyRoomPrefabElements above), I can just get the GameObject in the project hierarchy it's attached to using AssetDatabase.LoadAssetAtPath (getting the path using AssetDatabase.GUIDToAssetPath, and getting the GUID using AssetDatabase.FindAssets). Once I have that object reference, I can do the following:

    Code (CSharp):
    1. new SerializedObject(mansionGenService).FindProperty("_roomPrefabs").RemoveEmptyArrayElements();
    And here's RemoveEmptyArrayElements:

    Code (CSharp):
    1. public static int RemoveEmptyArrayElements(this SerializedProperty list)
    2. {
    3.     var elementsRemoved = 0;
    4.     if (list.serializedObject != null)
    5.     {
    6.         for (int i = list.arraySize - 1; i >= 0; i--)
    7.         {
    8.             if (list.GetArrayElementAtIndex(i).objectReferenceValue == null)
    9.             {
    10.                 list.RemoveElement(i);
    11.                 elementsRemoved++;
    12.             }
    13.         }
    14.     }
    15.  
    16.     return elementsRemoved;
    17. }
    Sweet and simple!
     
    Last edited: Jul 1, 2017
  9. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    It is possible and theres a couple of ways you can go about it depending on what you want to happen. You can have an editor window that detects a selection change and manually sets up and manages an Editor instance via Editor.CreateCachedEditor(). you could have a DrawGizmo class that detects when a specific object is selected and then performs an action if the cached selection doesn't equal the current selection. or you could use an asset post process to detect changes and then perform your actions then.

    SerializedProperties and SerializedObjects don't need an Editor class to work, just the UnityEditor namespace. You can use SerializedProperties in Editors, EditorWindows, EditorWizards, PropertyDrawers, AssetPostProcessors, and even custom static classes that listen to certain UnityEdtior attributes (like MenuItem or AssetPostProcess). The Editor Class is just the most common place you'll see SerializedProperty used. but the object doesn't need to be active and selected for you to edit its properties.

    when you do be sure to account for arrays that hold things that are not holding unity objects (an array of ints for example). I'm pretty sure it will simply throw an error for your code (cause you're accessing an objectReference on a SP that holds intvalues instead), but if it didn't it would likely clear the entire array, something you likely don't expect. A quick fix would be to confirm that the array only holds ObjectReferences
    Code (CSharp):
    1. if(list == null)
    2.     return;
    3.  
    4. ...
    5.  
    6. var element = list.GetArrayElementAtIndex(i);
    7. if(element.propertyType!= SerializedPropertyType.ObjectReference)
    8.     continue;
    9. ...
    10.