Skip to content

Instantly share code, notes, and snippets.

@shane-harper
Created May 16, 2025 09:47
Show Gist options
  • Save shane-harper/3014f80d52fa74d6f9728bbd85f21a22 to your computer and use it in GitHub Desktop.
Save shane-harper/3014f80d52fa74d6f9728bbd85f21a22 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using UnityEngine;
public class Example : MonoBehaviour
{
[SerializeReference] public List<IExampleInterface> List = new();
}
#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(Example), true)]
public class ExampleEditor : UnityEditor.Editor
{
private UnityEditorInternal.ReorderableList _list;
private void OnEnable()
{
_list = SerializableTypeList.Create<IExampleInterface>(serializedObject, "List", ((Example)target).List);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
_list.DoLayoutList();
serializedObject.ApplyModifiedProperties();
}
}
#endif
public interface IExampleInterface
{
}
[Serializable]
public struct ExampleTypeOne : IExampleInterface
{
public string Text;
}
[Serializable]
public struct ExampleTypeTwo : IExampleInterface
{
public int Number;
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
/// <summary>
/// Utility class for creating ReorderableLists that can be used to easily manage lists containing different types within a component
/// </summary>
public static class SerializableTypeList
{
/// <summary>
/// Create a new ReorderableList
/// </summary>
/// <param name="serializedObject">Serialized object containing the list</param>
/// <param name="propertyName">Name of the list property</param>
/// <param name="elements">Access to the list for adding new entries</param>
/// <typeparam name="T">Required type/interface</typeparam>
public static ReorderableList Create<T>(SerializedObject serializedObject, string propertyName, IList elements)
{
var serializedProperty = serializedObject.FindProperty(propertyName);
return new ReorderableList(serializedObject, serializedProperty, true, true, true, true)
{
headerHeight = 0,
drawElementCallback = (rect, index, _, _) =>
{
// Indent rect so that foldout arrow doesn't overlap the drag handle
const float indentWidth = 10f;
rect.x += indentWidth;
rect.width -= indentWidth;
// Get display name and draw property
var element = serializedProperty.GetArrayElementAtIndex(index);
var typeName = element.managedReferenceFullTypename;
var split = typeName.Split('.', ' ');
var label = new GUIContent(ObjectNames.NicifyVariableName(split[split.Length - 1]));
EditorGUI.PropertyField(rect, element, label, true);
},
elementHeightCallback = index =>
{
// Use height of element
var element = serializedProperty.GetArrayElementAtIndex(index);
return EditorGUI.GetPropertyHeight(element, true);
},
onAddDropdownCallback = (_, _) =>
{
// Display a context menu containing each type
var types = GetSerializableTypes<T>();
var menu = new GenericMenu();
foreach (var type in types)
{
var displayName = ObjectNames.NicifyVariableName(type.Name);
menu.AddItem(new GUIContent(displayName), false, () =>
{
// Attempt to add new instance of selected type
try
{
var instance = (T)Activator.CreateInstance(type);
elements.Add(instance);
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(serializedObject.targetObject);
}
catch (Exception ex)
{
Debug.LogErrorFormat(serializedObject.targetObject, "Failed to add instance of type '{0}'. {1}", type, ex.Message);
}
});
}
menu.ShowAsContext();
}
};
}
private static IEnumerable<Type> GetSerializableTypes<T>()
{
return AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => p.GetCustomAttributes(typeof(SerializableAttribute), true).Length > 0)
.Where(p => !p.IsAbstract && typeof(T).IsAssignableFrom(p));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment