[Custom Editor] Two Enums Same Row Problem

Hello,

I have been looking on the internet to solve this problem but did not find any solution. So basically I have this editor script:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(HexagonalGrid))]
public class HexagonalGridEditor : GridEditor
{
    private HexagonalGrid m_hexagonalGrid = null;

    /*******************************************************************/
    /************************ grid properties **************************/
    /*******************************************************************/

    private SerializedProperty m_orientation = null;
    private SerializedProperty m_type = null;
    private SerializedProperty m_pointToppedClassicType = null;
    private SerializedProperty m_flatToppedClassicType = null;

    /*******************************************************************/
    /********************** grid box properties ************************/
    /*******************************************************************/

    private SerializedProperty m_hexagonRadius = null;

    private void OnEnable()
    {
        m_hexagonalGrid = (HexagonalGrid)target;

        m_orientation = serializedObject.FindProperty("m_orientation");
        m_type = serializedObject.FindProperty("m_type");
        m_pointToppedClassicType = serializedObject.FindProperty("m_pointToppedClassicType");
        m_flatToppedClassicType = serializedObject.FindProperty("m_flatToppedClassicType");

        m_hexagonRadius = serializedObject.FindProperty("m_hexagonRadius");
    }

    public override void PropertiesInspector()
    {
        EditorGUILayout.Space();

        EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel);
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.PropertyField(m_type);
        EditorGUILayout.PropertyField(m_orientation);
        EditorGUILayout.EndHorizontal();

        if (m_hexagonalGrid.m_type == HexagonalGrid.Type.Classic)
        {
            if (m_hexagonalGrid.m_orientation == HexagonalGrid.Orientation.PointyTopped)
            {
                EditorGUILayout.PropertyField(m_pointToppedClassicType, new GUIContent("Subtype"));
            }
            else if (m_hexagonalGrid.m_orientation == HexagonalGrid.Orientation.FlatTopped)
            {
                EditorGUILayout.PropertyField(m_flatToppedClassicType, new GUIContent("Subtype"));
            }
        }

        EditorGUILayout.Space();

        EditorGUILayout.LabelField("Hexagon", EditorStyles.boldLabel);
        EditorGUILayout.PropertyField(m_hexagonRadius, new GUIContent("Radius"));

        EditorGUILayout.Space();
    }

}

My problem is at the beginning of the PropertiesInspector method (which is called by the mother class in the OnInspectorGUI method). I want to put the two enums m_type and m_pointToppedClassicType on the same row, but using BeginHorizontal gives me a result where the second enum is not entirely drawed in the inspector (and there is no scrollbar):

Any idea how to solve this ?

The automatic labels for stuff work really bad with drawing horizontally. You’ll want to remove the labels and add custom ones instead. I think this’ll work:

//old code:
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(m_type);
EditorGUILayout.PropertyField(m_orientation);
EditorGUILayout.EndHorizontal();

//replace with:
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Type");
EditorGUILayout.PropertyField(m_type, GUIContent.None);
EditorGUILayout.LabelField("Orientation");
EditorGUILayout.PropertyField(m_orientation, GUIContent.None);
EditorGUILayout.EndHorizontal();

Or you could just forgo the labels entirely.

PropertyFields are really cumbersome to use. It’s nice that they give you automatic undo and multi-object editing, but if you’re doing something that’s complex enough that you’re writing a custom editor anyway, it’s often better to ditch the SerializedProperty and work directly on the object instead.

The default field sizes and property values for what you’re doing are not sufficient. You need to tell the editor to layout the next field with sizes and configurations that work better. EditorGUIUtility class has some handy stuff that allows you to adjust the width of labels, prefixes, indents and all kinds of things, you can just revert it after you end the fields.

You can basically use the SerializedProperty with the flexibility you would get with using EditorGUILayout class to access the object directly, but with significantly better code readability. The advantages are great with SO, you just have to use the rest of the editor tools to lay things out like you want. It takes some trial and error, but its worth your time to understand.

There is a lot of control available.

Thank you for your quick answers!

@Baste , I just tried your solution and it’s actually even worth by adding custom label :-/. However, it works perfectly if I completely remove the labels. I’m opened for any new ideas =). Also, I’m kinda new to Unity Editor scripting, so I did not get your point when you were talking about “ditching the SerializedProperty and work directly on the object instead”. Would you be able to give me a simple example to illustrate ?

@LaneFox , I will have a look at EditorGUIUtility then, thanks for the advice :slight_smile:

There’s two basic ways to work with custom editors - a) access the class with the target variable or b) access the class with the serializedObject variable.

The difference is that working with serializedObject is a generic way to communicate with the class and Unity will handle all of the serialization, undo and basic layout stuff for you without any extra legwork. Using target will access the class specific, bypassing any automatic functions that serialization stuff would provide and simply directly changing the value of some thing on that class. The change is recorded to the class, but there is no undo support and the code to get it to display is much less friendly than using SerializedProperty.

Look at the code differences.

    public virtual void OnEnable()
    {
        // nothing necessary
    }
    public override void OnInspectorGUI()
    {
        target.WeaponMountPoint = EditorGUILayout.ObjectField("Weapon Mount", target.WeaponMountPoint, typeof (GameObject), true) as GameObject;
    }

…versus…

        public SerializedProperty WeaponMountPoint;

        public virtual void OnEnable()
        {
            WeaponMountPoint =    serializedObject.FindProperty("WeaponMountPoint");
        }
        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            EditorGUILayout.PropertyField(WeaponMountPoint);
            serializedObject.ApplyModifiedProperties();
        }

