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.

2013-09-26

Implementing a Transition-based State Machine

Warning: this is one of the code heavy posts I once warned about. Those allergic to discussions of programming topics or big listings of code (C# code; it's not that bad), keep walking. Or stay and become harder, better, faster, stronger...

In this article I will introduce a (probably not so) new approach to implementing State Machines, mostly for videogames. The reference is, as usual, Buckland's implementation in his Programming Game AI by Example book. His version of the state machine was and still is the de facto standard when coding small AIs or logical behaviours. Although new and improved systems exist, like Behaviour Trees, FSMs are still pervasive in the video game industry.

FSMs have been slightly improved through time, as languages and techniques developed, offering new possibilities, but the core remains pretty much unchanged. Here I will introduce one major revision to how SMs are implemented and, more important, used.

Basic introduction to the classic State Machine

In Buckland's book, State Machines are composed of several elements:
  • The State Machine itself, which contains
    •  A reference to the machine's owner
    • The current state to execute
    • The global state to execute
    • The previous state, in case a behaviour requires going back to it
  • A base generic abstract class State<T>, with methods Enter, Update and Exit. Enter is called when the state becomes the current one, Exit when it stops being so. Update is called on every execution cycle (of the state machine, not necessarily of the game or entity).
  • A collection of singletons derived from State<T>, one for each behaviour T can display.
  • A messaging system which allows communication between entities. State<T> includes an abstract method to evaluate messages and treat them if appropriate.
Important details:
  • States are, curiously, stateless. That's why a reference to the owner is required: it must contain all the state information the machine needs.
  • It is considered in bad taste (but unfortunately common) to transition from outside the states themselves, but the state machine usually allows this through several hijacking methods.
Thus, each state is responsible for checking its possible transitions. State hijacking is supported via the global states, which check conditions outside the scope of low level ones. If used, global states usually become a state machine of their own, to support all the possible situations.