9 min read

In this article by Angelo Tadres, author of the book Extending Unity with Editor Scripting, we will explore a way to create a custom GUI for our properties using Property Drawers.

If you’ve worked on a Unity project for a long time, you know that the bigger your scripts get, the more unwieldy they become. All your public variables take up space in the Inspector Window, and as they accumulate, they begin to convert into one giant and scary monster. Sometimes, organization is the clue. So, in this article, you will learn how to improve your inspectors using Property and Decorator Drawers.

(For more resources related to this topic, see here.)

A Property Drawer is an attribute that allows you to control how the GUI of a Serializable class or property is displayed in the inspector window.

An Attribute is a C# way of defining declarative tags, which you can place on certain entities in your source code to specify additional information. The information that attributes contain is retrieved at runtime through reflection.

This approach significantly reduces the amount of work you have to do for the GUI customization because you don’t need to write an entire Custom Inspector; instead, you can just apply appropriate attributes to variables in your scripts to tell the editor how you want those properties to be drawn.

Unity has several Property Drawers implemented by default. Let’s take a look at one of them called Range:

using UnityEngine;

public class RangeDrawerDemo : MonoBehaviour {
    [Range (0, 100)]
    public int IntValue = 50;
}

If you attach this script to a game object and then select it, you will see the following:

Using the Range attribute, we rendered a slider that moves between 0 and 100 instead of the common int field. This is valuable in terms of validating the input for this field. Avoid possible mistakes such as using a negative value to define the radius of a sphere collider, and so on.

Let’s take a look to the rest of the built-in Property Drawers.

Built-in Property Drawers

The Unity documentation has information about the built-in property drawers, but there is no such place where you can check all the available ones listed. In this section, we will resolve this.

Range

The Range attribute restricts a float or int variable in a script to a specific range. When this attribute is used, the float or int will be shown as a slider in the inspector instead of the default number field:

public RangeAttribute(float min, float max);

public RangeAttribute(float min, float max);
[Range (0, 1)]
public float FloatRange = 0.5f;  
[Range (0, 100)]
public int IntRange = 50;

You will get the following output:

Multiline

The Multiline attribute is used to show a string value in a multiline text area. You can decide the number of lines of text to make room for. The default is 3 and the text doesn’t wrap. The following is an example of the Multiline attribute:

public MultilineAttribute();
public MultilineAttribute(int lines);
[Multiline (2)]
public string StringMultiline = "This text is using a multiline property drawer";

You will get the following output:

TextArea

The TextArea attribute allows a string to be edited with a height-flexible and scrollable text area. You can specify the minimum and maximum values; a scrollbar will appear if the text is bigger than the area available. Its behavior is better compared to Multiline. The following is an example of the TextArea attribute:

public TextAreaAttribute();
public TextAreaAttribute(int minLines, int maxLines);
[TextArea (2, 4)]
public string StringTextArea = "This text is using a textarea property drawer";

You will get the following output:

ContextMenu

The ContextMenu attribute adds the method to the context menu of the component. When the user selects the context menu, the method will be executed. The method has to be nonstatic. In the following example, we call the method DoSomething, printing a log in the console:

public ContextMenu(string name);
[ContextMenu ("Do Something")]
public void DoSomething() {
     Debug.Log ("DoSomething called...");
}

You will get the following output:

ContextMenuItem

The ContextMenuItem attribute is used to add a context menu to a field that calls a named method. In the following example, we call a method to reset the value of the IntReset variable to 0:

public ContextMenuItemAttribute(string name, string function);
[ContextMenuItem("Reset this value", "Reset")]
public int IntReset = 100;
public void Reset() {
       IntReset = 0;
   }

You will get the following output:

Built-in Decorator Drawers

There is another kind of drawer called Decorator Drawer. These drawers are similar in composition to the Property Drawers, but the main difference is that Decorator Drawers are designed to draw decoration in the inspector and are unassociated with a specific field. This means, while you can only declare one Property Drawer per variable, you can stack multiple decorator drawers in the same field.

Let’s take a look in the following built-in Decorator Drawers.

Header

This is the attribute that adds a header to some fields in the inspector:

public HeaderAttribute(string header);
[Header("This is a group of variables")]
public int VarA = 10;
public int VarB = 20;

You will get the following output:

Space

The space attribute adds some spacing in the inspector:

public SpaceAttribute(float height);
public int VarC = 10;
[Space(40)]
public int VarD = 20;

You will get the following output:

Tooltip

This Tooltip attribute specifies a tooltip for a field:

public TooltipAttribute(string tooltip);
[Tooltip("This is a tooltip")]
public int VarE = 30;

You will get the following output:

Creating you own Property Drawers

If you have a serializable parameter or structure that repeats constantly in your video game and you would like to improve how this renders in the Inspector, you can try to write your own Property Drawer.

We will create a property drawer for an integer meant to be a variable to save time in seconds. This Property Drawer will draw a normal int field but also a label with the number of seconds converted to the m:s or h:m:s time format.

To implement a Property Drawer, you must create two scripts:

  • The attribute: This declares the attribute and makes it usable in your MonoBehaviour scripts. This will be part of your video game scripts.
  • The drawer: This is responsible for rendering the custom GUI and handling the input of the user. This is placed inside a folder called Editor.

