本文转自http://catlikecoding.com/unity/tutorials/editor/custom-list/
Custom List, displaying data your way
- create a custom editor
- use
SerializedObject
- manipulate a
SerializedProperty
that represents an array or list - use an enumeration for option flags
- use GUI buttons
This 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.
Customized lists.
Creating Test Data
We start with the finished Custom Data tutorial project, or by creating 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.
Creating a Custom 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 object stays 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 component it represents, by calling its Update
method. Then we can show the properties. And after we are done, we have to commit any changes via its ApplyModifiedProperties
method. This also takes care of 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.
Creating an Editor List
PropertyField
method. We will name this method Show and put it in its own 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 it show 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.
Properly Indenting
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.
Collapsing Lists
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.
Showing the Size
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.
Customizing the List
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.
Using Flags
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 its own script file or in the same script as EditorList
, but outside of the class.
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 options parameter. 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();
}
Hiding the Element Labels
[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 simple check. 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.
Adding Buttons
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.
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
andEditorGUILayout.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 functionality to the buttons is very simple, as we can directly use the methods for array manipulation provided by SerializedProperty
. We need the list and the current element index for this to work, so we add them as parameters to our ShowButtons
method and pass them along inside the loop of ShowElements
.
- How does
Button
work? - What happens when we move the bottom element?
- What are the contents of a new item?
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 the element to always be removed, not sometimes cleared. We can enforce this by checking whether the list's size has remained the same after deleting the element. If so, it has only been cleared and we 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.
Only Allowing Lists
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.
Multi-object Editing
No multi-object editing yet.
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.
Downloads
- custom-list.unitypackage
- The finished project.