DEV Community

Emanuel Vintila
Emanuel Vintila

Posted on • Originally published at reflection.to

2 1

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!

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay