4

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
    }
}
derHugo
  • 83,094
  • 9
  • 75
  • 115
MattFace
  • 317
  • 3
  • 14

2 Answers2

2

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;
Bacon Nugget
  • 107
  • 11
2

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)

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Will a using statement for an assembly not included in the build still cause errors if nothing in the code references anything in that assembly? I've never come across that before – Bacon Nugget Mar 02 '22 at 15:21
  • 1
    @BaconNugget well that's exactly what the pre-processor tags are about. During the build process any code between the pre-processors will simply not end up in the compiled code (basically like comments) => in the built there is no using statement ;) – derHugo Mar 02 '22 at 15:53
  • Usually I do use preprocessor tags, but I assumed that the compiler would clean up any using directives for assemblies that are not referenced in the code. I found out that's not necessarily true in [this post](https://stackoverflow.com/questions/4163320/unused-using-statements). – Bacon Nugget Mar 02 '22 at 17:54
  • 1
    @BaconNugget oh sorry I think I understood the question wrong. Yes, a `using` statement can throw errors even though it is actually not necessary in a script and nothing actually uses it! – derHugo Mar 02 '22 at 19:17
  • @derHugo Thank you, this is some really useful feedback. I'll likely make these changes tomorrow. I'm stuck on 2019.4 for this project so i can't use the fancy new 2020 features unfortunately, but I've learned a lot from this exercise. Thanks again for the help! – MattFace Mar 03 '22 at 07:55