HUZZAH games

RSS

Enumerated Arrays in Unity

I wanted a class in C# that represented a fixed-size array, where each member of the array corresponded to a value in an enum.  In code, I want to do something like this:

public enum Side {
    Front = 0,
    Left = 1,
    Rear = 2,
    Right = 3
}
private EnumeratedArray<float, Side> _armor;
...
_armor[Side.Front] -= 10.0f;

I also want the Unity Editor to recognize this data type. It should be fixed-size, and it should use names from the enum instead of “Element 0” … “Element 3”.

image

After a few days of hacking on this, it turns out that Unity’s Property Drawer system doesn’t support generics.  Damn.  Ok, let’s implement this without the generics.

First, the EnumeratedArray class:

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

[System.Serializable]
public class EnumeratedArray : IEnumerable {
    public readonly string[] enumNames;
#if UNITY_EDITOR
    private Type _enumType;
#endif

    [SerializeField]
    private T[] _data;

    public EnumeratedArray(Type t) {
#if UNITY_EDITOR
        _enumType = t;
#endif
        enumNames = Enum.GetNames(t);
        for (int i = 0; i < this.enumNames.Length; ++i) {
            this.enumNames[i] = enumNames[i];
        }
        _data = new T[this.enumNames.Length];
    }

    public T this[int index] {
        get { return _data[index]; }
        set { _data[index] = value; }
    }
    public T this[object obj] {
        get {
#if UNITY_EDITOR
            if (obj.GetType() != _enumType)
                Debug.LogError(obj + " is not of type " + _enumType);
#endif
            return _data[Convert.ToInt32(obj)];
        }
        set {
#if UNITY_EDITOR
            if (obj.GetType() != _enumType)
                Debug.LogError(obj + " is not of type " + _enumType);
#endif
            _data[Convert.ToInt32(obj)] = value;
        }
    }

    public int Length { get { return _data.Length; } }

    public IEnumerator GetEnumerator() {
        return _data.GetEnumerator();
    }
}

[System.Serializable]
public class EnumeratedArrayInt : EnumeratedArray {
    public EnumeratedArrayInt(Type t) : base(t) {}
}



Next, the Property Drawer:

using UnityEngine;
using UnityEditor;
using System;

public class EnumeratedArrayDrawer : PropertyDrawer {
    //Experimentally derived
    private static readonly int LABEL_INDENT = 29;
    private static readonly int FIELD_INDENT = 14;

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
        int length = GetEnumNames(property).Length;
        return EditorGUIUtility.singleLineHeight * (length + 1);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        EditorGUI.BeginProperty(position, label, property);
        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;

        //Array name
        EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);

        string[] enumNames = GetEnumNames(property);
        SerializedProperty data = property.FindPropertyRelative("_data");

        //Force this for when the enum changes size but the EnumeratedArray is not reinitialized
        data.arraySize = enumNames.Length;

        for (int i = 0; i < enumNames.Length; ++i) {
            float top = position.yMin + EditorGUIUtility.singleLineHeight * (i+1);
            SerializedProperty arrayElement = data.GetArrayElementAtIndex(i);

            //Enum name
            Rect labelRect = new Rect(LABEL_INDENT, top, position.width,
                    EditorGUIUtility.singleLineHeight);
            EditorGUI.PrefixLabel(labelRect, GUIUtility.GetControlID(FocusType.Passive),
                    new GUIContent(enumNames[i]));

            //Property field
            Rect fieldRect = new Rect(EditorGUIUtility.labelWidth + FIELD_INDENT, top,
                    position.width - EditorGUIUtility.labelWidth, EditorGUIUtility.singleLineHeight);
            EditorGUI.PropertyField(fieldRect, arrayElement, GUIContent.none);
        }

        EditorGUI.indentLevel = indent;
    }

    private string[] GetEnumNames(SerializedProperty property) {
        object o = property.serializedObject.targetObject;
        string[] result = ((EnumeratedArrayInt)o.GetType().GetField(property.name).GetValue(o)).enumNames;
        return result;
    }
}

[CustomPropertyDrawer(typeof(EnumeratedArrayInt))]
public class EnumeratedIntArrayDrawer : EnumeratedArrayDrawer {
}

A few notes on implementation:

  • The enum type was converted to an argument to the EnumeratedArray’s constructor.  The storage type has to come from a subclass, which you have to declare both in EnumeratedArray.cs and EnumeratedArrayDrawer.cs in order to get Unity to draw it correctly.  So instead of
    private EnumeratedArray<float, MyEnum> things = new EnumeratedArray<float, MyEnum>();

    it’s:

    private EnumeratedArrayFloat things = new EnumeratedArrayFloat(typeof(MyEnum));
    

    And don’t forget to add EnumeratedArrayFloat to both files. Not the best workaround, but I’ve done worse hacks.

  • Type checking is done only in the editor, so hopefully this won’t have a significant running time penalty when compared to just using an array.  I don’t know, I don’t have Unity Pro yet, so I can’t run the profiler.
  • That funky syntax
    public T this[int index] {

    is called an “indexer”. That’s what lets you use square brackets to access array elements. It’s the equivalent of C++’s operator[].

Let me know if you have any comments, questions, or improvements to the code! Both files are available for download by clicking here.