unity3d Extending the Editor Custom Property Drawer


Example

Sometimes you have custom objects that contain data but do not derive from MonoBehaviour. Adding these objects as a field in a class that is MonoBehaviour will have no visual effect unless you write your own custom property drawer for the object's type.

Below is a simple example of a custom object, added to MonoBehaviour, and a custom property drawer for the custom object.

public enum Gender {
    Male,
    Female,
    Other
}

// Needs the Serializable attribute otherwise the CustomPropertyDrawer wont be used
[Serializable]
public class UserInfo {
    public string Name;
    public int Age;
    public Gender Gender;
}

// The class that you can attach to a GameObject
public class PropertyDrawerExample : MonoBehaviour {
    public UserInfo UInfo;
}

[CustomPropertyDrawer( typeof( UserInfo ) )]
public class UserInfoDrawer : PropertyDrawer {

    public override float GetPropertyHeight( SerializedProperty property, GUIContent label ) {
        // The 6 comes from extra spacing between the fields (2px each)
        return EditorGUIUtility.singleLineHeight * 4 + 6;
    }

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

        EditorGUI.LabelField( position, label );

        var nameRect = new Rect( position.x, position.y + 18, position.width, 16 );
        var ageRect = new Rect( position.x, position.y + 36, position.width, 16 );
        var genderRect = new Rect( position.x, position.y + 54, position.width, 16 );

        EditorGUI.indentLevel++;

        EditorGUI.PropertyField( nameRect, property.FindPropertyRelative( "Name" ) );
        EditorGUI.PropertyField( ageRect, property.FindPropertyRelative( "Age" ) );
        EditorGUI.PropertyField( genderRect, property.FindPropertyRelative( "Gender" ) );

        EditorGUI.indentLevel--;

        EditorGUI.EndProperty();
    }
}

First off we define the custom object with all it's requirements. Just a simple class describing a user. This class is used in our PropertyDrawerExample class which we can add to a GameObject.

public enum Gender {
    Male,
    Female,
    Other
}

[Serializable]
public class UserInfo {
    public string Name;
    public int Age;
    public Gender Gender;
}

public class PropertyDrawerExample : MonoBehaviour {
    public UserInfo UInfo;
}

The custom class needs the Serializable attribute otherwise the CustomPropertyDrawer will not be used

Next up is the CustomPropertyDrawer

First we have to define a class that derives from PropertyDrawer. The class definition also needs the CustomPropertyDrawer attribute. The parameter passed is the type of the object you want this drawer to be used for.

[CustomPropertyDrawer( typeof( UserInfo ) )]
public class UserInfoDrawer : PropertyDrawer {

Next we override the GetPropertyHeight function. This allows us to define a custom height for our property. In this case we know that our property will have four parts: label, name, age, and gender. Therefore we use EditorGUIUtility.singleLineHeight * 4, we add another 6 pixels because we want to space each field with two pixels in between.

public override float GetPropertyHeight( SerializedProperty property, GUIContent label ) {
    return EditorGUIUtility.singleLineHeight * 4 + 6;
}

Next is the actual OnGUI method. We start it off with EditorGUI.BeginProperty([...]) and end the function with EditorGUI.EndProperty(). We do this so that if this property would be part of a prefab, the actual prefab overriding logic would work for everything in between those two methods.

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

After that we show a label containing the name of the field and we already define the rectangles for our fields.

EditorGUI.LabelField( position, label );

var nameRect = new Rect( position.x, position.y + 18, position.width, 16 );
var ageRect = new Rect( position.x, position.y + 36, position.width, 16 );
var genderRect = new Rect( position.x, position.y + 54, position.width, 16 );

Every field is spaced by 16 + 2 pixels and the height is 16 (which is the same as EditorGUIUtility.singleLineHeight)

Next we indent the UI with one tab for a bit nicer layout, display the properties, un-indent the GUI, and end with EditorGUI.EndProperty.

EditorGUI.indentLevel++;

EditorGUI.PropertyField( nameRect, property.FindPropertyRelative( "Name" ) );
EditorGUI.PropertyField( ageRect, property.FindPropertyRelative( "Age" ) );
EditorGUI.PropertyField( genderRect, property.FindPropertyRelative( "Gender" ) );

EditorGUI.indentLevel--;

EditorGUI.EndProperty();

We display the fields by using EditorGUI.PropertyField which requires a rectangle for the position and a SerializedProperty for the property to show. We acquire the property by calling FindPropertyRelative("...") on the property passed in the OnGUI function. Note that these are case-sensitive and non-public properties cannot be found!

For this example I am not saving the properties return from property.FindPropertyRelative("..."). You should save these in private fields in the class to prevent unnecessary calls

Result

Before

Result before

After

Result after