One of my favorite things about Unity is the extensibility of the editor. In my last post, I described the item spawning system for Office Mayhem, but due to the ObjectEntry struct that was used to store the specific information in the public list, Unity’s default editor wasn’t displaying it very nicely in the inspector.

For the convenience of our level designer, we needed a better way to organize this data, and in a way that didn’t require the individual entries of this required objects list to be hard-coded. And Unity’s incredibly powerful editor extension features make this possible.

In my opinion, one of the most satisfying things you can do with this engine is create your own custom editor for a script – or even an entirely new tab/editor window, so I’m rather proud to show this off.

In Unity, all you need to do is create a subclass of Editor with an attribute tag for the type of component you’re making an editor for:

#if (UNITY_EDITOR)

// Usings ...

[CustomEditor(typeof(ElevatorSpawner))]
public class ElevatorSpawnerEditor : Editor
{
    // ... code ...
}

#endif

That #if (UNITY_EDITOR) directive is necessary to avoid compile errors when building the game, because – obviously – when you’re building the game for release you don’t need (and shouldn’t include) any Unity engine code. You can get around this by putting custom editor scripts in a special top-level assets folder: Assets/Editor. For our project, we would rather organize our assets differently – hence the #if directive. But it is very useful to note that a top level folder in your assets called Editor will be ignored by the compiler when building the project.


For the purposes of this custom editor, I used a ReorderableList object to as an intermediary between the actual class’s list of structs and the Unity inspector representation.

(It should probably be noted: If you noticed from my last post, the struct was marked with [System.Serializable]. All properties the editor will manage must be serializable for the custom editor to work properly.)

When using a ReorderableList, you need to define a drawElementCallback on the list, which does what it sounds like… draws each element in the list. In Unity editor extensions, a SerializedProperty is a completely generic property a Component or MonoBehaviour can have, and as long as it’s serializable, Unity will assign it an appropriate field to input the property in the editor. There are dozens of built-in types that Unity knows about, and will assign a perfect selection field for: a Vector3 will have X, Y, and Z fields, a Color will show a bar with the chosen color and open a color selection window when clicked, and enum types will display a dropdown menu with all of that type’s possible values.

We need to use these SerializedProperty objects in our drawElementCallback to allow for editing the structs in the list, but because one is an enum and the other is an int, Unity handles all of this perfectly. All we need to do is space stuff out and add informational labels:

private void OnEnable()
{
    list = new ReorderableList(
        serializedObject,
        serializedObject.FindProperty("requiredObjectTags"),
        false,
        true,
        true,
        true);

    // create SerializedProperties for other properties...

    list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
    {
        SerializedProperty property = list.serializedProperty.GetArrayElementAtIndex(index);

        float padding = 2;
        float labelwidth = 1 * (rect.width / 2) / 3;
        float fieldwidth = 2 * (rect.width / 2) / 3;
        float bothwidth = labelwidth + fieldwidth;
        float height = EditorGUIUtility.singleLineHeight;

        rect.y += padding;

        EditorGUI.LabelField(
            new Rect(rect.x, rect.y, labelwidth, height),
            "Item:");

        EditorGUI.PropertyField(
            new Rect(rect.x + labelwidth, rect.y, fieldwidth, height),
            property.FindPropertyRelative("type"),
            GUIContent.none);

        EditorGUI.LabelField(
            new Rect(rect.x + bothwidth, rect.y, labelwidth, height),
            " Min:");

        EditorGUI.PropertyField(
            new Rect(rect.x + bothwidth + labelwidth, rect.y, fieldwidth, height),
            property.FindPropertyRelative("min"),
            GUIContent.none);
    };

    list.drawHeaderCallback = (Rect rect) =>
    {
        EditorGUI.LabelField(rect, "Required Objects");
    };
}

After that, all that’s left is to connect the other public properties of ElevatorSpawner to their own serialized property objects, and apply the changes to the actual object we’re editing in OnInspectorGUI:

public override void OnInspectorGUI()
{
    serializedObject.Update();
    list.DoLayoutList();

    EditorGUILayout.LabelField("Timing", EditorStyles.boldLabel);

    EditorGUILayout.PropertyField(firstTimeWait);
    EditorGUILayout.PropertyField(spawnEvery);
    EditorGUILayout.PropertyField(doorOpenTime);

    EditorGUILayout.LabelField("Spawning", EditorStyles.boldLabel);

    EditorGUILayout.PropertyField(spawnSpeed);
    EditorGUILayout.PropertyField(spawnForce);
    EditorGUILayout.PropertyField(spawnVariance);

    serializedObject.ApplyModifiedProperties();
}

And we finally have the nice, easy-to-understand GUI for the ElevatorSpawner script shown at the top of this post. Now, any designer (or anyone else for that matter) could very easily make changes without navigating a gross default interface or working in the script.

Phew!

Advertisements