Custom Editor Windows - Part 2

Build your own Unity Editor windows - Part 2

This is the second tutorial of this topic. In the first part, I wanted to introduce you to how to build your own Unity editor windows. Part two shows you how to make custom Inspector GUIs.

Prerequisites

  • Install Unity

  • Install IDE (I use Visual Studio 2019)

  • Import Game Creator (optional, only if needed for the inspector)

  • Import and enable modules (optional, only if needed for the inspector)

Unity Inspector Windows

General Informations

According to Unity:

The Inspector window (sometimes referred to as β€œthe Inspector”) displays detailed information about the currently selected GameObject, including all attached components and their properties, and allows you to modify the functionality of GameObjects in your Scene.

In our case, we want to customize how scripts we write look in the Inspector. A good example of a customized inspector GUI is the ammo scriptable object:

This is achieved by replacing the default layout with any GUI controls you set in a different script. The script is a custom editor of the type (class) you want to display and derives from Editor.

Inspector Attributes

An inspector window can have attributes to define how it works in certain cases, like multi object editing. Here is a list of the most used attributes when working with inspector windows:

Attribute Name

Description

[CanEditMultipleObjects]

Used to make a custom editor support multi-object editing.

[CustomEditor(typeof(class))]

Tells an Editor class which run-time type it is an editor for. Necessary for a custom editor (window and inspector).

[ExecuteInEditMode]

Makes all instances of a script execute in Edit Mode (when not in Play Mode)

Serialized Properties

I have first written about serialized properties in Inventory Custom Entries. But what exactly are they? A serialized property is a property of a serialized object. Such an object is generated when you i.e. use the attribute [CustomEditor(typeof(class))]. The type (class) you name gets serialized, Unity automatically handles dirtying individual serialized fields so they will be processed by the Undo system and styled correctly for Prefab overrides when drawn in the Inspector. You need to find the variables of this class using SerializedObject.FindProperty(variableName) to access them. You also need to use serializedObject.ApplyModifiedProperties() to save the modified properties back to the object.

Your first inspector

Enough theory, let's start with a simple example. Create a new script (I call mine TutorialInspector) in the Tutorials folder and another script in a folder with the name Editor and name it TutorialInspectorWindow. I have explained the use of the Editor folder in previous tutorials, check out Unity Editor Knowledge if you don't know. Open the first script, TutorialInspector, and modify it like this:

using UnityEngine;
using GameCreator.Core; // only if you want to use GC elements
​
[ExecuteInEditMode]
public class TutorialInspector : MonoBehaviour
{
public string objName;
public Vector3 worldPosition;
public Actions action;
}

Attach the script to an empty gameobject in the scene. This should look like this:

Why not customizing how this looks? Open the second script, TutorialInspectorWindow, and modify it:

using UnityEditor;
​
[CustomEditor(typeof(TutorialInspector), true)]
[CanEditMultipleObjects]
public class TutorialInspectorEditor : Editor
{
SerializedProperty spObjName;
SerializedProperty spWorldPosition;
SerializedProperty spAction;
​
void OnEnable()
{
spObjName = serializedObject.FindProperty("objName");
spWorldPosition = serializedObject.FindProperty("worldPosition");
spAction = serializedObject.FindProperty("action");
}
​
public override void OnInspectorGUI()
{
serializedObject.Update();
​
EditorGUILayout.PropertyField(spObjName);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spWorldPosition);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spAction);
​
serializedObject.ApplyModifiedProperties();
}
}

For displaying serialized properties in a custom editor window, you need to use the EditorGUILayout.PropertyField() method. This script we just wrote adds a bit of space between each of the variables:

But why don't we customize the look of the TutorialInspector script directly in it? Because we would have to use the #if UNITY_EDITOR define directive each time we want to use code that only works in the Unity Editor. It is much easier to replace the look of the script by using the CustomEditor way.

By the way, have you noticed that the field, where the script is shown in the first image, disappeared in the second one? That's because we don't have a field for this in our replaced layout πŸ™‚ .

More advanced examples

Custom names

You can replace the name of the variable and set your own text for it. Change the TutorialInspector script:

public string objName;
public Vector3 worldPosition;
public Actions action;
public bool boolean = true; // new variable

Change the TutorialInspectorEditor script:

SerializedProperty spObjName;
SerializedProperty spWorldPosition;
SerializedProperty spAction;
SerializedProperty spBoolean;
​
​
private static readonly GUIContent objNameDescription = new GUIContent("Object Name");
private static readonly GUIContent booleanDescription = new GUIContent("This is a boolean");
​
void OnEnable()
{
spObjName = serializedObject.FindProperty("objName");
spWorldPosition = serializedObject.FindProperty("worldPosition");
spAction = serializedObject.FindProperty("action");
spBoolean = serializedObject.FindProperty("boolean");
}
​
public override void OnInspectorGUI()
{
serializedObject.Update();
​
EditorGUILayout.PropertyField(spObjName, objNameDescription); // added custom name
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spWorldPosition);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spAction);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spBoolean, booleanDescription); // added custom description
​
serializedObject.ApplyModifiedProperties();
}

Fade group without persistence on InspectorChange

TutorialInspectorEditor:

AnimBool fadeGroup;
​
SerializedProperty spObjName;
SerializedProperty spWorldPosition;
SerializedProperty spAction;
SerializedProperty spBoolean;
​
​
private static readonly GUIContent objNameDescription = new GUIContent("Object Name");
private static readonly GUIContent booleanDescription = new GUIContent("This is a boolean");
​
void OnEnable()
{
spObjName = serializedObject.FindProperty("objName");
spWorldPosition = serializedObject.FindProperty("worldPosition");
spAction = serializedObject.FindProperty("action");
spBoolean = serializedObject.FindProperty("boolean");
fadeGroup = new AnimBool(true);
fadeGroup.valueChanged.AddListener(Repaint);
}
​
public override void OnInspectorGUI()
{
serializedObject.Update();
​
if (GUILayout.Button("Hide/Show extra fields"))
{
if (fadeGroup.target)
{
fadeGroup.target = false;
}
else
{
fadeGroup.target = true;
}
}
​
if (EditorGUILayout.BeginFadeGroup(fadeGroup.faded))
{
EditorGUILayout.PropertyField(spObjName, objNameDescription);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spWorldPosition);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spAction);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spBoolean, booleanDescription);
}
​
EditorGUILayout.EndFadeGroup();
​
serializedObject.ApplyModifiedProperties();
}

Fade group with persistence on InspectorChange

TutorialInspector:

public string objName;
public Vector3 worldPosition;
public Actions action;
public bool boolean = true;
​
public bool isExpanded;

TutorialInspectorEditor:

AnimBool fadeGroup;
​
SerializedProperty spObjName;
SerializedProperty spWorldPosition;
SerializedProperty spAction;
SerializedProperty spBoolean;
SerializedProperty spIsExpanded;
​
​
private static readonly GUIContent objNameDescription = new GUIContent("Object Name");
private static readonly GUIContent booleanDescription = new GUIContent("This is a boolean");
​
void OnEnable()
{
spObjName = serializedObject.FindProperty("objName");
spWorldPosition = serializedObject.FindProperty("worldPosition");
spAction = serializedObject.FindProperty("action");
spBoolean = serializedObject.FindProperty("boolean");
spIsExpanded = serializedObject.FindProperty("isExpanded");
fadeGroup = new AnimBool(spIsExpanded.boolValue);
fadeGroup.valueChanged.AddListener(Repaint);
}
​
public override void OnInspectorGUI()
{
serializedObject.Update();
​
if (GUILayout.Button("Hide/Show extra fields"))
{
if (spIsExpanded.boolValue == false)
{
fadeGroup.target = true;
spIsExpanded.boolValue = true;
}
else
{
fadeGroup.target = false;
spIsExpanded.boolValue = false;
}
}
​
if (EditorGUILayout.BeginFadeGroup(fadeGroup.faded))
{
EditorGUILayout.PropertyField(spObjName, objNameDescription);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spWorldPosition);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spAction);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spBoolean, booleanDescription);
}
​
EditorGUILayout.EndFadeGroup();
​
serializedObject.ApplyModifiedProperties();
}

Game Creator Section

TutorialInspectorEditor:

using GameCreator.Core;
using UnityEditor;
using UnityEngine;
using static GameCreator.Characters.CharacterEditor;
​
[CustomEditor(typeof(TutorialInspector), true)]
[CanEditMultipleObjects]
public class TutorialInspectorEditor : Editor
{
​
SerializedProperty spObjName;
SerializedProperty spWorldPosition;
SerializedProperty spAction;
SerializedProperty spBoolean;
​
private Section section;
​
​
private static readonly GUIContent objNameDescription = new GUIContent("Object Name");
private static readonly GUIContent booleanDescription = new GUIContent("This is a boolean");
​
void OnEnable()
{
section = new Section("General", null, this.Repaint);
​
spObjName = serializedObject.FindProperty("objName");
spWorldPosition = serializedObject.FindProperty("worldPosition");
spAction = serializedObject.FindProperty("action");
spBoolean = serializedObject.FindProperty("boolean");
}
​
public override void OnInspectorGUI()
{
serializedObject.Update();
​
section.PaintSection();
​
using (var group = new EditorGUILayout.FadeGroupScope(this.section.state.faded))
{
if (group.visible)
{
EditorGUILayout.BeginVertical(CoreGUIStyles.GetBoxExpanded());
​
EditorGUILayout.PropertyField(spObjName);
​
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spWorldPosition);
EditorGUILayout.PropertyField(spAction);
​
EditorGUILayout.Space();
EditorGUILayout.PropertyField(spBoolean);
​
EditorGUILayout.EndVertical();
}
}
​
serializedObject.ApplyModifiedProperties();
}
}

If the tutorial was useful to you, consider supporting me on Patreon: https://www.patreon.com/MiTschmrStudios​