SerializedObject
SerializedProperty
that represents an array or listThis tutorial comes after the Custom Data tutorial.
This tutorial is for Unity version 4.3 and above. The older version can still be found here.
We start with the finished Custom Data tutorial project, or bycreating a new empty project and importing custom-data.unitypackage.
Then we create a new test script named ListTester with some test arrays, and make a new prefab and prefab instance with it, so we can see it all works as expected.
using UnityEngine; public class ListTester : MonoBehaviour { public int[] integers; public Vector3[] vectors; public ColorPoint[] colorPoints; public Transform[] objects; }New test object, with wide inspector.
UnityEditor.Editor
, and apply the
UnityEditor.CustomEditor
attribute to tell Unity that we want it to do the drawing for our component.
using UnityEditor; using UnityEngine; [CustomEditor(typeof(ListTester))] public class ListTesterInspector : Editor { }Custom inspector script.
OnInspectorGUI
method of the
Editor
class. Leaving the method empty will result in an empty inspector as well.
public override void OnInspectorGUI () { }Empty inspector.
SerializedObject
instead of a single
SerializedProperty
. Secondly, an instance of the editor exists as long as the objectstays selected, keeping a reference to its data instead of getting it via a method parameter.Finally, we can use
EditorGUILayout
, which takes care of positioning for us.
We can get to the serialized object via the serializedObject
property.To prepare it for editing, we must first synchronize it with the componentit represents, by calling its Update
method. Then we can show the properties.And after we are done, we have tocommit any changes via its ApplyModifiedProperties
method. This also takes careof Unity's undo history. In between these two is where we'll draw our properties.
public override void OnInspectorGUI () { serializedObject.Update(); EditorGUILayout.PropertyField(serializedObject.FindProperty("integers")); EditorGUILayout.PropertyField(serializedObject.FindProperty("vectors")); EditorGUILayout.PropertyField(serializedObject.FindProperty("colorPoints")); EditorGUILayout.PropertyField(serializedObject.FindProperty("objects")); serializedObject.ApplyModifiedProperties(); }Inspector with empty properties.
PropertyField
doesn't show any children – like array elements – unless we tell it to do so.
public override void OnInspectorGUI () { serializedObject.Update(); EditorGUILayout.PropertyField(serializedObject.FindProperty("integers"), true); EditorGUILayout.PropertyField(serializedObject.FindProperty("vectors"), true); EditorGUILayout.PropertyField(serializedObject.FindProperty("colorPoints"), true); EditorGUILayout.PropertyField(serializedObject.FindProperty("objects"), true); serializedObject.ApplyModifiedProperties(); }Inspector with children.
PropertyField
method. We will name this method
Show and put it in itsown static utility class, so we can use it wherever we want. We'll name this class
EditorList and place it in the
Editor folder.
using UnityEditor; using UnityEngine; public static class EditorList { public static void Show (SerializedProperty list) { } }EditorList script.
public override void OnInspectorGUI () { serializedObject.Update(); EditorList.Show(serializedObject.FindProperty("integers")); EditorList.Show(serializedObject.FindProperty("vectors")); EditorList.Show(serializedObject.FindProperty("colorPoints")); EditorList.Show(serializedObject.FindProperty("objects")); serializedObject.ApplyModifiedProperties(); }
EditorGUILayout.PropertyField
without having itshow the children of the list. Then we can show the list elements ourselves with help of the
arraySize
property and the
GetArrayElementAtIndex
method of
SerializedProperty
. We'll leave the size for later.
public static void Show (SerializedProperty list) {
EditorGUILayout.PropertyField(list);
for (int i = 0; i < list.arraySize; i++) {
EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i));
}
}
Lists without indented elements.
public static void Show (SerializedProperty list) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; for (int i = 0; i < list.arraySize; i++) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } EditorGUI.indentLevel -= 1; }Messed up indenting.
ColorPointDrawer
behaves well.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { int oldIndentLevel = EditorGUI.indentLevel; label = EditorGUI.BeginProperty(position, label, property); Rect contentPosition = EditorGUI.PrefixLabel(position, label); if (position.height > 16f) { position.height = 16f; EditorGUI.indentLevel += 1; contentPosition = EditorGUI.IndentedRect(position); contentPosition.y += 18f; } contentPosition.width *= 0.75f; EditorGUI.indentLevel = 0; EditorGUI.PropertyField(contentPosition, property.FindPropertyRelative("position"), GUIContent.none); contentPosition.x += contentPosition.width; contentPosition.width /= 3f; EditorGUIUtility.labelWidth = 14f; EditorGUI.PropertyField(contentPosition, property.FindPropertyRelative("color"), new GUIContent("C")); EditorGUI.EndProperty(); EditorGUI.indentLevel = oldIndentLevel; }Correct indenting, but no collapsing.
isExpanded
property of our list.
public static void Show (SerializedProperty list) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; if (list.isExpanded) { for (int i = 0; i < list.arraySize; i++) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } } EditorGUI.indentLevel -= 1; }Correctly collapsing.
arraySize
here?public static void Show (SerializedProperty list) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; if (list.isExpanded) { EditorGUILayout.PropertyField(list.FindPropertyRelative("Array.size")); for (int i = 0; i < list.arraySize; i++) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } } EditorGUI.indentLevel -= 1; }Complete lists.
showListSize
work?public static void Show (SerializedProperty list, bool showListSize = true) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; if (list.isExpanded) { if (showListSize) { EditorGUILayout.PropertyField(list.FindPropertyRelative("Array.size")); } for (int i = 0; i < list.arraySize; i++) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } } EditorGUI.indentLevel -= 1; }
public override void OnInspectorGUI () { serializedObject.Update(); EditorList.Show(serializedObject.FindProperty("integers")); EditorList.Show(serializedObject.FindProperty("vectors")); EditorList.Show(serializedObject.FindProperty("colorPoints"), false); EditorList.Show(serializedObject.FindProperty("objects"), false); serializedObject.ApplyModifiedProperties(); }Hiding some of the list sizes.
public static void Show (SerializedProperty list, bool showListSize = true, bool showListLabel = true) { if (showListLabel) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; } if (!showListLabel || list.isExpanded) { if (showListSize) { EditorGUILayout.PropertyField(list.FindPropertyRelative("Array.size")); } for (int i = 0; i < list.arraySize; i++) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } } if (showListLabel) { EditorGUI.indentLevel -= 1; } }
public override void OnInspectorGUI () { serializedObject.Update(); EditorList.Show(serializedObject.FindProperty("integers"), true, false); EditorList.Show(serializedObject.FindProperty("vectors")); EditorList.Show(serializedObject.FindProperty("colorPoints"), false, false); EditorList.Show(serializedObject.FindProperty("objects"), false); serializedObject.ApplyModifiedProperties(); }Hiding some of the list labels.
The first thing we need to do is create an enumeration of all our options. We name itEditorListOption and give it the System.Flags
attribute. We place it in itsown script file or in the same script as EditorList
, but outside of the class.
Flags
attribute required?using UnityEditor; using UnityEngine; using System; [Flags] public enum EditorListOption { }
|
.
[Flags] public enum EditorListOption { None = 0, ListSize = 1, ListLabel = 2, Default = ListSize | ListLabel }
Show
method can now be replaced with a single optionsparameter. Then we'll extract the individual options with the help of the bitwise AND operator
&
and store them in local variables to keep things clear.
public static void Show (SerializedProperty list, EditorListOption options = EditorListOption.Default) { bool showListLabel = (options & EditorListOption.ListLabel) != 0, showListSize = (options & EditorListOption.ListSize) != 0; if (showListLabel) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; } if (!showListLabel || list.isExpanded) { if (showListSize) { EditorGUILayout.PropertyField(list.FindPropertyRelative("Array.size")); } for (int i = 0; i < list.arraySize; i++) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } } if (showListLabel) { EditorGUI.indentLevel -= 1; } }
public override void OnInspectorGUI () { serializedObject.Update(); EditorList.Show(serializedObject.FindProperty("integers"), EditorListOption.ListSize); EditorList.Show(serializedObject.FindProperty("vectors")); EditorList.Show(serializedObject.FindProperty("colorPoints"), EditorListOption.None); EditorList.Show(serializedObject.FindProperty("objects"), EditorListOption.ListLabel); serializedObject.ApplyModifiedProperties(); }
[Flags] public enum EditorListOption { None = 0, ListSize = 1, ListLabel = 2, ElementLabels = 4, Default = ListSize | ListLabel | ElementLabels, NoElementLabels = ListSize | ListLabel }
Show
method is extract this option and perform a simplecheck. Let's also move the element loop to its own private method, for clarity.
public static void Show (SerializedProperty list, EditorListOption options = EditorListOption.Default) { bool showListLabel = (options & EditorListOption.ListLabel) != 0, showListSize = (options & EditorListOption.ListSize) != 0; if (showListLabel) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; } if (!showListLabel || list.isExpanded) { if (showListSize) { EditorGUILayout.PropertyField(list.FindPropertyRelative("Array.size")); } ShowElements(list, options); } if (showListLabel) { EditorGUI.indentLevel -= 1; } } private static void ShowElements (SerializedProperty list, EditorListOption options) { bool showElementLabels = (options & EditorListOption.ElementLabels) != 0; for (int i = 0; i < list.arraySize; i++) { if (showElementLabels) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } else { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i), GUIContent.none); } } }Hiding some of the element labels, wide and narrow.
ColorPointDrawer
does not claim an extra line when it does not receive a label.
public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { return label != GUIContent.none && Screen.width < 333 ? (16f + 18f) : 16f; }No longer needlessly claiming extra lines.
First we'll add an option for buttons, and also a convenient option to activate everything.
[Flags] public enum EditorListOption { None = 0, ListSize = 1, ListLabel = 2, ElementLabels = 4, Buttons = 8, Default = ListSize | ListLabel | ElementLabels, NoElementLabels = ListSize | ListLabel, All = Default | Buttons }
We predefine static GUIContent
for these buttons and include handy tooltips as well.We also add a separate method for showing the buttons and call it after each element, if desired.
static
instead of const
?private static GUIContent moveButtonContent = new GUIContent("\u21b4", "move down"), duplicateButtonContent = new GUIContent("+", "duplicate"), deleteButtonContent = new GUIContent("-", "delete"); private static void ShowElements (SerializedProperty list, EditorListOption options) { bool showElementLabels = (options & EditorListOption.ElementLabels) != 0, showButtons = (options & EditorListOption.Buttons) != 0; for (int i = 0; i < list.arraySize; i++) { if (showElementLabels) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } else { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i), GUIContent.none); } if (showButtons) { ShowButtons(); } } } private static void ShowButtons () { GUILayout.Button(moveButtonContent); GUILayout.Button(duplicateButtonContent); GUILayout.Button(deleteButtonContent); }
public override void OnInspectorGUI () { serializedObject.Update(); EditorList.Show(serializedObject.FindProperty("integers"), EditorListOption.ListSize); EditorList.Show(serializedObject.FindProperty("vectors")); EditorList.Show(serializedObject.FindProperty("colorPoints"), EditorListOption.Buttons); EditorList.Show( serializedObject.FindProperty("objects"), EditorListOption.ListLabel | EditorListOption.Buttons); serializedObject.ApplyModifiedProperties(); }Quite huge buttons.
EditorGUILayout.BeginHorizontal
and
EditorGUILayout.EndHorizontal
.
private static void ShowElements (SerializedProperty list, EditorListOption options) { bool showElementLabels = (options & EditorListOption.ElementLabels) != 0, showButtons = (options & EditorListOption.Buttons) != 0; for (int i = 0; i < list.arraySize; i++) { if (showButtons) { EditorGUILayout.BeginHorizontal(); } if (showElementLabels) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } else { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i), GUIContent.none); } if (showButtons) { ShowButtons(); EditorGUILayout.EndHorizontal(); } } }Pretty large buttons.
private static GUILayoutOption miniButtonWidth = GUILayout.Width(20f);
private static void ShowButtons () {
GUILayout.Button(moveButtonContent, EditorStyles.miniButtonLeft, miniButtonWidth);
GUILayout.Button(duplicateButtonContent, EditorStyles.miniButtonMid, miniButtonWidth);
GUILayout.Button(deleteButtonContent, EditorStyles.miniButtonRight, miniButtonWidth);
}
Mini buttons.
Fortunately, adding functionalityto the buttons is very simple, as we can directly use the methods for array manipulation providedby SerializedProperty
. We need the list and the current element index for this towork, so we add them as parameters to our ShowButtons
method and pass them alonginside the loop of ShowElements
.
Button
work?private static void ShowElements (SerializedProperty list, EditorListOption options) { bool showElementLabels = (options & EditorListOption.ElementLabels) != 0, showButtons = (options & EditorListOption.Buttons) != 0; for (int i = 0; i < list.arraySize; i++) { if (showButtons) { EditorGUILayout.BeginHorizontal(); } if (showElementLabels) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } else { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i), GUIContent.none); } if (showButtons) { ShowButtons(list, i); EditorGUILayout.EndHorizontal(); } } } private static void ShowButtons (SerializedProperty list, int index) { if (GUILayout.Button(moveButtonContent, EditorStyles.miniButtonLeft, miniButtonWidth)) { list.MoveArrayElement(index, index + 1); } if (GUILayout.Button(duplicateButtonContent, EditorStyles.miniButtonMid, miniButtonWidth)) { list.InsertArrayElementAtIndex(index); } if (GUILayout.Button(deleteButtonContent, EditorStyles.miniButtonRight, miniButtonWidth)) { list.DeleteArrayElementAtIndex(index); } }
While this is how Unity handles deletion in this case, it is weird. Instead, we want theelement to always be removed, not sometimes cleared. We can enforce this by checking whether thelist's size has remained the same after deleting the element. If so, it has only been cleared andwe should delete it again, for real this time.
private static void ShowButtons (SerializedProperty list, int index) {
if (GUILayout.Button(moveButtonContent, EditorStyles.miniButtonLeft, miniButtonWidth)) {
list.MoveArrayElement(index, index + 1);
}
if (GUILayout.Button(duplicateButtonContent, EditorStyles.miniButtonMid, miniButtonWidth)) {
list.InsertArrayElementAtIndex(index);
}
if (GUILayout.Button(deleteButtonContent, EditorStyles.miniButtonRight, miniButtonWidth)) {
int oldSize = list.arraySize;
list.DeleteArrayElementAtIndex(index);
if (list.arraySize == oldSize) {
list.DeleteArrayElementAtIndex(index);
}
}
}
private static GUIContent moveButtonContent = new GUIContent("\u21b4", "move down"), duplicateButtonContent = new GUIContent("+", "duplicate"), deleteButtonContent = new GUIContent("-", "delete"), addButtonContent = new GUIContent("+", "add element"); private static void ShowElements (SerializedProperty list, EditorListOption options) { bool showElementLabels = (options & EditorListOption.ElementLabels) != 0, showButtons = (options & EditorListOption.Buttons) != 0; for (int i = 0; i < list.arraySize; i++) { if (showButtons) { EditorGUILayout.BeginHorizontal(); } if (showElementLabels) { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i)); } else { EditorGUILayout.PropertyField(list.GetArrayElementAtIndex(i), GUIContent.none); } if (showButtons) { ShowButtons(list, i); EditorGUILayout.EndHorizontal(); } } if (showButtons && list.arraySize == 0 && GUILayout.Button(addButtonContent, EditorStyles.miniButton)) { list.arraySize += 1; } }A big add button.
ListTester
that is not a list.
public int notAList;
ListTestInspector
.
public override void OnInspectorGUI () { serializedObject.Update(); EditorList.Show(serializedObject.FindProperty("integers"), EditorListOption.ListSize); EditorList.Show(serializedObject.FindProperty("vectors")); EditorList.Show(serializedObject.FindProperty("colorPoints"), EditorListOption.Buttons); EditorList.Show( serializedObject.FindProperty("objects"), EditorListOption.ListLabel | EditorListOption.Buttons); EditorList.Show(serializedObject.FindProperty("notAList")); serializedObject.ApplyModifiedProperties(); }Not a list shown.
public static void Show (SerializedProperty list, EditorListOption options = EditorListOption.Default) { if (!list.isArray) { EditorGUILayout.HelpBox(list.name + " is neither an array nor a list!", MessageType.Error); return; } bool showListLabel = (options & EditorListOption.ListLabel) != 0, showListSize = (options & EditorListOption.ListSize) != 0; if (showListLabel) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; } if (!showListLabel || list.isExpanded) { if (showListSize) { EditorGUILayout.PropertyField(list.FindPropertyRelative("Array.size")); } ShowElements(list, options); } if (showListLabel) { EditorGUI.indentLevel -= 1; } }Only lists allowed.
CanEditMultipleObjects
attribute to our
ListTesterInspector
.
[CustomEditor(typeof(ListTester)), CanEditMultipleObjects]Multi-object editing.
public static void Show (SerializedProperty list, EditorListOption options = EditorListOption.Default) { if (!list.isArray) { EditorGUILayout.HelpBox(list.name + " is neither an array nor a list!", MessageType.Error); return; } bool showListLabel = (options & EditorListOption.ListLabel) != 0, showListSize = (options & EditorListOption.ListSize) != 0; if (showListLabel) { EditorGUILayout.PropertyField(list); EditorGUI.indentLevel += 1; } if (!showListLabel || list.isExpanded) { SerializedProperty size = list.FindPropertyRelative("Array.size"); if (showListSize) { EditorGUILayout.PropertyField(size); } if (size.hasMultipleDifferentValues) { EditorGUILayout.HelpBox("Not showing lists with different sizes.", MessageType.Info); } else { ShowElements(list, options); } } if (showListLabel) { EditorGUI.indentLevel -= 1; } }Divergent lists will not be shown.
arraySize
here?
SerializedProperty
has an
arraySize
property, which is a convenientway to get the size of the array or list represented by the property. Why not use it here?
At this point we don't want a simple integer, we want the size as a SerializedProperty
so it's easy to edit. Fortunately, we can extract the size as a property via the special relative path Array.size.We will use arraySize
in all other cases.
showListSize
work?
showListSize
as an optional parameter by assigning a constant value toit. This is equivalent to adding a second method declaration without the argument, then callingthe original method with the constant value as an argument.
void DoWork (bool fast = true) {}
is the same as
void DoWork (bool fast) {} void DoWork () { DoWork(true); }
Be advised that using optional parameters can lead to weird errors on some platform builds.I only use them in editor scripts, which won't be included in builds.
Flags
attribute required?
Flags
attribute, it will work fine without. What theattribute does is signify that you are using the enumeration for flags that can be combinedtogether, which will affect how such values are converted to strings, among other things.
As each option gets its own digit, their values must be set to 1, 10, 100, 1000, 10000, and so on. However,we are dealing with binary numbers here, while in our scripts we write decimal numbers. So we haveto use powers of two instead of powers of ten, writing 1, 2, 4, 8, 16, etcetera.
static
instead of
const
?
EditorList
code is accessed, and then stay as they are as long as we don't mess with them.
Button
work?
GUI.Button
both shows a button and returns whether it was clicked. Soyou typically call it inside an
if
statement and perform the necessary work in the corresponding code block.
What actually happens is that your own GUI method, in this case OnInspectorGUI
,gets called far more often than just once. It gets called when performing layout, when repainting, andwhenever a significant GUI event happens, which is quite often. Only when a mouse click event comes along that is consumedby the button, will it return true
.
To get an idea of how often the GUI methods get called, put Debug.Log(Event.current);
at the start of your OnInspectorGUI
method, then fool around a bit in the editor and watch the console.
Usually you need not worry about this, but be aware of it when performing heavy work like generatingtextures. You don't want to do that dozens of times per second if you don't need to.
SerializedProperty
, the new element will be aduplicate of the element just above it. If there's no other element, it gets default values.