Working with arrays as SerializedProperty(ies) in editor script

I can’t find a way to access the contents of an array which is a SerializedProperty.
Specifically:

// .... in MyScript ... //
public var myIntArray : int[] = [1, 2, 3, 4];

// ... in another script ... ///
var so : SerializedObject = new SerializedObject(MyScript);
var sp : SerializedProperty = so.FindProperty("myIntArray"); // serialized property of an int array

// In an effort to figure out how this works, I tried:
Debug.Log(sp.propertyType.ToString()); // value = Generic
Debug.Log(sp.hasChildren.ToString()); // value = True
Debug.Log(sp.hasVisibleChildren.ToString()); // value = True
Debug.Log(sp.type.ToString(); // value = vector ????

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!

Okay, so even if I try to get the size or elements of my array as SerializedProperty like this it fails (crashes)

sp = so.FindProperty("myIntArray.length");
sp = so.FindProperty("myIntArray[0]");

// Any attempt to access data in sp after this crashes unity
Debug.Log(sp.intValue); // CRASH

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.

Well, I think I may have found the key:

http://unity3d.com/support/documentation/ScriptReference/SerializedProperty.Next.html

It’s a matter of first getting the serialized property and then iterating through all its elements. I’ll post my final update when I get it all sorted.

BTW, I know of EditorGUI.PropertyField(), but I’m trying to access the array elements individually and not with the drop down GUI tool.

Okay, I found a way. Not the nicest, but whatever:

private var propertyName : String = "propertyName";
private var so : SerializedObject = new SerializedObject(target);

function OnGUI() {
  // Since OnGUI runs continually, I need to "rewind" the index in the SerializedProperty each time back to the named property's index
  
  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

  var arrayLength : int = 0;
  var count : int = 0;
  var arrayElementCount : int = 0;
  
  while(true) {
    // 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.
    
    if(count == 2) { // Iteration 2: This should be the size of the array.
      
      arrayLength = sp.intValue; // get array size
      
    } else if(count > 2) { // Iteration 2+: The remaining should be the array elements
      
      // Do something wonderful with the array here... in my case, create a GUI.IntField
      
      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.

      if(arrayElementCount == arrayLength) // we got the last array element
        break; // stop iterating, we're done
    }
   sp.Next(true); // Step to the next property in the SerializedObject including children
    count++;
  }
}

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?

5 Likes

Thank you for posting your findings! I was just battling with this very same issue and your code has helped me tremendously.

1 Like

I did it this way:

void ArrayGUI(SerializedObject obj,string name)
    {
        int no = obj.FindProperty(name + ".Array.size").intValue;
        EditorGUI.indentLevel = 3;
        int c = EditorGUILayout.IntField("Size", no);
        if (c != no)
            obj.FindProperty(name + ".Array.size").intValue = c;

        for (int i=0;i<no;i++) {
            var prop = obj.FindProperty(string.Format("{0}.Array.data[{1}]", name, i));
            EditorGUILayout.PropertyField(prop);
        }
    }
1 Like

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.

void ArrayGUI(SerializedObject obj, string name)
    {
        int size = obj.FindProperty(name + ".Array.size").intValue;

        int newSize = EditorGUILayout.IntField(name + " Size", size);
		
        if (newSize != size)
            obj.FindProperty(name + ".Array.size").intValue = newSize;
		
		EditorGUI.indentLevel = 3;

        for (int i=0;i<newSize;i++) 
		{
            var prop = obj.FindProperty(string.Format("{0}.Array.data[{1}]", name, i));
            EditorGUILayout.PropertyField(prop);
        }
    }
1 Like

Necropost but whatever. You can also use:

public void InsertArrayElementAtIndex( int index );
public bool MoveArrayElement( int srcIndex, int dstIndex );
public SerializedProperty GetArrayElementAtIndex( int index );

You can set the value in the array index by writing to the SerializedProperty returned by GetArrayElementAtIndex.

2 Likes

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?

Ah it is brillant. So to use it one just has to do this.

public void OnEnable(){
		CVGO = GameObject.FindWithTag ("GameController");
		so = new SerializedObject (CVGO.GetComponent (typeof(CharacterChanger)));
		}

To set it up and then call it in the “OnInspectorGUI” like this.

ArrayGUI (so, "characterNames");

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.

Small improvement using the SerializedProperty of the array var instead of strings.

void ArrayGUI(SerializedObject obj, SerializedProperty _property)
    {
        int size = _property.arraySize;
      
        int newSize = EditorGUILayout.IntField(_property.name + " Size", size);
      
        if (newSize != size)
        {
            _property.arraySize = newSize;
        }
      
        EditorGUI.indentLevel = 3;
      
        for (int i=0;i<newSize;i++)
        {
            var prop = _property.GetArrayElementAtIndex(i);
            EditorGUILayout.PropertyField(prop);
        }
    }

Why not simply get the arraySize by its serializedProperty? :

void ArrayGUI (SerializedProperty property) {
    SerializedProperty arraySizeProp = property.FindPropertyRelative("Array.size");
    EditorGUILayout.PropertyField(arraySizeProp);

    EditorGUI.indentLevel ++;

    for (int i = 0; i < arraySizeProp.intValue; i++) {
        EditorGUILayout.PropertyField(property.GetArrayElementAtIndex(i));
    }

    EditorGUI.indentLevel --;
}
3 Likes

@ThomLaurent_1 Perfect

Another improvement, using a foldout to get the same look as other arrays

    private void DrawPropertyArray(SerializedProperty property, ref bool fold)
    {
        fold = EditorGUILayout.Foldout(fold, property.displayName);
        if (fold)
        {
            SerializedProperty arraySizeProp = property.FindPropertyRelative("Array.size");
            EditorGUILayout.PropertyField(arraySizeProp);

            EditorGUI.indentLevel++;

            for (int i = 0; i < arraySizeProp.intValue; i++)
            {                EditorGUILayout.PropertyField(property.GetArrayElementAtIndex(i));
            }

            EditorGUI.indentLevel--;
        }
    }
3 Likes

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

EditorGUILayout.PropertyField(m_levels.GetArrayElementAtIndex(m_levelIndex));

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.

5775808--609007--Capture.PNG

    void MethodName()
    {
        MonoScript script;
        script = MonoScript.FromMonoBehaviour(target as MonoBehaviour);
        EditorGUI.BeginDisabledGroup(true);
        EditorGUILayout.ObjectField("Script", script, typeof(MonoScript), false);
        EditorGUI.EndDisabledGroup();
    }

Add this as well to make it look exactly the same as the default script

Improving upon @BerengerVolumiq 's modifications:

private static void DrawPropertyArray(SerializedProperty property, ref bool fold) {
    fold = EditorGUILayout.Foldout(fold, new GUIContent(
        property.displayName,
        "These are the waypoints that will be used for the moving object's path."), true);
    if (!fold) return;
    var arraySizeProp = property.FindPropertyRelative("Array.size");
    EditorGUILayout.PropertyField(arraySizeProp);

    EditorGUI.indentLevel++;

    for (var i = 0; i < arraySizeProp.intValue; i++) {
        EditorGUILayout.PropertyField(property.GetArrayElementAtIndex(i));
    }

    EditorGUI.indentLevel--;
}

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.

The only issue right now is that it doesn’t seem to support drag and drop functionality :confused:

Made a post about it here:

Hopefully somebody knows what to do.

An improvement upon @HyperBrid1 's code:

private void MethodName() {
    var script = MonoScript.FromMonoBehaviour(target as MonoBehaviour);
    EditorGUI.BeginDisabledGroup(true);
    EditorGUILayout.ObjectField("Script", script, typeof(MonoScript), false);
    EditorGUI.EndDisabledGroup();
}

Just joining the declaration and init