DEV Community

Emanuel Vintila
Emanuel Vintila

Posted on • Originally published at reflection.to

Making a custom inspector in Unity to handle properties

Unity's built-in inspector only lets us modify fields. In this article we will see how we can extend the inspector to also display properties.

In order to create a custom inspector, we must derive from UnityEditor.Editor. This is the class declaration along with the properties that we will be using.

public class MyEditor : Editor
{
    protected Type InspectedType { get; set; }
    protected object InspectedObject { get; set; }
    protected List<PropertyInfo> Properties { get; set; }
    protected List<FieldInfo> Fields { get; set; }
}

First, in the OnEnable method of the inspector, let us collect the inspected object's settable fields and properties.

protected virtual void OnEnable()
{
    InspectedObject = serializedObject.targetObject;
    InspectedType = InspectedObject.GetType();

    Properties = InspectedType
        .GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Where(property => property.DeclaringType == InspectedType)
        .Where(property => (property.SetMethod?.IsPublic).GetValueOrDefault())
        .ToList();

    Fields = InspectedType
        .GetFields(BindingFlags.Public | BindingFlags.Instance)
        .Concat(InspectedType
                .GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(field => field.GetCustomAttribute<SerializeField>() != null)
               )
        .Where(field => field.IsInitOnly == false)
        .ToList();
}

Now, we will need a method that creates a field for a specific type with a label and a value.

protected virtual object MakeFieldForType(Type type, string label, object value)
{
    T F<T>(Func<string, T, GUILayoutOption[], T> fn)
    {
        return fn(label, (T) value, null);
    }

    if (type == typeof(bool))
        return F<bool>(EditorGUILayout.Toggle);
    if (type == typeof(int))
        return F<int>(EditorGUILayout.IntField);
    if (type == typeof(long))
        return F<long>(EditorGUILayout.LongField);
    if (type == typeof(float))
        return F<float>(EditorGUILayout.FloatField);
    if (type == typeof(double))
        return F<double>(EditorGUILayout.DoubleField);
    if (type == typeof(string))
        return F<string>(EditorGUILayout.TextField);

    throw new ArgumentException(nameof(type));
}

Finally, let us override the OnInspectorGUI method; it uses the MakeFieldForType method defined above.

public override void OnInspectorGUI()
{
    EditorGUILayout.LabelField("Properties");
    foreach (PropertyInfo property in Properties)
    {
        string label = property.Name;
        object value = property.GetValue(InspectedObject);
        property.SetValue(InspectedObject, MakeFieldForType(property.PropertyType, label, value));
    }

    EditorGUILayout.Separator();

    EditorGUILayout.LabelField("Fields");
    foreach (FieldInfo field in Fields)
    {
        string label = field.Name;
        object value = field.GetValue(InspectedObject);
        field.SetValue(InspectedObject, MakeFieldForType(field.FieldType, label, value));
    }
}

Obviously, this is a basic example, you would have to handle more types in the MakeFieldForType method.

There are drawbacks to this method. The SerializedObject's undo functionality is lost. The multi-editing support is also lost, but it can be added back pretty easily (leave a comment with your solution to this one 😉).

In order to test our new custom inspector, we need to derive a class from it, and annotate it with the CustomEditor attribute, which tells Unity that it should use the editor for the respective type.

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class ExampleClass : MonoBehaviour
{ 
    public float Prop { get; set; }
    public float PropPrivateSet { get; private set; }
    public float PropReadOnly { get; }
    public float PropComputed => field * 2;

    public float field;
    public readonly float readonlyField = 5;
    [SerializeField]
    private float privateField;
}

[CustomEditor(typeof(ExampleClass))]
public class ExampleClassEditor : MyEditor { }

Why do this when the built-in inspector is more powerful? Because the built-in inspector does not support properties, and properties are a crucial part of encapsulation. You would not ever want to expose fields to the public, instead you would wrap them in properties. Even auto-implemented properties are much better than fields.

Imagine one day you decide you wanted to implement some other logic when setting a specific field. You would have to either wrap it in a property (the C# way), or write getter and setter methods for it (which is more or less equivalent to writing a property). Doing this means breaking already existing usages of your field in existing code.

In conclusion: always use properties instead of fields in the public interface!

Top comments (0)