Skip to content

Instantly share code, notes, and snippets.

@shane-harper
Last active May 19, 2025 07:11
Show Gist options
  • Save shane-harper/7bd579c3f6ca0a419f056fb9bb49b681 to your computer and use it in GitHub Desktop.
Save shane-harper/7bd579c3f6ca0a419f056fb9bb49b681 to your computer and use it in GitHub Desktop.
using System;
using UnityEngine;
#if UNITY_EDITOR
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
#endif
/// <summary>
/// Attribute used to annotate a serialized list field to automatically align its length and elements
/// with the values of a specified enum type. Each element in the list corresponds to a value in the enum.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class EnumListAttribute : PropertyAttribute
{
public readonly Type EnumType;
public EnumListAttribute(Type type)
{
EnumType = type;
}
}
#if UNITY_EDITOR
/// <summary>
/// Custom property drawer for fields annotated with the <see cref="EnumListAttribute"/>.
/// Automatically ensures that the serialized list matches the number of values in the specified enum type,
/// aligning each list element with its corresponding enum value.
/// </summary>
[CustomPropertyDrawer(typeof(EnumListAttribute))]
internal class EnumListAttributeDrawer : PropertyDrawer
{
private List<string> _cachedNames;
private Type _cachedType;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (attribute is not EnumListAttribute { EnumType: { IsEnum: true } } enumListAttribute)
{
EditorGUI.PropertyField(position, property, label, true);
return;
}
var names = GetEnumNames(enumListAttribute.EnumType);
if (!ValidateArraySize(property, names.Count))
{
property.serializedObject.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(property.serializedObject.targetObject);
}
var index = GetIndexFromPropertyPath(property.propertyPath);
var displayLabel = index < names.Count
? new GUIContent(ObjectNames.NicifyVariableName(names[index]))
: label;
position.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(position, property, displayLabel, true);
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, true);
}
private IReadOnlyList<string> GetEnumNames(Type type)
{
if (_cachedNames != null && _cachedType == type)
{
return _cachedNames;
}
_cachedType = type;
_cachedNames = Enum.GetValues(type)
.Cast<Enum>()
.Where(e => Convert.ToInt32(e) >= 0)
.Select(e => Enum.GetName(type, e))
.ToList();
return _cachedNames;
}
private static int GetIndexFromPropertyPath(string path)
{
var match = Regex.Match(path, @"\[(\d+)\]");
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
}
private static bool ValidateArraySize(SerializedProperty property, int requiredSize)
{
var parent = GetParentArrayProperty(property);
if (parent == null || parent.arraySize == requiredSize)
{
return false;
}
parent.arraySize = requiredSize;
return true;
}
private static SerializedProperty GetParentArrayProperty(SerializedProperty property)
{
var path = property.propertyPath;
var arrayPath = path[..path.LastIndexOf(".Array", StringComparison.Ordinal)];
return property.serializedObject.FindProperty(arrayPath);
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment