Custom Data, an introduction to serialized classes and property drawers
- use a serialized class
- create a custom property drawer
- use
SerializedProperty
- use Unity's immidiate-mode GUI in the editor
You're assumed to know your way around Unity's editor and know the basics of Unity C# scripting. If you've completed some of the other tutorials then you're good to go.
This tutorial is for Unity version 4.3 and above.
Colored Points
We will create a colored point, a data structure that contains both a color and a position.
We start by creating a new empty project and adding a new C# script named ColorPoint with the required variables.
using UnityEngine; public class ColorPoint { public Color color; public Vector3 position; }
using UnityEngine; public class ColorPointTester : MonoBehaviour { public ColorPoint point; public ColorPoint[] points; public Vector3 vector; public Vector3[] vectors; }
System.Serializable
attribute to our class.
By doing this it becomes possible to serialize all public fields of the class to a data stream, which allows it to be stored.
using UnityEngine; using System; [Serializable] public class ColorPoint { public Color color; public Vector3 position; }
Drawing the Property
Fortunately, we can replace Unity's default way of drawing properties in the editor with our own variant.
This is done by creating a class that extends from UnityEditor.PropertyDrawer
and using the
UnityEditor.CustomPropertyDrawer
attribute to associate it with the type we want it to do the drawing for.
We will name this class ColorPointDrawer and because this is an editor class we place it in a new folder named Editor.
using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(ColorPoint))] public class ColorPointDrawer : PropertyDrawer { }
OnGUI
method of PropertyDrawer
with our own version.
This OnGUI
method has three parameters. First is a Rect
that tells us
what area of the window we should use to draw our property. Second is the property itself,
represented by a SerializedProperty
. Third is a GUIContent
that defines
the label we should use for the property.
Let's start by drawing just the label using the GUIEditor.PrefixLabel
method followed
by the positions using the GUIEditor.PropertyField
method.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { EditorGUI.PrefixLabel(position, label); EditorGUI.PropertyField(position, property.FindPropertyRelative("position")); }
GUIContent.none
.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { EditorGUI.PrefixLabel(position, label); EditorGUI.PropertyField(position, property.FindPropertyRelative("position"), GUIContent.none); }
PrefixLabel
method returns an adjusted rectangle for the
space to the right of it. So we will use this rect instead.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { Rect contentPosition = EditorGUI.PrefixLabel(position, label); EditorGUI.PropertyField(contentPosition, property.FindPropertyRelative("position"), GUIContent.none); }
PropertyField
method adjusts for the current editor indent level. While this is usually convenient, we don't want
any automatic adjustments in this case.
The intent level is set via the static int
EditorGUI.indentLevel
. To temporarily eliminate automatic indenting, we simply set it to zero.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { Rect contentPosition = EditorGUI.PrefixLabel(position, label); EditorGUI.indentLevel = 0; EditorGUI.PropertyField(contentPosition, property.FindPropertyRelative("position"), GUIContent.none); }
Fixing the Prefix
We need to tell the editor where our property starts and where it ends, because right now
we are only showing part of its contents. We can use the EditorGUI.BeginProperty
method
to construct a new label and signal the start of a property, and use the
EditorGUI.EndProperty
method to signal when we are done.
This will give us a label that provides the expected functionality via its context menu.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); Rect contentPosition = EditorGUI.PrefixLabel(position, label); EditorGUI.indentLevel = 0; EditorGUI.PropertyField(contentPosition, property.FindPropertyRelative("position"), GUIContent.none); EditorGUI.EndProperty(); }
Adding the Color
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); Rect contentPosition = EditorGUI.PrefixLabel(position, label); contentPosition.width *= 0.75f; EditorGUI.indentLevel = 0; EditorGUI.PropertyField(contentPosition, property.FindPropertyRelative("position"), GUIContent.none); contentPosition.x += contentPosition.width; contentPosition.width /= 3f; EditorGUI.PropertyField(contentPosition, property.FindPropertyRelative("color"), new GUIContent("C")); EditorGUI.EndProperty(); }
EditorGUIUtility.labelWidth
. Using a width of 14 pixels works fine.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); Rect contentPosition = EditorGUI.PrefixLabel(position, label); 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(); }
Claiming an Extra Line
We have to override the GetPropertyHeight
method to specify how much vertical space
we need. The default for one line is 16 pixels. Adding a second line requires 18 additional pixels,
16 for the second line plus a margin of 2 between then.
It turns out that Screen.width
contains the width of our inspector panel when we need
it, so we can use that. Vectors switch to multiple lines when this width drops below 333, so we will
do this as well.
public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { return Screen.width < 333 ? (16f + 18f) : 16f; }
First, we can detect that we're using two lines by checking the height of our position rect.
Second, we need to set the height back to 16 so the color property stays on one line.
Third, we have to move down a line after we've drawn the property label.
Fourth, we have to increase the indent level by one and apply it to our position, using the
EditorGUI.IndentedRect
method.
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { 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(); }
ColorPoint
.
It supports undo, redo, prefabs, and multi-object editing like normal. It uses only one line
if the inspector is wide enough, otherwise it uses two.
The next editor tutorial is Custom List.
Downloads
- custom-data.unitypackage
- The finished project.