MattFace
MattFace

Reputation: 335

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
    }
}

Upvotes: 4

Views: 910

Answers (2)

derHugo
derHugo

Reputation: 90789

Instead of a hardcoded default element height of

X * EditorGUIUtility.singleLineHeight

you should rather use EditorUtility.GetPropertyHeight and get the actual height of the properties like

private float ElementHeight(int index)
{
    var property = list.serializedProperty.GetArrayElementAtIndex(index);
    return EditorUtility.GetPropertyHeight(property);
}

This returns the height the property would require using it's default property drawer - the one that is applied using PropertyField which you do ;)


Besides that: What is the purpose of your custom editor exactly?

Since Unity 2020 the default drawer for lists and arrays is the ReorderableList anyway so to be honest I don't really see your custom editor add anything that wouldn't be the default Inspector anyway :)

This is how it looks like without your custom editor:

enter image description here

If you rather want to change how a Sequence is drawn in the Inspector in general, you would be better implementing a custom PropertyDrawer instead and not have to deal with the list at all.

And the drawer dealing with the index is btw dangerous / not reliable!

Currently your index is only applied if this is actually opened in the Inspector and not in Debug mode. What if later you want to modify this via script?

In my opinion it would be better if your SequenceManager rather simply passes in the index into the Init method if it is really needed for anything except the log ;)


Also be very careful: In your Sequence script you have

using UnityEditor;

be aware that this namespace is only available within the Unity editor itself and completely stripped of during the build.

=> You want to make sure that nothing of this namespace is trying to be used in a built application so you will have to wrap any references to this namespace in according pre-processor tags like e.g.

#if UNITY_EDITOR
using UnityEditor;
#endif

and the same for any code related to this namespace (see also Platform Dependent Compilation)

Upvotes: 2

Bacon Nugget
Bacon Nugget

Reputation: 117

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;

Upvotes: 2

Related Questions