The Editor folder is one of the several special folders Unity has. All scripts inside this folder will be treated as editor scripts rather than runtime scripts.

For the first script, create one file inside your Unity project called TimeAttribute.cs and then add the following code:

using UnityEngine;

public class TimeAttribute : PropertyAttribute {
  public readonly bool DisplayHours;
  public TimeAttribute (bool displayHours = false) { 
    DisplayHours = displayHours;
  }
}

Here we defined the name of the attribute and its parameters. You must create your attribute class extending from the PropertyAttribute class.

The name of the class contains the suffix “attribute”; however, when you want to use the attribute over a certain property, the suffix is not needed. In this case, we will use Time and not TimeAttribute to use the property drawer.

The TimeAttribute has an optional parameter called DisplayHours. The idea is to display a label under the int field with the time in m:s format by default; if the DisplayHours parameter is true, this will be displayed in h:m:s format.

Now is the moment to implement the drawer. To do this, let’s create a new script called TimeDrawer.cs inside an Editor folder:

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer (typeof(TimeAttribute))]
public class TimeDrawer : PropertyDrawer {
  
  public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
    return EditorGUI.GetPropertyHeight (property) * 2;
  }
  
  public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
    if (property.propertyType == SerializedPropertyType.Integer) {
      property.intValue = EditorGUI.IntField (new Rect (position.x, position.y, position.width, position.height / 2), label, Mathf.Abs(property.intValue));
      EditorGUI.LabelField (new Rect (position.x, position.y + position.height / 2, position.width, position.height / 2), " ", TimeFormat (property.intValue));
      
    } else {
      EditorGUI.LabelField (position, label.text, "Use Time with an int.");
    }
  }
  
  private string TimeFormat (int seconds) {
    TimeAttribute time = attribute as TimeAttribute;
    if (time.DisplayHours) {
      return string.Format ("{0}:{1}:{2} (h:m:s)", seconds / (60 * 60), ((seconds % (60 * 60)) / 60).ToString ().PadLeft(2,'0'), (seconds % 60).ToString ().PadLeft(2,'0'));
    } else {
      return string.Format ("{0}:{1} (m:s)", (seconds / 60).ToString (), (seconds % 60).ToString ().PadLeft(2,'0'));
    }
  }
}

Property Drawers don’t support layouts to create a GUI; for this reason, the class you must use here is EditorGUI instead of EditorGUILayout. Using this class requires a little extra effort; you need to define the Rect function that will contain the GUI element each time you want to use it.

The CustomPropertyDrawer attribute is part of the UnityEditor namespace, and this is what Unity uses to bind a drawer with a Property attribute. In this case, we passed the TimeAttribute.

Your must extend the TimeAttribute from the PropertyDrawer class, and in this way, you will have access to the core methods to create Property Drawers:

  • GetPropertyHeight: This method is responsible for handling the height of the drawer. You need to overwrite this method in order to use it. In our case, we force the size of the drawer to be double.
  • OnGUI: This is where you place all the code related to rendering the GUI.

You can create Decorator Drawers too. You just need to follow the same steps we performed to create a Property Drawer, but instead of extending your Drawer from PropertyDrawer, you need to extend from DecoratorDrawer.

Also, you will have access to the variable attribute. This has a reference to the attribute class we created, and with this we can access to their variables.

To test our code, create a new script called TimeDrawerDemo.cs and add the following code:

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer (typeof(TimeAttribute))]
public class TimeDrawer : PropertyDrawer {
  
  public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
    return EditorGUI.GetPropertyHeight (property) * 2;
  }
  
  public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
    if (property.propertyType == SerializedPropertyType.Integer) {
      property.intValue = EditorGUI.IntField (new Rect (position.x, position.y, position.width, position.height / 2), label, Mathf.Abs(property.intValue));
      EditorGUI.LabelField (new Rect (position.x, position.y + position.height / 2, position.width, position.height / 2), " ", TimeFormat (property.intValue));
      
    } else {
      EditorGUI.LabelField (position, label.text, "Use Time with an int.");
    }
  }
  
  private string TimeFormat (int seconds) {
    TimeAttribute time = attribute as TimeAttribute;
    if (time.DisplayHours) {
      return string.Format ("{0}:{1}:{2} (h:m:s)", seconds / (60 * 60), ((seconds % (60 * 60)) / 60).ToString ().PadLeft(2,'0'), (seconds % 60).ToString ().PadLeft(2,'0'));
    } else {
      return string.Format ("{0}:{1} (m:s)", (seconds / 60).ToString (), (seconds % 60).ToString ().PadLeft(2,'0'));
    }
  }
}

After compiling, attach this script to a game object. You will see this on the inspector:

The time property uses the time attribute. We can check the three possible scenarios here:

  • The attribute uses a property that is not an int
  • The attribute has the variable DisplayHours = false
  • The attribute has the variable DisplayHours = true

A little change makes it easier to set up this data in our game objects.

Summary

In this article, we created a custom Property Drawer to be used in properties mean to store time in seconds, and in the process, you learned how these are implemented. We also explored the available built-in Property Drawers and Decorator Drawers in Unity.

Applying this knowledge to your projects will enable you to add validation to sensible data in your properties and make your scripts more developer friendly. This will also allow you to have a professional look.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here