Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remove +/- buttons from Array in Unity Custom Inspector

I'm using the EnumNamedArrayAttribute PropertyDrawer detailed in an old Unity Discussions thread here, in bonzairob's response, with a few small additions and it works well for my purposes, with some minor quirks when the length of the array changes. But this brings up the question, can I expand this to remove the Plus and Minus buttons that now appear at the bottom of the array? Or even make the Array Size field inaccessible?

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(EnumNamedArrayAttribute))]
public class DrawerEnumNamedArray : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        EnumNamedArrayAttribute enumNames = attribute as EnumNamedArrayAttribute;

        string[] variableName = property.propertyPath.Split('.');
        SerializedProperty p = property.serializedObject.FindProperty(variableName[0]);
        if(p.arraySize != enumNames.names.Length) {
            p.arraySize = enumNames.names.Length;
        }

        int index = System.Convert.ToInt32(property.propertyPath.Substring(property.propertyPath.IndexOf("[")).Replace("[", "").Replace("]", ""));
        if (index < enumNames.names.Length) {
            label.text = enumNames.names[index];
        }
        EditorGUI.PropertyField( position, property, label, true );
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
        return EditorGUI.GetPropertyHeight(property, label, true);
    }
}

I'm afraid I'm rather new to Custom Inspectors, and this problem has proven unusually difficult to Google.

like image 651
Questy Avatar asked Feb 22 '26 00:02

Questy


1 Answers

The really tricky thing here is: For Lists and arrays Unity only applies custom drawers on a per-element basis -> with a drawer alone you can not change the list drawing itself!

The only real way to change this is to rather implement a custom drawer/inspector for the type containing this list.

In most cases this is rather undesired as you have to start implementing custom editors for each component using such a list - uncanny.


However, you could instead of the "magic" attribute simply create a custom type and have a custom drawer for that one instead:

public abstract class EnumArray
{
    public abstract string[] names { get; }
}

[Serializable]
public class EnumArray<TEnum, TValues> : EnumArray, IEnumerable<TValues>
{
    [SerializeField]
    private TValues[] values;

    public IEnumerator<TValues> GetEnumerator()
    {
        foreach (var value in values)
        {
            yield return value;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public int Length => names.Length;

    public TValues this[TEnum index]
    {
        get => values[k_Indices[index]];
        set => values[k_Indices[index]] = value;
    }

    public TValues this[int index]
    {
        get => values[index];
        set => values[index] = value;
    }

    private static readonly Dictionary<TEnum, int> k_Indices = ((TEnum[])Enum.GetValues(typeof(TEnum))).Select((x, i) => (x, i)).ToDictionary(o => o.x, o => o.i);
    private static readonly string[] k_Names = Enum.GetNames(typeof(TEnum));

    public override string[] names => k_Names;
}

and have a custom drawer for that instead

[CustomPropertyDrawer(typeof(EnumArray), true)]
public class EnumArrayDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var height = EditorGUIUtility.singleLineHeight; // header

        var values = property.FindPropertyRelative("values");

        if (values.isExpanded)
        {
            for (var i = 0; i < values.arraySize; i++)
            {
                var element = values.GetArrayElementAtIndex(i);
                height += EditorGUI.GetPropertyHeight(element, new GUIContent("DummyLabel"), true);
            }
            
            height += EditorGUIUtility.singleLineHeight; // additional buffer for the list border
        }

        return height;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        using (new EditorGUI.PropertyScope(position, label, property))
        {
            var variableName = property.propertyPath.Split('.')[^1];
            var enumArray = property.serializedObject.targetObject.GetType().GetField(variableName).GetValue(property.serializedObject.targetObject) as EnumArray;

            var names = enumArray.names;
            var values = property.FindPropertyRelative("values");

            values.arraySize = names.Length;

            var headerRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
            values.isExpanded = EditorGUI.Foldout(headerRect, values.isExpanded, label);

            if (values.isExpanded)
            {
                using (new EditorGUI.IndentLevelScope())
                {
                    // This is what Unity internally uses for the default List drawing
                    // except we do not show any add/remove buttons and also do not want to reorder items
                    // also don't show header as we already have our custom foldout header anyway
                    var list = new ReorderableList(values.serializedObject, values, false, false, false, false)
                    {
                        drawElementCallback = (rect, index, active, focused) =>
                        {
                            var element = values.GetArrayElementAtIndex(index);
                            EditorGUI.PropertyField(rect, element, new GUIContent(names[index]), true);
                        },
                        elementHeightCallback = (index) =>
                        {
                            var element = values.GetArrayElementAtIndex(index);
                            return EditorGUI.GetPropertyHeight(element, GUIContent.none, true);
                        }
                    };

                    list.DoList(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight, position.width, position.height - EditorGUIUtility.singleLineHeight));
                }
            }
        }
    }
}

So assuming some enum like e.g.

public enum Axis
{
    X,
    Y,
    Z
}

instead of so far using

[EnumArrayNames(typeof(SomeEnum))]
public string[] Example;

you now rather simply use

public EnumArray<SomeEnum, string> Example;

and can as usual simply iterate it and access values

foreach(var item in Example)
{
    Debug.Log(item);
}

Example[2] = "SomeValue";

and as little bonus you can also directly access via the enum key

Debug.Log(Example[SomeEnum.ValueB]);

And this is how it would look like:

public class TestComponent : MonoBehaviour
{
    public EnumArray<Axis, string> strings;
    public EnumArray<Axis, Color> colors;
    public EnumArray<Axis, Vector3> vectors;
    public EnumArray<Axis, SomeCustomClass> custom;
}

[Serializable]
public class SomeCustomClass
{
    public string someString;
    public Color someColor;
    public Vector3 someVector;
}

enter image description here


Note: Does not work with arrays/lists as value type because in that case the var variableName = property.propertyPath.Split('.')[^1]; fails since property in that case again refers to the elements, not the array itself

like image 166
derHugo Avatar answered Feb 24 '26 14:02

derHugo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!