Search Unity

Working with arrays as SerializedProperty(ies) in editor script

Discussion in 'Scripting' started by guavaman, Jul 20, 2011.

  1. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    5,627
    I can't find a way to access the contents of an array which is a SerializedProperty.
    Specifically:

    Code (csharp):
    1.  
    2. // .... in MyScript ... //
    3. public var myIntArray : int[] = [1, 2, 3, 4];
    4.  
    5. // ... in another script ... ///
    6. var so : SerializedObject = new SerializedObject(MyScript);
    7. var sp : SerializedProperty = so.FindProperty("myIntArray"); // serialized property of an int array
    8.  
    9. // In an effort to figure out how this works, I tried:
    10. Debug.Log(sp.propertyType.ToString()); // value = Generic
    11. Debug.Log(sp.hasChildren.ToString()); // value = True
    12. Debug.Log(sp.hasVisibleChildren.ToString()); // value = True
    13. Debug.Log(sp.type.ToString(); // value = vector ????
    14.  
    The returned SerializedPropertyType "Generic" is not a listed type in SeralizedPropertyType. The returned value for sp.type "vector" (not vector2 or vector3) is also confusing.

    I can't figure out how to access the values in the array from the SerializedProperty. Am I understanding this incorrectly? Do I have to so.FindProperty(myIntArray[#]) for each element in the array separately? If so, why is it not giving me an error when I do it for the whole array?

    Edit: Forgot to ask, hasChildren and hasVisibleChildren are true. Not even sure what this is referring to. Docs for these has no information at all. How do I access these children?

    Thanks!
     
    Last edited: Jul 20, 2011
  2. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    5,627
    Okay, so even if I try to get the size or elements of my array as SerializedProperty like this it fails (crashes)

    Code (csharp):
    1.  
    2. sp = so.FindProperty("myIntArray.length");
    3. sp = so.FindProperty("myIntArray[0]");
    4.  
    5. // Any attempt to access data in sp after this crashes unity
    6. Debug.Log(sp.intValue); // CRASH
    7.  
    SerializeField article states:

    They specifically mention arrays being serializable more than one time on that page. Or course this is the case with public arrays in the inspector -- the data gets saved, and one can override array values in the inspector for prefabs. However there appears to be no way of accessing arrays through SerializedProperty even though the inspector clearly does this when editing arrays.

    Heck even SerializedPropertyType contains a SerializedPropertyType.ArraySize value, so obviously some sort of array handling exists within SerializedObject and SerializedProperty. Is it just undocumented?

    Useless info:
    I'm working on a prefab property editor which will let me change certain values in hundreds of prefabs (the master prefabs, not the instances in the scene) via a spreadsheed like view. If I just simply edit the values directly in the prefabs, all instances of those prefabs in the scene will not inherit the new changes. However, if I change the values through SerializedProperty, all instanced prefabs in the world automatically update values that haven't been previously overridden which is the desired effect. However, not being able to edit arrays like this pretty much kills the effectiveness of this tool as I rely on arrays quite heavily.
     
    Last edited: Jul 20, 2011
  3. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    5,627
    Last edited: Jul 20, 2011
  4. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    5,627
    Okay, I found a way. Not the nicest, but whatever:

    Code (csharp):
    1.  
    2. private var propertyName : String = "propertyName";
    3. private var so : SerializedObject = new SerializedObject(target);
    4.  
    5. function OnGUI() {
    6.   // Since OnGUI runs continually, I need to "rewind" the index in the SerializedProperty each time back to the named property's index
    7.  
    8.   var sp : SerializedProperty = so.FindProperty(propertyName); // Get SP again each time to "rewind" it to proper starting index. If you don't, Next() will continue off from the last position
    9.  
    10.   var arrayLength : int = 0;
    11.   var count : int = 0;
    12.   var arrayElementCount : int = 0;
    13.  
    14.   while(true) {
    15.     // Iteration 0  1 do not contain the data we need. This was verified by Debug.Log(sp.propertyType) through every iteration to find where the array length and starting indices were stored.
    16.    
    17.     if(count == 2) { // Iteration 2: This should be the size of the array.
    18.      
    19.       arrayLength = sp.intValue; // get array size
    20.      
    21.     } else if(count > 2) { // Iteration 2+: The remaining should be the array elements
    22.      
    23.       // Do something wonderful with the array here... in my case, create a GUI.IntField
    24.      
    25.       arrayElementCount++; // Increment array element count so we know when to stop. sp.CountRemaining() will not help us because it gives the count of all remaining properties in the entire SO, not the children of this SP.
    26.  
    27.       if(arrayElementCount == arrayLength) // we got the last array element
    28.         break; // stop iterating, we're done
    29.     }
    30.    sp.Next(true); // Step to the next property in the SerializedObject including children
    31.     count++;
    32.   }
    33. }
    Well anyway, it's messy and could be cleaned up. It's kinda slow too having to do all this iteration with Next(), but whatever.

    Ain't it great talkin to yourself?
     
    Last edited: Jul 30, 2011
    weareandrei, nikk98, Sehee26 and 2 others like this.
  5. VoxelBoy

    VoxelBoy

    Joined:
    Nov 7, 2008
    Posts:
    240
    Thank you for posting your findings! I was just battling with this very same issue and your code has helped me tremendously.
     
    weareandrei likes this.
  6. Jake-L

    Jake-L

    Joined:
    Oct 17, 2009
    Posts:
    397
    I did it this way:

    Code (csharp):
    1. void ArrayGUI(SerializedObject obj,string name)
    2.     {
    3.         int no = obj.FindProperty(name + ".Array.size").intValue;
    4.         EditorGUI.indentLevel = 3;
    5.         int c = EditorGUILayout.IntField("Size", no);
    6.         if (c != no)
    7.             obj.FindProperty(name + ".Array.size").intValue = c;
    8.  
    9.         for (int i=0;i<no;i++) {
    10.             var prop = obj.FindProperty(string.Format("{0}.Array.data[{1}]", name, i));
    11.             EditorGUILayout.PropertyField(prop);
    12.         }
    13.     }
     
    vedram likes this.
  7. minimatt32

    minimatt32

    Joined:
    Jul 8, 2012
    Posts:
    1
    Thanks Jake that was just what I needed!

    For anyone else looking for a solution.
    I made a small adjustment as you could only increase the size of the array or list.
    The code below will increase and decrease the size.
    All I changed was the for loop and some text display.

    Code (csharp):
    1.  
    2. void ArrayGUI(SerializedObject obj, string name)
    3.     {
    4.         int size = obj.FindProperty(name + ".Array.size").intValue;
    5.  
    6.         int newSize = EditorGUILayout.IntField(name + " Size", size);
    7.        
    8.         if (newSize != size)
    9.             obj.FindProperty(name + ".Array.size").intValue = newSize;
    10.        
    11.         EditorGUI.indentLevel = 3;
    12.  
    13.         for (int i=0;i<newSize;i++)
    14.         {
    15.             var prop = obj.FindProperty(string.Format("{0}.Array.data[{1}]", name, i));
    16.             EditorGUILayout.PropertyField(prop);
    17.         }
    18.     }
     
    idbrii likes this.
  8. spectre1989

    spectre1989

    Joined:
    Oct 6, 2009
    Posts:
    125
    Necropost but whatever. You can also use:
    Code (csharp):
    1. public void InsertArrayElementAtIndex( int index );
    2. public bool MoveArrayElement( int srcIndex, int dstIndex );
    3. public SerializedProperty GetArrayElementAtIndex( int index );
    You can set the value in the array index by writing to the SerializedProperty returned by GetArrayElementAtIndex.
     
    noio and vedram like this.
  9. Anxo

    Anxo

    Joined:
    Jan 7, 2014
    Posts:
    13
    Sorry for the noob question but the way Guavaman wrote it in the GUI function I can understand as it would display it in the editor but you guys wrote your code in a separate function "ArrayGUI" How would you implement it so that it would show the array in the editor GUI?

     
  10. Anxo

    Anxo

    Joined:
    Jan 7, 2014
    Posts:
    13
    Ah it is brillant. So to use it one just has to do this.

    Code (csharp):
    1.  
    2. public void OnEnable(){
    3.         CVGO = GameObject.FindWithTag ("GameController");
    4.         so = new SerializedObject (CVGO.GetComponent (typeof(CharacterChanger)));
    5.         }
    6.  
    To set it up and then call it in the "OnInspectorGUI" like this.

    Code (csharp):
    1.  
    2. ArrayGUI (so, "characterNames");
    3.  
    4.  
    But now the problem I am having is that when I change the array in editor, it defaults back after I hit play. So my changes do not take effect.
     
  11. elhispano

    elhispano

    Joined:
    Jan 23, 2012
    Posts:
    52
    Small improvement using the SerializedProperty of the array var instead of strings.

    Code (CSharp):
    1.  
    2. void ArrayGUI(SerializedObject obj, SerializedProperty _property)
    3.     {
    4.         int size = _property.arraySize;
    5.      
    6.         int newSize = EditorGUILayout.IntField(_property.name + " Size", size);
    7.      
    8.         if (newSize != size)
    9.         {
    10.             _property.arraySize = newSize;
    11.         }
    12.      
    13.         EditorGUI.indentLevel = 3;
    14.      
    15.         for (int i=0;i<newSize;i++)
    16.         {
    17.             var prop = _property.GetArrayElementAtIndex(i);
    18.             EditorGUILayout.PropertyField(prop);
    19.         }
    20.     }
    21.  
     
  12. ThomLaurent

    ThomLaurent

    Joined:
    Sep 27, 2014
    Posts:
    10
    Why not simply get the `arraySize` by its `serializedProperty`? :

    Code (CSharp):
    1. void ArrayGUI (SerializedProperty property) {
    2.     SerializedProperty arraySizeProp = property.FindPropertyRelative("Array.size");
    3.     EditorGUILayout.PropertyField(arraySizeProp);
    4.  
    5.     EditorGUI.indentLevel ++;
    6.  
    7.     for (int i = 0; i < arraySizeProp.intValue; i++) {
    8.         EditorGUILayout.PropertyField(property.GetArrayElementAtIndex(i));
    9.     }
    10.  
    11.     EditorGUI.indentLevel --;
    12. }
     
    BillianoDev and HyperBrid like this.
  13. zero_null

    zero_null

    Joined:
    Mar 11, 2014
    Posts:
    159
  14. BerengerVolumiq

    BerengerVolumiq

    Joined:
    Feb 6, 2017
    Posts:
    14
    Another improvement, using a foldout to get the same look as other arrays

    Code (CSharp):
    1.  
    2.     private void DrawPropertyArray(SerializedProperty property, ref bool fold)
    3.     {
    4.         fold = EditorGUILayout.Foldout(fold, property.displayName);
    5.         if (fold)
    6.         {
    7.             SerializedProperty arraySizeProp = property.FindPropertyRelative("Array.size");
    8.             EditorGUILayout.PropertyField(arraySizeProp);
    9.  
    10.             EditorGUI.indentLevel++;
    11.  
    12.             for (int i = 0; i < arraySizeProp.intValue; i++)
    13.             {                EditorGUILayout.PropertyField(property.GetArrayElementAtIndex(i));
    14.             }
    15.  
    16.             EditorGUI.indentLevel--;
    17.         }
    18.     }
     
    HyperBrid, radiatoryang and ftejada like this.
  15. CharlieBudd

    CharlieBudd

    Joined:
    Jul 24, 2017
    Posts:
    17
    Seems this method now results in an element "N" fold out being displayed in the inspector (see screen shot). I'm using it to display a level editor for an array of levels one level at a time

    Code (CSharp):
    1.  
    2. EditorGUILayout.PropertyField(m_levels.GetArrayElementAtIndex(m_levelIndex));
    3.  
    maybe unity have changed how arrays are serialised? seems odd and is a bit annoying. You can remove the Element label using GUIConent.none but not the foldout icon.
     

    Attached Files:

  16. HyperBrid

    HyperBrid

    Joined:
    Oct 29, 2019
    Posts:
    27
    Code (CSharp):
    1.     void MethodName()
    2.     {
    3.         MonoScript script;
    4.         script = MonoScript.FromMonoBehaviour(target as MonoBehaviour);
    5.         EditorGUI.BeginDisabledGroup(true);
    6.         EditorGUILayout.ObjectField("Script", script, typeof(MonoScript), false);
    7.         EditorGUI.EndDisabledGroup();
    8.     }
    Add this as well to make it look exactly the same as the default script
     
  17. SmushyTaco

    SmushyTaco

    Joined:
    Apr 7, 2020
    Posts:
    88
    Improving upon @BerengerVolumiq's modifications:

    Code (CSharp):
    1. private static void DrawPropertyArray(SerializedProperty property, ref bool fold) {
    2.     fold = EditorGUILayout.Foldout(fold, new GUIContent(
    3.         property.displayName,
    4.         "These are the waypoints that will be used for the moving object's path."), true);
    5.     if (!fold) return;
    6.     var arraySizeProp = property.FindPropertyRelative("Array.size");
    7.     EditorGUILayout.PropertyField(arraySizeProp);
    8.  
    9.     EditorGUI.indentLevel++;
    10.  
    11.     for (var i = 0; i < arraySizeProp.intValue; i++) {
    12.         EditorGUILayout.PropertyField(property.GetArrayElementAtIndex(i));
    13.     }
    14.  
    15.     EditorGUI.indentLevel--;
    16. }
    I added a place for a tooltip (You could put property.tooltip there too if you want), I added true to the end of the fold parameter so clicking the array triangle also folds and unfolds the array, I used var where possible, I reduced nesting by inverting the fold if statement, and I made the function static.
     
    Last edited: May 5, 2020
  18. SmushyTaco

    SmushyTaco

    Joined:
    Apr 7, 2020
    Posts:
    88
    The only issue right now is that it doesn't seem to support drag and drop functionality :/
     
  19. SmushyTaco

    SmushyTaco

    Joined:
    Apr 7, 2020
    Posts:
    88
  20. SmushyTaco

    SmushyTaco

    Joined:
    Apr 7, 2020
    Posts:
    88
    An improvement upon @HyperBrid's code:

    Code (CSharp):
    1. private void MethodName() {
    2.     var script = MonoScript.FromMonoBehaviour(target as MonoBehaviour);
    3.     EditorGUI.BeginDisabledGroup(true);
    4.     EditorGUILayout.ObjectField("Script", script, typeof(MonoScript), false);
    5.     EditorGUI.EndDisabledGroup();
    6. }
    Just joining the declaration and init
     
  21. hk1ll3r

    hk1ll3r

    Joined:
    Sep 13, 2018
    Posts:
    88
    To get nice custom editors for arrays/lists, Unity has an internal class called ReorderableList. I'm still playing around with it, you can learn more on it here:
    Here is a blog post explaining how to use it:
    https://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/

    Here is a github repo that seems to make it easily accessible:
    https://github.com/garettbass/UnityExtensions.ArrayDrawer

    I'm still figuring this out myself, but this seems to be the list drawer that is used in many assets.
     
  22. StormMuller

    StormMuller

    Joined:
    Jun 2, 2015
    Posts:
    17
    I can't seem to get rid of the "Element 0, Element 1,..." labels in the inspector.

    Is is possible?

    The expand arrow icon thing has been removed but not the element labels.

    I've attached a screenshot of the result and here is the inspector:
    Code (CSharp):
    1. public class SomeContainerEditor : Editor
    2.     {
    3.         SerializedProperty someArrayProperty;
    4.  
    5.         public void OnEnable()
    6.         {
    7.             someArrayProperty = serializedObject.FindProperty("someArrayProperty");
    8.         }
    9.  
    10.         public override void OnInspectorGUI()
    11.         {
    12.             serializedObject.Update();
    13.  
    14.             for (int i = 0; i < someArrayProperty.arraySize; i++)
    15.             {
    16.                 var someArrayItem= someArrayProperty.GetArrayElementAtIndex(i);
    17.  
    18.                 EditorGUILayout.PropertyField(someArrayItem);
    19.             }
    20.  
    21.             serializedObject.ApplyModifiedProperties();
    22.         }
    23. }
    upload_2020-7-22_23-52-51.png

    As you can see above, it's an array of a custom Type which I've created a property drawer for. The property drawer seems to be working as intended.
     
  23. StormMuller

    StormMuller

    Joined:
    Jun 2, 2015
    Posts:
    17
    I'm assuming there is some sort of information that gets serialized in the
    someArrayItem
    property that indicates that it's part of an array and not just a plain instance?
     
    Last edited: Jul 23, 2020
  24. StormMuller

    StormMuller

    Joined:
    Jun 2, 2015
    Posts:
    17
    I changed
    EditorGUILayout.PropertyField(controllerStringProperty);
    to
    EditorGUILayout.PropertyField(controllerStringProperty, GUIContent.none);
    works like a charm
     
    KimberleyC and methusalah999 like this.
  25. nikk98

    nikk98

    Joined:
    Mar 8, 2021
    Posts:
    43
    This thread helped me a lot. Here's a struct that encapsulates access to an array property. Works for updating enum values in bulk, but can be adjusted to handle any other types.


    Code (CSharp):
    1.  
    2. #if UNITY_EDITOR
    3. using System;
    4. using UnityEditor;
    5.  
    6. public struct ArraySerializedProperty {
    7.     private SerializedObject _serializedObject;
    8.     private string _name;
    9.  
    10.     public void SetLength(int size) {
    11.         if (size == Length()) return;
    12.         GetArraySizeProperty().intValue = size;
    13.     }
    14.  
    15.     public int Length() {
    16.         return GetArraySizeProperty().intValue;
    17.     }
    18.  
    19.     public void SetEnumIntValue(int i, int enumValueIndex) {
    20.         GetArrayElementProperty(i).enumValueIndex = enumValueIndex;
    21.     }
    22.  
    23.     public void UpdateEnumArray<T>(T[] values) where T : Enum {
    24.         if (Length() != values.Length) {
    25.             SetLength(values.Length);
    26.         }
    27.  
    28.         for (var i = 0; i < values.Length; i++) {
    29.             var value = values[i];
    30.             var enumValueIndex = (int)(object)value;
    31.             SetEnumIntValue(i, enumValueIndex);
    32.         }
    33.     }
    34.  
    35.     private SerializedProperty GetArraySizeProperty() {
    36.         return _serializedObject.FindProperty($"{_name}.Array.size");
    37.     }
    38.  
    39.     private SerializedProperty GetArrayElementProperty(int i) {
    40.         return _serializedObject.FindProperty($"{_name}.Array.data[{i}]");
    41.     }
    42.  
    43.     public static ArraySerializedProperty Create(SerializedObject serializedObject, string name) {
    44.         return new ArraySerializedProperty {
    45.             _name = name,
    46.             _serializedObject = serializedObject
    47.         };
    48.     }
    49. }
    50. #endif
    51.  
    Also can be found in this gist: https://gist.github.com/nioan/67ec261ade471b95c3e96cc6dcff2bb3

    Usage:
    Code (CSharp):
    1. SomeEnum[] values = ....
    2. var  someEnumArrayProperty = ArraySerializedProperty.Create(serializedObject, "_someEnumArray");
    3. someEnumArrayPropery.UpdateEnumArray(values);
     
    Last edited: May 23, 2022
    Hikiko66 likes this.