Search code examples
c#unity-game-engineunity-editorunity3d-editor

Unity3D Display a UnityEvent Corrrectly in a ReorderableList


I am trying to make a custom inspector for my sequence class. The idea is to allow the user to configure various UnityEvents that get called at the start or the end of a sequence.

I want to have a collection of sequences in a ReorderableList so that it is highly configurable inside the inspector.

I am very close to a solution but i am fairly inexperienced with editor scripting. My scripts are almost working correctly. But I still need to solve the best way to dynamically adjust the vertical height of each item, in the DrawListItem method, and the total vertical height in the ElementHeight Method.

I am considering trying to deserialize the unity events so that i can use the GetPersistentEventCount method to get an idea of the required vertical height, but this seems like it is probably overkill. I suspect that there must be a simpler way to retrieve this data.

Currently when i add multiple items to the sequence I am getting what is pictured below, where the event fields overlap each other and the add/remove buttons are beneath the lower Unity Event.

Does anyone know the best way to resolve this?

enter image description here

using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System;
using UnityEngine.Events;

[CustomEditor(typeof(SequenceManager))]
public class SequenceManagerEditor : Editor
{
    SerializedProperty Sequences;

    ReorderableList list;
    private void OnEnable()
    {
        Sequences = serializedObject.FindProperty("Sequences");
        list = new ReorderableList(serializedObject, Sequences, true, true, true, true);
        list.drawElementCallback = DrawListItems;
        list.drawHeaderCallback = DrawHeader;
        list.elementHeightCallback = ElementHeight;
    }

    //Draws the elements in the list
    void DrawListItems(Rect rect, int index, bool isActive, bool isFocused)
    {
        SerializedProperty element = list.serializedProperty.GetArrayElementAtIndex(index);
        

        ////NAME
        EditorGUI.LabelField(new Rect(
            rect.x, 
            rect.y + EditorGUIUtility.standardVerticalSpacing, 
            50, 
            EditorGUIUtility.singleLineHeight), "Name");
        EditorGUI.PropertyField(
            new Rect(
                rect.x + 50, 
                rect.y + EditorGUIUtility.standardVerticalSpacing, 
                rect.width - 50, 
                EditorGUIUtility.singleLineHeight),
            element.FindPropertyRelative("Name"),
            GUIContent.none
            );

        //ON INIT
            EditorGUI.LabelField(new Rect(
            rect.x, 
            rect.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing * 3, 
            50, 
            EditorGUIUtility.singleLineHeight), "OnInit");
        EditorGUI.PropertyField(new Rect(
                rect.x + 50, 
                rect.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing * 3, 
                rect.width - 50, 
                3 * rect.y + 5 * EditorGUIUtility.singleLineHeight),
            element.FindPropertyRelative("OnInit"),
            GUIContent.none);

        //ON DONE
        EditorGUI.LabelField(new Rect(
            rect.x, 
            rect.y + 7 * EditorGUIUtility.singleLineHeight, 
            50, 
            EditorGUIUtility.singleLineHeight), "OnDone");
        EditorGUI.PropertyField(
            new Rect(
                rect.x + 50, 
                rect.y + 7 * EditorGUIUtility.singleLineHeight, 
                rect.width - 50, 
                3 * rect.y + 12 * EditorGUIUtility.singleLineHeight),
               element.FindPropertyRelative("OnDone"),
            GUIContent.none);

        SerializedProperty indexProperty = element.FindPropertyRelative("index");
        indexProperty.intValue = index;
    }

    private float ElementHeight(int index)
    {
        return (13 * EditorGUIUtility.singleLineHeight);
    }

    //Draws the header
    void DrawHeader(Rect rect)
    {
        string name = "Sequences";
        EditorGUI.LabelField(rect, name);
    }


    public override void OnInspectorGUI()
    {
        //base.OnInspectorGUI();
        serializedObject.Update();
        this.list.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
     }    
 }

for the sake of completeness, I've also added the Sequence class and SequenceManager class below.

using UnityEngine;
using UnityEditor;
using System;
using UnityEngine.Events;

[Serializable]
public class Sequence
{
    public string Name;
    public UnityEvent OnInit;
    public UnityEvent OnDone;
    private Module _module;
    public int index;
    private bool active;

    //Called By The Sequence Manager At the start of a sequence 
    internal void Init(Module p_module)
    {
        Debug.Log($"sequence: {Name} with index: {index} has started");
        active = true;
        _module = p_module;
         if(OnInit.HasNoListners())
        {
            Done();
        } 
        else
        {
            OnInit.Invoke();
        }
    }
    
    
    //Called Manually to Trigger the End of the Sequence
    internal void Done()
    {
        if (!OnDone.HasNoListners())
        {
            OnDone.Invoke();
        }
        active = false;
        Debug.Log($"sequence: {Name} with index: {index} is done");
        _module.FinishedSequence(index);
    }

    //Check if active
    internal bool GetActive()
    {
        return active;
    }
}

using System;
namespace UnityEngine
{
    [Serializable]
    public class SequenceManager: MonoBehaviour
    {
       
        #region Properties
        public Sequence[] Sequences;
        #endregion
    }
}

Solution

  • It's possible to access public fields of Sequence by casting the elements of serializedObject.targetObjects in your editor script. You can also use serializedObject.targetObject or Editor.target if you aren't using [CanEditMultipleObjects].

    if(target is not SequenceManager manager)
    {
        //target was not a SequenceManager, the manager variable will be null
        return;
    }
    

    and within ElementHeight():

    var sequence = manager.Sequences[index];
    var size = sequence.OnInit.GetPersistentEventCount()
             + sequence.OnDone.GetPersistentEventCount();
    

    Edit - After trying out some things, I learned that you can get the array size through the serialized property. You can just use this in ElementHeight():

    var element = list.serializedProperty.GetArrayElementAtIndex(index);
    var size = element.FindPropertyRelative("OnInit.m_PersistentCalls.m_Calls").arraySize
             + element.FindPropertyRelative("OnDone.m_PersistentCalls.m_Calls").arraySize;