Skip to content

Flexible Editor Property Fields – Unity

After coming across an issue where the PropertyField method of a SerializedProperty wasn’t working when used in an Editor Window in Unity, I decided to create my own flexible field. This is the result.

ReflectionExtensions.cs

using UnityEditor;

using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;

// Researched from:
// https://answers.unity.com/questions/929293/get-field-type-of-serializedproperty.html
// https://stackoverflow.com/questions/7072088/why-does-type-getelementtype-return-null

public static class ReflectionExtensions
{
    public static Type GetType(SerializedProperty property)
    {
        string[] splitPropertyPath = property.propertyPath.Split('.');
        Type type = property.serializedObject.targetObject.GetType();

        for (int i = 0; i < splitPropertyPath.Length; i++)
        {
            if (splitPropertyPath[i] == "Array")
            {
                type = type.GetEnumerableType();
                i++; //skip "data[x]"
            }
            else
                type = type.GetField(splitPropertyPath[i], BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance).FieldType;
        }

        return type;
    }

    public static Type GetEnumerableType(this Type type)
    {
        if (type == null)
            throw new ArgumentNullException("type");

        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            return type.GetGenericArguments()[0];

        var iface = (from i in type.GetInterfaces()
                     where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)
                     select i).FirstOrDefault();

        if (iface == null)
            throw new ArgumentException("Does not represent an enumerable type.", "type");

        return GetEnumerableType(iface);
    }
}

CustomEditorGUI.cs

using UnityEngine;
using UnityEditor;
using System.Linq;
using System.Reflection;

/*
 * Generic and Gradient property types are unimplemented
 * It's possible gradient can be implemented: https://answers.unity.com/questions/436295/how-to-have-a-gradient-editor-in-an-editor-script.html
 * ExposedReference or ObjectReference may not be
 * Reference:
 * https://github.com/Unity-Technologies/UnityCsReference
 * https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/EditorGUI.cs
 */

public static class CustomEditorGUI
{
    public static void PropertyField(Rect position, SerializedProperty property, bool includeChildren = false)
    {
        PropertyField(position, property, new GUIContent(property.displayName), includeChildren);
    }

