Search Unity

Property Drawers in Unity 4

September 7, 2012 in Technology | 7 min. read
Topics covered
Share

A new exciting editor extension feature is coming for Unity 4 - Property Drawers!

What started out as a NinjaCamp project between Rune and I has now been included in Unity. Property Drawers significantly reduce the amount of work you have to do in order to customize how your scripts look in the Inspector. You no longer have to write an entire Custom Editor (although that is still supported as well). Instead, you can just apply appropriate attributes to variables in your scripts to tell the editor how you want those properties to be drawn.

This allows you to enforce sensible settings on your properties, create "intelligent" properties that gather data from a data source to show a set of valid options, make your scripts more developer friendly or just have a nice tidy up.

Let's take a look at a script before and after applying some PropertyAttributes to spice things up.

Changing the appearance and behavior of the field controls is as easy as adding attributes to your fields as in the script below.

public class Example : MonoBehaviour {
    public string playerName = "Unnamed";

    [Multiline]
    public string playerBiography = "Please enter your biography";

    [Popup ("Warrior", "Mage", "Archer", "Ninja")]
    public string @class = "Warrior";

    [Popup ("Human/Local", "Human/Network", "AI/Easy", "AI/Normal", "AI/Hard")]
    public string controller;

    [Range (0, 100)]
    public float health = 100;

    [Regex (@"^(?:\d{1,3}\.){3}\d{1,3}$", "Invalid IP address!\nExample: '127.0.0.1'")]
    public string serverAddress = "192.168.0.1";

    [Compact]
    public Vector3 forward = Vector3.forward;

    [Compact]
    public Vector3 target = new Vector3 (100, 200, 300);

    public ScaledCurve range;
    public ScaledCurve falloff;

    [Angle]
    public float turnRate = (Mathf.PI / 3) * 2;
}

Here's how it works. When the Inspector renders the component for your script, it will check for each property if there is a ProperyDrawer defined for the type of the property or if the property has an attribute that inherits from PropertyAttribute. If there is, then an associated PropertyDrawer will handle rendering of that single property. You are able to define both attributes and drawers to customize rendering and data exchange for a given property.

Writing your own PropertyAttribute and PropertyDrawer

Let's take a closer look at RegexAttribute and its PropertyDrawer to show how it fits together. First off, we should define an attribute that derives from PropertyAttribute. This attribute can store any kind of data you wish and it is intended to later be used by the PropertyDrawer.

public class RegexAttribute : PropertyAttribute {
    public readonly string pattern;
    public readonly string helpMessage;
    public RegexAttribute (string pattern, string helpMessage) {
        this.pattern = pattern;
        this.helpMessage = helpMessage;
    }
}

The new RegexAttribute can be applied to a field in a script as shown below.

using UnityEngine;

public class RegexExample : MonoBehaviour {
    [Regex (@"^(?:\d{1,3}\.){3}\d{1,3}$", "Invalid IP address!\nExample: '127.0.0.1'")]
    public string serverAddress = "192.168.0.1";
}

So far we have just created an attribute which holds data. However this alone will not cause anything to render. We need to define a drawer for the attribute, so let's implement a RegexDrawer. It has to derive from PropertyDrawer and it must have a CustomPropertyDrawer attribute which tells Unity which kinds of attributed fields it can draw controls for. In our case we want it to draw the regex controls for any strings with the RegexAttribute attached to it, so we specify that here.

using UnityEditor;
using UnityEngine;
using System.Text.RegularExpressions;

[CustomPropertyDrawer (typeof (RegexAttribute))]
public class RegexDrawer : PropertyDrawer {
    // These constants describe the height of the help box and the text field.
    const int helpHeight = 30;
    const int textHeight = 16;

    // Provide easy access to the RegexAttribute for reading information from it.
    RegexAttribute regexAttribute { get { return ((RegexAttribute)attribute); } }

    // Here you must define the height of your property drawer. Called by Unity.
    public override float GetPropertyHeight (SerializedProperty prop,
                                             GUIContent label) {
        if (IsValid (prop))
            return base.GetPropertyHeight (prop, label);
        else
            return base.GetPropertyHeight (prop, label) + helpHeight;
    }

    // Here you can define the GUI for your property drawer. Called by Unity.
    public override void OnGUI (Rect position,
                                SerializedProperty prop,
                                GUIContent label) {
        // Adjust height of the text field
        Rect textFieldPosition = position;
        textFieldPosition.height = textHeight;
        DrawTextField (textFieldPosition, prop, label);

        // Adjust the help box position to appear indented underneath the text field.
        Rect helpPosition = EditorGUI.IndentedRect (position);
        helpPosition.y += textHeight;
        helpPosition.height = helpHeight;
        DrawHelpBox (helpPosition, prop);
    }

    void DrawTextField (Rect position, SerializedProperty prop, GUIContent label) {
        // Draw the text field control GUI.
        EditorGUI.BeginChangeCheck ();
        string value = EditorGUI.TextField (position, label, prop.stringValue);
        if (EditorGUI.EndChangeCheck ())
            prop.stringValue = value;
    }

    void DrawHelpBox (Rect position, SerializedProperty prop) {
        // No need for a help box if the pattern is valid.
        if (IsValid (prop))
            return;

        EditorGUI.HelpBox (position, regexAttribute.helpMessage, MessageType.Error);
    }

    // Test if the propertys string value matches the regex pattern.
    bool IsValid (SerializedProperty prop) {
        return Regex.IsMatch (prop.stringValue, regexAttribute.pattern);
    }
}

Writing a PropertyDrawer for a custom Serializable Class

Sometimes you have a custom serializable class that is used in multiple different scripts, and you want to use some specific GUI for that class everywhere it's used. Consider this ScaledCurve class:

// Custom serializable class
[System.Serializable]
public class ScaledCurve {
    public float scale = 1;
    public AnimationCurve curve = AnimationCurve.Linear (0, 0, 1, 1);
}

Normally every instance of this class would be shown in the Inspector with a foldout, but you can create a PropertyDrawer to show the class with custom GUI. This GUI is then used everywhere the class is used. The Property Drawer used here places all the controls in one line. Here's the image again so you can compare the look of the Range and Falloff properties without and with the custom PropertyDrawer:

To make a PropertyDrawer for a class, pass the type of that class to the CustomPropertyDrawer attribute of the PropertyDrawer:

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer (typeof (ScaledCurve))]
public class ScaledCurveDrawer : PropertyDrawer {
    const int curveWidth = 50;
    const float min = 0;
    const float max = 1;
    public override void OnGUI (Rect pos, SerializedProperty prop, GUIContent label) {
        SerializedProperty scale = prop.FindPropertyRelative ("scale");
        SerializedProperty curve = prop.FindPropertyRelative ("curve");

        // Draw scale
        EditorGUI.Slider (
            new Rect (pos.x, pos.y, pos.width - curveWidth, pos.height),
            scale, min, max, label);

        // Draw curve
        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        EditorGUI.PropertyField (
            new Rect (pos.width - curveWidth, pos.y, curveWidth, pos.height),
            curve, GUIContent.none);
        EditorGUI.indentLevel = indent;
    }
}

Wrapping Up

Finally a few words about how to structure these classes. Your scripts remain in the asset folder as before. Your new attributes should also be placed in the asset folder just like any scripts, however a recommendation is to keep your asset folder organized, so consider creating an Attributes folder. Your new property drawers should be placed in an editor folder, inside your assets folder. Check out this image, hopefully it should make it pretty clear.

If you have open beta access to Unity 4, you can learn more about PropertyDrawers in your local copy of the Unity 4 Script Reference - just search for PropertyDrawer. Once Unity 4 is released, the information will be in the online version of the Script Reference as well.

That's all! We're excited to see what kinds of cool extensions you will create with Unity 4!

September 7, 2012 in Technology | 7 min. read
Topics covered