I have a class, EnumMap<TEnum,TValue>
, for associating each member of an enum type with a value of a given type, since I find myself running into situations like that frequently. It acts similarly to a dictionary, but is stored as parallel Lists for serialization.
Since, during design time, I want to edit the contents of the value list but not its structure (and I don't want to edit the enum list at all), I wrote a custom property drawer that renders EnumMaps as if they were basic Serializable C# classes with a named member for each value of the enum. That is, if you have an enum defined like enum Example { A, B, C }
, and a field with the type EnumMap<Example, int>
, the field will render as if it's an instance of a class with three int
fields named A
, B
, and C
:
[Serializable]
public class EquivalentClass
{
public int A, B, C;
}
The property drawer works great in every situation I've tested so far except for when an EnumMap
maps to an EnumMap
- ie, EnumMap<SomeEnumType, EnumMap<SomeOtherEnumType, SomeValueType>>
. In that case, the child EnumMaps display the names of the parent enum type, and the property drawer errors out if the child enum has more name values than the parent.
I believe it has to do with how I'm getting the display names of the enums, but the documentation on SerializedProperty.enumDisplayNames
and SerializedProperty.enumValueIndex
isn't particularly helpful, and I haven't had any luck with Google. I've also tried SerializedProperty.enumNames
but that didn't seem to change anything. (Side question - what exactly is the difference between those?) It could also be something to do with how I'm actually accessing the property in the first place, since I'm pretty new to custom editors.
Question: what's the idiomatic way to retrieve display names for a given enum value when writing custom editors? And how can you ensure that nested properties are retrieving the right relative values, instead of their parents'?
Here's the code for EnumMap
:
[Serializable]
public class EnumMap<TEnum, TValue> : ISerializationCallbackReceiver
where TEnum : Enum
{
public TValue this[TEnum e]
{
get => m_values[getOrAddEnumPosition(e)];
set => m_values[getOrAddEnumPosition(e)] = value;
}
[SerializeField] List<TEnum> m_enums;
[SerializeField] List<TValue> m_values;
public void OnBeforeSerialize ()
{
createAndResizeLists();
}
public void OnAfterDeserialize ()
{
createAndResizeLists();
}
void createAndResizeLists ()
{
if (m_enums == null) m_enums = new List<TEnum>();
if (m_values == null) m_values = new List<TValue>();
foreach (var e in EnumUtil.AllValues<TEnum>())
{
getOrAddEnumPosition(e);
}
}
int getOrAddEnumPosition (TEnum e)
{
int position = m_enums.IndexOf(e);
if (position != -1)
{
return position;
}
else
{
m_enums.Add(e);
m_values.Add(default);
return m_enums.Count - 1;
}
}
}
And here's its property drawer:
[CustomPropertyDrawer(typeof(EnumMap<,>))]
public class EnumMapDrawer : PropertyDrawer
{
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
Rect foldoutPosition = position;
foldoutPosition.height = foldoutLabelHeight(label);
property.isExpanded = EditorGUI.Foldout(foldoutPosition, property.isExpanded, label, true);
if (property.isExpanded)
{
EditorGUI.indentLevel++;
Rect fieldPosition = EditorGUI.IndentedRect(position);
fieldPosition.y += foldoutLabelHeight(label) + EditorGUIUtility.standardVerticalSpacing; // ensure fields start below the label
var enums = property.FindPropertyRelative("m_enums");
var values = property.FindPropertyRelative("m_values");
for (int i = 0; i < enums.arraySize; i++)
{
var value = values.GetArrayElementAtIndex(i);
var currentEnum = enums.GetArrayElementAtIndex(i);
string enumLabel = currentEnum.enumNames[currentEnum.enumValueIndex];
fieldPosition.height = EditorGUI.GetPropertyHeight(value);
EditorGUI.PropertyField(fieldPosition, value, new GUIContent(enumLabel), true);
fieldPosition.y += fieldPosition.height + EditorGUIUtility.standardVerticalSpacing;
}
EditorGUI.indentLevel--;
}
EditorGUI.EndProperty();
}
public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
{
if (!property.isExpanded) return base.GetPropertyHeight(property, label);
var values = property.FindPropertyRelative("m_values");
float height = foldoutLabelHeight(label) + EditorGUIUtility.standardVerticalSpacing; // include the label and some space
for (int i = 0; i < values.arraySize; i++)
{
height += EditorGUI.GetPropertyHeight(values.GetArrayElementAtIndex(i), true) + EditorGUIUtility.standardVerticalSpacing;
}
return height;
}
float foldoutLabelHeight (GUIContent label)
{
return EditorGUI.GetPropertyHeight(SerializedPropertyType.String, label);
}
}
Here's some minimal code to reproduce the property drawer issue:
public enum LongerEnum
{
A, B, C, D
}
public enum ShorterEnum
{
One, Two, Three
}
public class EnumMapTester : MonoBehaviour
{
public EnumMap<ShorterEnum, EnumMap<LongerEnum, int>> Test;
}