With the SerializedObject/SerializedProperty approach it is much cleaner to write code for the inspector and you get all of the benefits of undo and auto-property drawing without having to define what you’re actually telling it to draw unlike the alternative of direct target access where you have to know what you’re drawing beforehand, then use the appropriate call, define gui properties, and have no undo benefits.

The direct/target approach is really just something you should fall back on when there is absolutely no way to get the PropertyField to do what you want. This is a fairly uncommon occurrence though, unless you have some obscurely unique requirements in your inspector. You can even write custom PropertyDrawers to handle any weird cases and it’s probably still better than using direct/target.

1 Like

You’ll want to set the size of the label. You set that through the params argument to the LabelField (and any of the EditorGUILayout methods). You need to send in GUILayoutOptions, which you generally construct through factory methods in GUILayout.

Or in simpler terms, use GUILayout.Width to set the label’s width:

EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Type", GUILayout.Width(100f)); //100 is the number of pixels
EditorGUILayout.PropertyField(m_type, GUIContent.none);
EditorGUILayout.LabelField("Orientation", GUILayout.Width(100f));
EditorGUILayout.PropertyField(m_orientation, GUIContent.none);
EditorGUILayout.EndHorizontal();

I find that it almost never happens that I’m doing something that’s
a) complex enough that I want to use a custom editor
b) simple enough that SerializedProperty can handle the job.

Mostly this is because the vast majority of editor customization is better done with attributes + PropertyDrawers for those attributes. So if I want to say hide a thing in the inspector conditionally, or select values from a drop-down instead of through the default drawer, that’s an attribute and a drawer for that attribute.

For OP’s case, I would actually just not use a custom editor, because you now have to maintain an editor for something very simple. Just add a [Space] attribute to m_type and the field under m_orientation, and you’ll have grouped the elements nicely.

The only times left where I create custom editors is when:
a) I want to add buttons or extra debug info to the inspector, in which case neither of the options are relevant.

b) I want to create a drawer for a class that’s doing ISerializationCallbackReceiver stuff to serialize data manually. In that case, the data I’m drawing isn’t serializable by Unity, and by extension SerializedProperty cannot be used.

c) I’m doing interesting stuff with arrays or Lists. In that case, SerializedProperty doesn’t want to give me the array, it wants me to iterate the array through the most cumbersome interface*.

Here’s how I reset an array through the target (just the interesting bits):

if (GUILayout.Button("Reset array")) {
    Undo.RecordObject(script, "Reset values");
    script.array = new[] {"Foo", "Bar", "FooBar"};
}

Here’s the same thing with a SerializedProperty setup:

if (GUILayout.Button("Reset array")) {
    serializedObject.Update();

    arrayProp.ClearArray();
    arrayProp.arraySize = 3;

    arrayProp.GetArrayElementAtIndex(0).stringValue = "Foo";
    arrayProp.GetArrayElementAtIndex(1).stringValue = "Bar";
    arrayProp.GetArrayElementAtIndex(2).stringValue = "FooBar";

    serializedObject.ApplyModifiedProperties();
}

The boilerplate is the same - both need to assign something to a field in OnEnable - the typed script in the target version, the serializedProperty in the serializedProperty version. The serializedProperty version’s OnEnable boilerplate scales linearly with the number of fields you care about, where the target version’s OnEnable boilerplate is constant.

Here’s how I sort the array through the target:

if (GUILayout.Button("Reset array")) {
    Undo.RecordObject(script, "Reset values");
    Array.Sort(script.array);
}

Here’s the easiest way I could come up with to sort the array through serialized properties:

if (GUILayout.Button("Sort array")) {
    serializedObject.Update();

    var sortingArray = new string[arrayProp.arraySize];
    for (int i = 0; i < sortingArray.Length; i++) {
        sortingArray[i] = arrayProp.GetArrayElementAtIndex(i).stringValue;
    }

    Array.Sort(sortingArray);

    for (int i = 0; i < sortingArray.Length; i++) {
        arrayProp.GetArrayElementAtIndex(i).stringValue = sortingArray[i];
    }

    serializedObject.ApplyModifiedProperties();
}

I could also create a method in the script for sorting, but that’s often not an option. Besides, if I end up pushing editor code into the main script because the API for the editor code is bad, that’s just all over a bad day.

The serializedProperty code becomes incredibly cumbersome! If have some data that I want to work with, but instead of working with the data I’m working with a huge, string-based wrapper for the data with a very limiting API.

My general advice would be to:
Always use the .target if:

  • you’re working with data you serialize manually
  • you’re working with arrays

If none of those are the case, use SerializedProperty if multi-object editing is important (because that is horrible if you’re doing .target), and use .target otherwise.

  • Also note that the documentation doesn’t really ever say that serializedProperty.GetArrayElementAtIndex is the method you want to use to set array elements at an index. That’s just something you have to figure out.

Never really thought of it that way. I generally use custom editors for drawing all grids, horizontal/vertical groups, organizing variables into boxes, foldouts and stuff like that so I see plenty of use within a) && b). I suppose with all of the options available a lot of editor code often boils down to preference.

Absolutely, I totally agree and often do that for simple stuff.

Sweet blueberry juniper snacks, yes, it’s well known that SerializedProperty has the absolute worst interface for dealing with arrays and lists. There are some working solutions to make it more normal, but the stock API is completely abysmal for arrays and lists. SerializedProperty quickly goes from “this is nice” to “save me Oprah Winfrey!” when working with arrays.

Hey guys, thank you very much for all the thoughts! I know that in my case I do not need to write a custom editor, but I just want to learn about it through some small personal projects.

However I take note of all your advice and explanations, I actually did not think that a simple question would lead to such a consideration :smile: