Skip to content

Instantly share code, notes, and snippets.

@ByronMayne
Last active September 27, 2022 19:04
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ByronMayne/70a46e73f3af7fb9fec7437174dd4858 to your computer and use it in GitHub Desktop.
Save ByronMayne/70a46e73f3af7fb9fec7437174dd4858 to your computer and use it in GitHub Desktop.
Override the default UnityEventDrawer to draw a very small version when there are no callbacks.
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Events;
[CustomPropertyDrawer(typeof(UnityEvent))]
public class CollapsableEventDrawer : UnityEventDrawer
{
private class Styles
{
public readonly GUIStyle preButton = "RL FooterButton";
public GUIContent iconToolbarPlus = EditorGUIUtility.IconContent("Toolbar Plus", "|Add to list");
}
private const string CALLS_PROPERTY_PATH = "m_PersistentCalls.m_Calls";
private const string GET_STATE_METHOD_NAME = "GetState";
private const string REORDERABLE_LIST_FIELD_NAME = "m_ReorderableList";
private const float BUTTON_WIDTH = 30;
private const float BUTTON_SPACING = 30;
private static readonly BindingFlags NON_PULIC_INSTANCE_FLAGS = BindingFlags.NonPublic | BindingFlags.Instance;
// Points the field inside Sate which has the reorderable list.
private static FieldInfo _stateReorderableListFieldInfo;
// Points to the GetState method defined in UnityEventDrawer.
private static MethodInfo _getStateMethod;
// Cached array for using reflection
private static object[] _getStateArgs = new object[1];
// A class that contains all the custom styling we need
private static Styles _styles;
// True if we have persistent calls and false if we don't.
private bool _hasPersistentCalls;
// Holds all our reorderable lists that belong to our state
private IDictionary<State, ReorderableList> _lists;
/// <summary>
/// Used to initialize our values.
/// </summary>
public CollapsableEventDrawer()
{
_lists = new Dictionary<State, ReorderableList>();
}
/// <summary>
/// A wrapper around the GetState function that is private in <see cref="UnityEventDrawer"/>
/// </summary>
private State GetState(SerializedProperty property)
{
if (_getStateMethod == null)
{
_getStateMethod = typeof(UnityEventDrawer).GetMethod(GET_STATE_METHOD_NAME, NON_PULIC_INSTANCE_FLAGS);
}
_getStateArgs[0] = property;
return (State)_getStateMethod.Invoke(this, _getStateArgs);
}
/// <summary>
/// Tries to get the cached ReorderableList from the state.
/// </summary>
private ReorderableList GetList(SerializedProperty property)
{
State state = GetState(property);
if(_lists.ContainsKey(state))
{
return _lists[state];
}
ReorderableList list = GetReorderableListFromState(state);
list.draggable = true;
_lists[state] = list;
return list;
}
/// <summary>
/// Returns back the number of calls that are currently in the Unity Event.
/// </summary>
private bool HasPersistentCalls(SerializedProperty property)
{
return property.FindPropertyRelative(CALLS_PROPERTY_PATH).arraySize > 0;
}
/// <summary>
/// Returns to Unity to tell it how much space we should use to draw. If we
/// have one element we only reserve 32 units. 16 for the element itself and
/// 16 for spacing from the next element. If we any calls we let Unity
/// draw the default.
/// </summary>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
_hasPersistentCalls = HasPersistentCalls(property);
ReorderableList list = GetList(property);
if (!_hasPersistentCalls)
{
list.elementHeight = 0;
list.displayAdd = false;
list.displayRemove = false;
return EditorGUIUtility.singleLineHeight * 2f;
}
list.elementHeight = 43;
list.displayAdd = true;
list.displayRemove = true;
return base.GetPropertyHeight(property, label);
}
/// <summary>
/// Used to draw our element override a bit of the Unity logic in the case
/// of having no elements.
/// </summary>
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if(_styles == null)
{
_styles = new Styles();
}
base.OnGUI(position, property, label);
if (!_hasPersistentCalls)
{
position.x += position.width - BUTTON_SPACING;
position.width = BUTTON_WIDTH;
if(GUI.Button(position, _styles.iconToolbarPlus, _styles.preButton))
{
State state = GetState(property);
ReorderableList list = GetReorderableListFromState(state);
list.onAddCallback(list);
state.lastSelectedIndex = 0;
}
}
}
/// <summary>
/// Gets the internal instance of the <see cref="ReorderableList"/> that exists
/// in the state.
/// </summary>
private static ReorderableList GetReorderableListFromState(State state)
{
if (_stateReorderableListFieldInfo == null)
{
_stateReorderableListFieldInfo = typeof(State).GetField(REORDERABLE_LIST_FIELD_NAME, NON_PULIC_INSTANCE_FLAGS);
}
return (ReorderableList)_stateReorderableListFieldInfo.GetValue(state);
}
}
@ByronMayne
Copy link
Author

eventdrawers
This is what it looks like

@ByronMayne
Copy link
Author

Right now this is just the drawer for any UnityEvent type with no argument. If you want it to draw on other event types you have to add it to the attribute above.

@woistjadefox
Copy link

woistjadefox commented Jan 26, 2018

Holy crap man, you made my day! This stuff should be so badly built-in within the editor.
Another feature I would love to see, is that a missing reference to a method or object is shown by a warning in the console. Or another way to detect broken event links. Cheers

@GordeyChernyy
Copy link

Wow! It has rearrange function too! Thanks!

@thestonefox
Copy link

@ByronMayne great solution to tidying up UnityEvents in the inspector. I was trying to get it to also work with UnityEvent without needing to specify every custom T0

Do you know if this is possible?

I tried typeof(UnityEvent<>), true but it didn't work. I'm having to specify every actual object like typeof(UnityEvent<MyType>), true

@ByronMayne
Copy link
Author

You have to do it per type unfortunately. Unless that has changed. I have not used Unity for about 7 months so I have not been keeping up to date

@ByronMayne
Copy link
Author

Saying that you might be able to break into Unity with reflection and do it but that will take some digging

@thestonefox
Copy link

Yeah I think reflection is the only way I can see of doing it. I'll get something working and post the result back here in case anyone comes across this and needs a solution in the future!

@HolgerDurach
Copy link

Yes, I know I am late to the party, but this one is still useful, and by now you can use [CustomPropertyDrawer(typeof(UnityEventBase))] to have it also work for events with parameters.

@ByronMayne
Copy link
Author

I wrote this 4 years ago I am surprised it's still useful that is... unexpected. hahah

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment