2013-12-02

Non trivial Property Drawers in Unity

Another programming related post... I promise I'll publish something nicer next time.
Also, because Google insist on making their products consistently worse, I am unable to upload pictures. This is going to be even drier than usual.

Recently, Unity added a new tool to its arsenal, in the form of Property Drawers. They allow specifying how to display your own types inside Unity's editor interface, when contained as a variable of a MonoBehaviour. Previously we were limited to specifying how to draw a whole component, which had its limitations in terms of reuse.

So, now that you know they exist, you might start looking around for examples... only to find the most trivial one repeated over and over again: a Property Drawer for a class with one simple variable. But you want to display an array, or an array of colours, or a visual angle selector (which is used in an example, but its code is curiously missing). Something actually useful when editing complex games in Unity.

So, I'll save you an hour of reading through documentation and debugging weird errors. Property Drawers are very easy to use when you know what to pay attention to.

Basic case: a class with a Sprite picker

Let's create a simple class LayeredElement.cs, to organize Sprites.

LayeredElement.cs:

    [Serializable]
    public class LayeredElement {
        /// <summary>
        /// Order in which this element is painted,
        /// 0 being the background.
        /// </summary>
        public int layer;
        public int Layer { get { return this.layer; } }
    }

    [Serializable]
    public class NonConfigurablePart : LayeredElement {
        public Sprite sprite;
        public Sprite Sprite { get { return sprite; } }
    }


To display NonConfigurablePart we need to expose the layer and sprite variables. Currently I don't know how to display a texture/sprite selector using EditorGUI, so I'll use a simple selector and display the sprite independently.

NonConfigurablePartDrawer.cs:

    using UnityEditor;
    using UnityEngine;

    [CustomPropertyDrawer( typeof( NonConfigurablePart ) )]
    public class NonConfigurablePartDrawer : PropertyDrawer {
        // Default Unity line height
        private int textHeight = 16;
        // All sprites wiil take the same area.
        private int spriteDim = 60;

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
            EditorGUI.BeginProperty( position, label, property );

            // Store the original indentation.
            // Since we are being drawn inside a component,
            // changing it could modify how later elements are displayed.
            int origIndent = EditorGUI.indentLevel;

            // Obtain the properties we are interested in. Accessing variables by name is error prone, so be careful.
            SerializedProperty layerProp = property.FindPropertyRelative( "layer" );
            SerializedProperty spriteProp = property.FindPropertyRelative( "sprite" );

            // Name of the variable this NonConfigurablePart has in the current MonoBehaviour.
            EditorGUI.LabelField( new Rect( position.xMin, position.yMin, position.width, textHeight ), label );

            // We don't use indentation, actually, so just get rid of it
            EditorGUI.indentLevel = 0;
            // Calculate the area to use for the layer field.
            // 130 is the number of pixels Unity usually leaves before showing variable setters.
            // Found via trial and error =)
            Rect layerRect = new Rect( 130, position.yMin, position.width - 130, textHeight );

            // Check if it was modified this frame, to avoid overwriting the property constantly
            EditorGUI.BeginChangeCheck();
            int layer = EditorGUI.IntField( layerRect, "Layer", layerProp.intValue );
            if (layer < 0) layer = 0;
            if (EditorGUI.EndChangeCheck()) {
                layerProp.intValue = layer;
            }

            // Calculate where to draw the selector (right below the layer field).
            Rect selectorRect = new Rect( layerRect.xMin, layerRect.yMax, position.width - 130, textHeight );
            // PropertyField updates the property automatically, so no need to BeginChangeCheck
            EditorGUI.PropertyField( selectorRect, spriteProp, new GUIContent( "" ), false );

            // Calculate where to draw the sprite
            Rect spriteRect = new Rect( 130, selectorRect.yMax + 5, spriteDim, spriteDim );
            this.DrawSprite( (Sprite)spriteProp.objectReferenceValue, spriteRect );

            // That's it. Some little house cleaning and leave.
            EditorGUI.indentLevel = origIndent;
            EditorGUI.EndProperty();
        }

        // Calculate height required by the component.
        // Notice the absence of position; we cannot rely on a controlled width...
        public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
            // Layer (and label), selector and the sprite itself.
            return this.textHeight * 2 + this.spriteDim + 5;
        }

        private void DrawSprite(Sprite sprite, Rect spriteRect) {
            if (sprite != null) {
                EditorGUI.DrawTextureTransparent( spriteRect, sprite.texture, ScaleMode.ScaleToFit );
            } else {
                // Draw a visibly unconfigured rectangle
                EditorGUI.DrawRect( spriteRect, Color.magenta );
            }
        }
    }


There is nothing extraordinarily complex here, but some very specific calls and behaviours.
Keep reading for something a little bit more complex.