Sure, it’s a little complicated but here are the parts:
UseGameEventEditor
has this inspector:
[CustomPropertyDrawer(typeof(UseGameEventEditorAttribute))]
public class GameEventPropertyDrawer : PropertyDrawer
{
private bool _isInitialized = false;
// Caching the calculated height to avoid recalculating multiple times
private float _cachedHeight = EditorGUIUtility.singleLineHeight;
private Dictionary<string, bool> _foldouts = new Dictionary<string, bool>();
private Dropdown _dropdown = new Dropdown("Events");
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var gameEventAttribute = attribute as UseGameEventEditorAttribute;
// Initialize dropdown only once
if (!_isInitialized)
{
_dropdown.ClearEntries();
InitializeDropdownOptions();
_dropdown.Label = property.displayName;
_dropdown.OnEntrySelected = null; // Clear all existing event handlers
_dropdown.OnEntrySelected += e => OnGameEventSelected(e, property);
_isInitialized = true;
}
EditorGUI.BeginProperty(position, label, property);
// Synchronize dropdown with serialized property value
UpdateDropdownSelection(property);
// Reserve space for the dropdown above the fields
Rect dropdownPosition = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
// Draw the dropdown
_dropdown.DrawDropdown(dropdownPosition);
// Calculate the starting position for fields below the dropdown
float fieldsStartY = dropdownPosition.yMax + EditorGUIUtility.standardVerticalSpacing;
Rect fieldsPosition = new Rect(position.x, fieldsStartY, position.width, EditorGUIUtility.singleLineHeight);
// Render the GameEvent fields dynamically if a value is selected
if (gameEventAttribute != null && gameEventAttribute.ShowProperties)
{
if (property.managedReferenceValue != null)
{
// Draw the fields below the dropdown
var gameEventInstance = property.managedReferenceValue;
DrawGameEventFields(ref fieldsPosition, gameEventInstance);
// Update cached height for proper layout updates
_cachedHeight = fieldsPosition.yMax - position.y;
}
else
{
// Reset to default height if no value is selected
_cachedHeight = dropdownPosition.height;
}
}
EditorGUI.EndProperty();
}
private void InitializeDropdownOptions()
{
var gameEventAttribute = attribute as UseGameEventEditorAttribute;
_dropdown.AddEntryRange(
GameEventService.GetGameEventsInProjectAsDropdownEntry(gameEventAttribute.IncludeSystemEvents));
}
private void OnGameEventSelected(DropdownEntry selectedEntry, SerializedProperty property)
{
// Cast the data back to GameEventHumanized
var selectedEvent = (GameEventHumanized)selectedEntry.Value;
if (selectedEvent != null && selectedEvent.defaultConstructor)
{
property.managedReferenceValue = Activator.CreateInstance(selectedEvent.type);
}
else
{
property.managedReferenceValue = null;
}
// Apply changes to the serialized object to notify Unity
property.serializedObject.ApplyModifiedProperties();
property.serializedObject.Update();
}
private void UpdateDropdownSelection(SerializedProperty property)
{
// If there is a serialized value, find its corresponding index in the dropdown
if (property != null && property.managedReferenceValue != null && _dropdown.Entries.Count > 0)
{
var currentValue = property.managedReferenceValue.GetType();
var selectedIndex =
_dropdown.Entries.FindIndex(entry => ((GameEventHumanized)entry.Value)?.type == currentValue);
if (selectedIndex >= 0)
{
_dropdown.selectedIndex = selectedIndex; // Synchronize dropdown with serialized value
}
}
else
{
_dropdown.selectedIndex = 0; // Default to the first option if no value is set
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
// Return the cached height for this property
return _cachedHeight;
}
private void DrawGameEventFields(ref Rect position, object gameEventInstance)
{
if (gameEventInstance == null) return;
var type = gameEventInstance.GetType();
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (!field.IsPublic && field.GetCustomAttribute<SerializeField>() == null) continue;
// Nicify the label and get the field value
string fieldLabel = ObjectNames.NicifyVariableName(field.Name);
object fieldValue = field.GetValue(gameEventInstance);
// Render the field using the utility method
position = RenderField(position, fieldLabel, gameEventInstance, field, fieldValue);
}
}
private Rect DrawClassField(Rect position, object instance, FieldInfo field, string label, object value)
{
// Ensure foldout state exists for this field
if (!_foldouts.TryGetValue(label, out bool isExpanded))
{
isExpanded = false;
_foldouts[label] = isExpanded;
}
// Render the foldout and update its state
_foldouts[label] =
EditorGUI.Foldout(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight),
_foldouts[label], label);
position.y += EditorGUIUtility.singleLineHeight;
// If expanded, draw the class fields
if (_foldouts[label])
{
if (value == null)
{
// Allow creating an instance of the class if it's null
if (GUI.Button(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight),
"Create Instance"))
{
value = Activator.CreateInstance(field.FieldType);
field.SetValue(instance, value);
}
position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
}
else
{
// Get all nested fields
var nestedFields = field.FieldType
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(f => f.IsPublic || f.IsDefined(typeof(SerializeField)));
foreach (var nestedField in nestedFields)
{
string nestedLabel = ObjectNames.NicifyVariableName(nestedField.Name);
object nestedValue = nestedField.GetValue(value);
// Render each nested field using the utility method
position = RenderField(position, nestedLabel, value, nestedField, nestedValue);
}
}
}
return position;
}
private Rect RenderField(Rect position, string label, object instance, FieldInfo field, object fieldValue)
{
// Handle different field types
if (field.FieldType == typeof(int))
{
fieldValue = EditorGUI.IntField(position, label, (int)fieldValue);
}
else if (field.FieldType == typeof(float))
{
fieldValue = EditorGUI.FloatField(position, label, (float)fieldValue);
}
else if (field.FieldType == typeof(bool))
{
fieldValue = EditorGUI.Toggle(position, label, (bool)fieldValue);
}
else if (field.FieldType == typeof(string))
{
fieldValue = EditorGUI.TextField(position, label, (string)fieldValue);
}
else if (field.FieldType == typeof(Vector3))
{
fieldValue = EditorGUI.Vector3Field(position, label, (Vector3)fieldValue);
}
else if (field.FieldType == typeof(Vector4))
{
fieldValue = EditorGUI.Vector4Field(position, label, (Vector4)fieldValue);
}
else if (field.FieldType == typeof(Color))
{
fieldValue = EditorGUI.ColorField(position, label, (Color)fieldValue);
}
else if (field.FieldType == typeof(Rect))
{
fieldValue = EditorGUI.RectField(position, label, (Rect)fieldValue);
}
else if (field.FieldType == typeof(Quaternion))
{
Quaternion quaternion = (Quaternion)fieldValue;
Vector4 vector = new Vector4(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
vector = EditorGUI.Vector4Field(position, label, vector);
fieldValue = new Quaternion(vector.x, vector.y, vector.z, vector.w);
}
else if (typeof(Object).IsAssignableFrom(field.FieldType))
{
fieldValue = EditorGUI.ObjectField(position, label, fieldValue as Object, field.FieldType, true);
}
else if (field.FieldType.IsEnum)
{
fieldValue = EditorGUI.EnumPopup(position, label, (Enum)fieldValue);
}
else if (field.FieldType.IsClass && field.FieldType != typeof(object))
{
// Handle nested class fields recursively
position = DrawClassField(position, instance, field, label, fieldValue);
return position; // Return here because DrawClassField adjusts the position itself
}
else
{
EditorGUI.LabelField(position, label, $"Unsupported type: {field.FieldType.Name}");
}
// Apply the updated value back to the field
field.SetValue(instance, fieldValue);
// Move the position for the next field
position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
return position;
}
}
Reflection is needed, because GameEvent
is a non unity object and also it’s abstract
Here is the dropdown used in that drawer:
public class Dropdown
{
public string Label;
public List<DropdownEntry> Entries;
public Action<DropdownEntry> OnEntrySelected;
public int selectedIndex = -1; // Default to -1 to indicate no selection
private DropdownEntry selectedEntry; // Cache the selected entry for efficient comparisons
public Dropdown()
{
ClearEntries();
Label = string.Empty;
}
public Dropdown(string label)
{
ClearEntries();
Label = label;
}
public void AddEntryRange(List<DropdownEntry> entries)
{
Entries.AddRange(entries);
}
public void AddEntry(DropdownEntry entry)
{
Entries.Add(entry);
}
public void RemoveEntry(DropdownEntry entry)
{
Entries.Remove(entry);
CorrectInvalidSelection();
}
public void RemoveEntry(int index)
{
if (index >= 0 && index < Entries.Count)
{
Entries.RemoveAt(index);
CorrectInvalidSelection();
}
}
public void ClearEntries()
{
Entries = new List<DropdownEntry>();
selectedEntry = null;
selectedIndex = -1; // Reset selection
}
private void CorrectInvalidSelection()
{
// Ensure a valid selection index and entry
if (selectedIndex >= Entries.Count)
{
selectedIndex = Entries.Count - 1;
}
if (selectedIndex >= 0 && selectedIndex < Entries.Count)
{
selectedEntry = Entries[selectedIndex];
}
else
{
selectedEntry = null;
selectedIndex = -1;
}
}
public void DrawDropdown()
{
UpdateSelectionFromEntries();
// Draw the dropdown
int newSelection =
EditorGUILayout.Popup(Label, selectedIndex, Entries.ConvertAll(entry => entry.Name).ToArray());
ProcessNewSelection(newSelection);
}
public void DrawDropdown(Rect position)
{
UpdateSelectionFromEntries();
// Draw the dropdown using explicit Rect-based positioning
int newSelection = EditorGUI.Popup(position, Label, selectedIndex, Entries.ConvertAll(entry => entry.Name).ToArray());
ProcessNewSelection(newSelection);
}
private void UpdateSelectionFromEntries()
{
// Ensure the selectedIndex is valid, even after entries have changed
if (selectedIndex < 0 || selectedIndex >= Entries.Count)
{
selectedIndex = -1;
selectedEntry = null;
}
}
private void ProcessNewSelection(int newSelection)
{
if (newSelection != selectedIndex)
{
selectedIndex = newSelection;
if (selectedIndex >= 0 && selectedIndex < Entries.Count)
{
selectedEntry = Entries[selectedIndex];
OnEntrySelected?.Invoke(selectedEntry);
}
else
{
selectedEntry = null;
}
}
}
}
And then, in a custom editor I had this to draw the list of the playback object that is in the post:
private void DrawPlaybackEventsList()
{
for (int i = 0; i < playbackEventsProperty.arraySize; i++)
{
SerializedProperty element = playbackEventsProperty.GetArrayElementAtIndex(i);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// Display playback time
SerializedProperty timeProp = element.FindPropertyRelative("playbackTime");
timeProp.floatValue = EditorGUILayout.Slider("Time", timeProp.floatValue, 0f, 1f);
// Display associated GameEvent dropdown with unique PropertyField
SerializedProperty eventProp = element.FindPropertyRelative("associatedEvent");
if (eventProp != null)
{
EditorGUILayout.PropertyField(eventProp, new GUIContent("Event"), true);
}
else
{
Debug.LogError($"associatedEvent not found in PlaybackEvent at index {i}.");
}
// Remove button
if (GUILayout.Button("Remove"))
{
playbackEventsProperty.DeleteArrayElementAtIndex(i);
}
EditorGUILayout.EndVertical();
}
}
I have an updated code, where I made it work only one form for editing any of the options in the list (the user clicks on an object of the list, and then gets to edit that element individually).
But I would love to know why this behavior is happening when I have multiple in a list