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)