    public static void PropertyField(Rect position, SerializedProperty property, GUIContent label, bool includeChildren = false)
    {
        if (includeChildren || property.propertyType == SerializedPropertyType.Generic)
        {
            property.isExpanded = EditorGUILayout.Foldout(property.isExpanded, property.displayName);
            if (includeChildren && property.isExpanded)
            {
                foreach (SerializedProperty childProperty in property)
                    PropertyField(position, childProperty, new GUIContent(property.displayName), false);
            }

            return;
        }

        switch (property.propertyType)
        {
            case SerializedPropertyType.AnimationCurve:
                property.animationCurveValue = EditorGUI.CurveField(position, label, property.animationCurveValue);
                break;
            case SerializedPropertyType.ArraySize:
                property.intValue = EditorGUI.DelayedIntField(position, label, property.intValue);
                break;
            case SerializedPropertyType.Boolean:
                property.boolValue = EditorGUI.Toggle(position, label, property.boolValue);
                break;
            case SerializedPropertyType.Bounds:
                property.boundsValue = EditorGUI.BoundsField(position, label, property.boundsValue);
                break;
            case SerializedPropertyType.BoundsInt:
                property.boundsIntValue = EditorGUI.BoundsIntField(position, label, property.boundsIntValue);
                break;
            case SerializedPropertyType.Character:
                string newValue = EditorGUI.TextField(position, label, new string(new char[] { (char)property.intValue }));
                property.intValue = newValue.Length > 0 ? newValue[0] : '\0';
                break;
            case SerializedPropertyType.Color:
                property.colorValue = EditorGUI.ColorField(position, label, property.colorValue);
                break;
            case SerializedPropertyType.Enum:
                GUIContent[] displayNames = property.enumDisplayNames.Select(name => new GUIContent(name)).ToArray();
                property.enumValueIndex = EditorGUI.Popup(position, label, property.enumValueIndex, displayNames);
                break;
            case SerializedPropertyType.ExposedReference:
                property.exposedReferenceValue = EditorGUI.ObjectField(position, label, property.objectReferenceValue, ReflectionExtensions.GetType(property), true);
                break;
            case SerializedPropertyType.Float:
                property.floatValue = EditorGUI.FloatField(position, label, property.floatValue);
                break;
            case SerializedPropertyType.Integer:
                property.intValue = EditorGUI.IntField(position, label, property.intValue);
                break;
            case SerializedPropertyType.LayerMask:
                MethodInfo method = typeof(EditorGUI).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).First(t => t.Name == "LayerMaskField");
                method.Invoke(null, new object[] { position, property, label });
                break;
            case SerializedPropertyType.ObjectReference:
                property.objectReferenceValue = EditorGUI.ObjectField(position, label, property.objectReferenceValue, ReflectionExtensions.GetType(property), true);
                break;
            case SerializedPropertyType.Quaternion:
                Quaternion quaternion = property.quaternionValue;
                Vector4 quaternionValues = new Vector4(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
                quaternionValues = EditorGUI.Vector4Field(position, label, quaternionValues);
                property.quaternionValue = new Quaternion(quaternionValues.x, quaternionValues.y, quaternionValues.z, quaternionValues.w);
                break;
            case SerializedPropertyType.Rect:
                property.rectValue = EditorGUI.RectField(position, label, property.rectValue);
                break;
            case SerializedPropertyType.RectInt:
                property.rectIntValue = EditorGUI.RectIntField(position, label, property.rectIntValue);
                break;
            case SerializedPropertyType.String:
                property.stringValue = EditorGUI.TextField(position, label, property.stringValue);
                break;
            case SerializedPropertyType.Vector2:
                property.vector2Value = EditorGUI.Vector2Field(position, label, property.vector2Value);
                break;
            case SerializedPropertyType.Vector2Int:
                property.vector2IntValue = EditorGUI.Vector2IntField(position, label, property.vector2IntValue);
                break;
            case SerializedPropertyType.Vector3:
                property.vector3Value = EditorGUI.Vector3Field(position, label, property.vector3Value);
                break;
            case SerializedPropertyType.Vector3Int:
                property.vector3IntValue = EditorGUI.Vector3IntField(position, label, property.vector3IntValue);
                break;
            case SerializedPropertyType.Vector4:
                property.vector4Value = EditorGUI.Vector4Field(position, label, property.vector4Value);
                break;
            /*
            case SerializedPropertyType.Gradient:
            var method = typeof(EditorGUI).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).First(t => t.Name == "GradientField");
            var change = m.Invoke(null, new object[] { rect, gradient });
            method = typeof(EditorGUI).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).First(t => t.Name == "DefaultPropertyField");
            method.Invoke(null, new object[] { position, property, label });
            break;
            */
            default:
                Debug.LogError("SerializedPropertyType: " + property.propertyType + " not handled");
                break;
        }
    }
}

CustomEditorGUILayout.cs

sing UnityEngine;
using UnityEditor;

public static class CustomEditorGUILayout
{
    public static bool PropertyField(SerializedProperty property, params GUILayoutOption[] options)
    {
        return PropertyField(property, new GUIContent(property.displayName), false, options);
    }

    public static bool PropertyField(SerializedProperty property, GUIContent label, params GUILayoutOption[] options)
    {
        return PropertyField(property, label, false, options);
    }

    public static bool PropertyField(SerializedProperty property, bool includeChildren, params GUILayoutOption[] options)
    {
        return PropertyField(property, new GUIContent(property.displayName), includeChildren, options);
    }

    public static bool PropertyField(SerializedProperty property, GUIContent label, bool includeChildren, params GUILayoutOption[] options)
    {
        if (includeChildren || property.propertyType == SerializedPropertyType.Generic)
        {
            property.isExpanded = EditorGUILayout.Foldout(property.isExpanded, property.displayName);
            if (includeChildren && property.isExpanded)
            {
                foreach (SerializedProperty childProperty in property)
                    PropertyField(childProperty, new GUIContent(property.displayName), false, options);
            }

            return false;
        }
        Rect position = EditorGUILayout.GetControlRect(label.text.Length > 0, EditorGUI.GetPropertyHeight(property), options);
        CustomEditorGUI.PropertyField(position, property, label, includeChildren);
        return property.hasChildren && property.isExpanded && !includeChildren;
    }
}

The above code is provided under the MIT license.

Summary

It’s a pretty narrow use case, but I hope it helps anyone else doing something similar! Good luck. If you appreciate it, consider donating or checking out one of my assets on the Unity Asset store.

Published inCode ExamplesDevelopment Tips

Be First to Comment

    Leave a Reply

    Your email address will not be published. Required fields are marked *