Introduction
During your development sometimes you need to have a custom way of editing your parameters in Unity inspector panel. Perhaps you wish to toggle between the visibility of certain fields depending on a boolean value. Maybe you require to introduce tabs to maximize the precious panel space while eliminating the need for prolonged scrolling. It also may be the case that you are looking for providing users a way of saving the state of your game with custom editor. In this tutorial I’ll show a couple of examples of custom editors inside Unity.
Table of contents:
- What are custom editors and why do we need them
- Creating a custom editor
- Custom editors look of the panel and parameters fields
1. What are custom editors and why do we need them
Unity’s developers introduced custom editors to provide game creators with a way of expanding the graphical user interface (GUI) according to their needs. At the core of the engine’s editor lies an array of functions responsible for drawing all the panels, fields and everything related to what we see on screen. We as game programmers sometimes need to have a way of performing certain configurations directly in the editor in a more efficient and often non-standard way.
Let’s suppose that we have a script with lot’s of fields defining the stats of our character. Usually in such cases we simply list all our fields as class member variables so that we can preview and edit them inside the editor. However, it is also the case that the input fields would take a lot of space in the Inspector panel. Furthermore, the list would not be organized or styled in any way. Let’s have a look at the following example script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharacterStats : MonoBehaviour
{
new public string name;
public string lastname;
public int age;
public int hp;
public int mp;
public int lvl;
public float str;
public float mstr;
public float def;
public float dex;
public float vit;
public float luck;
public float fire_def;
public float water_def;
public float wind_def;
public float thunder_def;
public float fire_mstr;
public float water_mstr;
public float wind_mstr;
public float thunder_mstr;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
When we attach it to a GameObject inside our game we will quickly be presented with this enormous panel.
Yuck! We have all the stats listed one after another and there’s really no clear distinction between them. In addition there’s really no clear labelling of the fields so we may end up confused what each of them even refers to! Let’s now have a look what we can do about that!
2. Creating a custom editor
First, let’s start by creating the ‘Editor’ folder under our project’s ‘Assets’. This is a special folder where Unity will look for all custom editor scripts it needs to apply during the development of a project. Inside, I’m going to create a new script called CharacterStats_CustomEditor
, which is going to basically extend the standard UI rendering of CharacterStats
class in editor.
When it comes to engine’s editor the most important function is OnInspectorGUI as it is responsible for drawing all elements of user UI. We can access it from UnityEditor namespace so it is essential that we import it prior writing any code modifying default drawing behaviour.
using UnityEditor;
After that I’ll make sure that the class inherits from Editor instead of MonoBehaviour since we are interested in influencing the look and feel of the panel. In addition, we have to add a special attribute just above the class declaration in order to inform Unity what type of script we want to modify. Here’s the starting point “bare” class with all important lines highlighted:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(CharacterStats))]
public class CharacterStats_CustomEditor : Editor
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Please note that should the CharacterStats
be ever inherited from, the child scripts will not apply any GUI modifications we make here. We can easily change that by setting the second argument of the attribute to true
.
[CustomEditor(typeof(CharacterStats), true)]
We are now ready to start modifying our custom editor panel!
Custom vs default drawing behaviour
As I already mentioned, the main function that deals with drawing the GUI elements inside the editor is called OnInspectorGUI(
). In order to modify any standard behaviour in terms of the editor we have to use its might. Let’s therefore remove the Start()
and Update()
functions and override OnInspectorGUI()
function inside our class.
public class CharacterStats_CustomEditor : Editor
{
public override void OnInspectorGUI()
{
}
}
When you now inspect the script in the editor you will notice that all the fields are gone! The reason for this is that we have replaced the standard drawing of these elements with our own custom implementation. That said, we can easily restore their default drawing by calling the method from parent (base) class.
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
}
However, since we want to create a custom panel, we will have to draw everything ourselves using methods from EditorGUILayout. I’ll comment out the default drawing for now.
Show / hide fields depending on boolean value
After that, I’ll add a boolean that will control the visibility of 3 first fields and add a reference to CharacterStats
in order to access them. All data is stored in a special SerializedObject between all Play and Editor sessions. In practice, in order to permanently save and preserve the data in the fields every time we close / open the editor we have to do it via this object. Luckily, Unity provides a reference to it inside our script and all we have to do is to access it.
public class CharacterStats_CustomEditor : Editor
{
CharacterStats characterStats;
public bool personalDetails;
public override void OnInspectorGUI()
{
//base.OnInspectorGUI();
characterStats = (CharacterStats) target;
EditorGUILayout.Separator();
personalDetails = EditorGUILayout.Toggle("Personal Details", personalDetails);
if(personalDetails)
{
base.serializedObject.FindProperty("name").stringValue = EditorGUILayout.TextField("Name :", characterStats.name);
base.serializedObject.FindProperty("lastname").stringValue = EditorGUILayout.TextField("Last Name :", characterStats.lastname);
base.serializedObject.FindProperty("age").intValue = EditorGUILayout.IntField("Age :", characterStats.age);
}
EditorGUILayout.Separator();
}
}
On line 10 I’m accessing a special variable target that references the object we are currently inspecting in the editor. In our case it’s CharacterStats
script so I’m making a cast to it.
On lines 16-21 I’m checking the value of personalDetails
boolean and display the values taken from SerializedObject
depending on its state. The string argument provided inside its FindProperty()
member method corresponds to the name of the variable declared in CharacterStats
class (e.g. FindProperty("name")
for name
). After that I access the correct value by specifying the type of the variable (e.g. stringValue
for name
field). Next, I assign the drawing of the field using the correct method from EditorGUILayout
(e.g. EditorGUILayout.TextField("Name :", characterStats.name)
for the name
field). By doing so I can now toggle between the visibility of these fields with a simple tick of a box!
Let’s repeat this process for all the fields until we end up with specialized, toggable sections!
[CustomEditor(typeof(CharacterStats))]
public class CharacterStats_CustomEditor : Editor
{
CharacterStats characterStats;
public bool personalDetails;
public bool basicStats;
public bool battleStats;
public bool elementalStats;
public override void OnInspectorGUI()
{
//base.OnInspectorGUI();
characterStats = (CharacterStats) target;
EditorGUILayout.Separator();
personalDetails = EditorGUILayout.Toggle("Personal Details", personalDetails);
if(personalDetails)
{
base.serializedObject.FindProperty("name").stringValue = EditorGUILayout.TextField("Name :", characterStats.name);
base.serializedObject.FindProperty("lastname").stringValue = EditorGUILayout.TextField("Last Name :", characterStats.lastname);
base.serializedObject.FindProperty("age").intValue = EditorGUILayout.IntField("Age :", characterStats.age);
}
EditorGUILayout.Separator();
basicStats = EditorGUILayout.Toggle("Basic Details", basicStats);
if (basicStats)
{
base.serializedObject.FindProperty("hp").intValue = EditorGUILayout.IntField("HP :", characterStats.hp);
base.serializedObject.FindProperty("mp").intValue = EditorGUILayout.IntField("MP :", characterStats.mp);
base.serializedObject.FindProperty("lvl").intValue = EditorGUILayout.IntField("Level :", characterStats.lvl);
}
EditorGUILayout.Separator();
battleStats = EditorGUILayout.Toggle("Battle Details", battleStats);
if (battleStats)
{
base.serializedObject.FindProperty("str").floatValue = EditorGUILayout.FloatField("Strength :", characterStats.str);
base.serializedObject.FindProperty("mstr").floatValue = EditorGUILayout.FloatField("Magic Strength :", characterStats.mstr);
base.serializedObject.FindProperty("def").floatValue = EditorGUILayout.FloatField("Defence :", characterStats.def);
base.serializedObject.FindProperty("dex").floatValue = EditorGUILayout.FloatField("Dexterity :", characterStats.dex);
base.serializedObject.FindProperty("vit").floatValue = EditorGUILayout.FloatField("Vitality :", characterStats.vit);
base.serializedObject.FindProperty("luck").floatValue = EditorGUILayout.FloatField("Luck :", characterStats.luck);
}
EditorGUILayout.Separator();
elementalStats = EditorGUILayout.Toggle("Elemental Details", elementalStats);
if (elementalStats)
{
base.serializedObject.FindProperty("fire_def").floatValue = EditorGUILayout.FloatField("Fire Defence :", characterStats.fire_def);
base.serializedObject.FindProperty("water_def").floatValue = EditorGUILayout.FloatField("Water Defence :", characterStats.water_def);
base.serializedObject.FindProperty("wind_def").floatValue = EditorGUILayout.FloatField("Wind Defence :", characterStats.wind_def);
base.serializedObject.FindProperty("thunder_def").floatValue = EditorGUILayout.FloatField("Thunder Defence :", characterStats.thunder_def);
base.serializedObject.FindProperty("fire_mstr").floatValue = EditorGUILayout.FloatField("Fire Magic Strength :", characterStats.fire_mstr);
base.serializedObject.FindProperty("water_mstr").floatValue = EditorGUILayout.FloatField("Water Magic Strength :", characterStats.water_mstr);
base.serializedObject.FindProperty("wind_mstr").floatValue = EditorGUILayout.FloatField("Wind Magic Strength :", characterStats.wind_mstr);
base.serializedObject.FindProperty("thunder_mstr").floatValue = EditorGUILayout.FloatField("Thunder Magic Strength :", characterStats.thunder_mstr);
}
}
}
The output of this script should now give us much cleaner results in the editor.
Saving the fields contents
The fields are now nicely labelled and grouped. However, when you try to edit any of them you will notice that the changes are not being saved! That’s because we still have to tell Unity to save the contents stored in the SerializedObject
. To do this we simply are going to add one line of code at the very end of OnInspectorGUI()
function:
public override void OnInspectorGUI()
{
...
base.serializedObject.ApplyModifiedProperties();
}
However, this will take care of only the fields that are serializable and mostly primitive built-in types. There is only a limited types that can be serialized:
- non-abstract classes with
[Serializable]
attribute - structs with
[Serializable]
attribute - references to objects that derive from
UntiyEngine.Object
- the built-in data types such as
int
,float
,double
,bool
andstring
array
of a fieldtype we can serialize (see point above)List<T>
of a fieldtype we can serialize (see point above)
If you want to save fields of object types such as GameObject
or more custom ones, you have to place this at the end of your OnInspectorGUI()
method:
public override void OnInspectorGUI()
{
...
base.serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(menu);
EditorSceneManager.MarkSceneDirty(menu.gameObject.scene);
}
}
Please note that in order to use EditorSceneManager
you also have to import its namespace in the beginning.
using UnityEditor.SceneManagement;
Grouping parameters with tabs
We now have a nice panel with sections consisting of specialized parameters. However, the ‘Elemental Details’ section could be further enhanced since it contains two types of elemental values: one for describing the elemental strengths and one for describing elemental defences of the character. I will add an enum type variable that I’ll then use to create a small tabs system to quickly switch between the two.
[CustomEditor(typeof(CharacterStats))]
public class CharacterStats_CustomEditor : Editor
{
...
public bool elementalStats;
public enum ElementalType { ElementalStrengths, ElementalDefences }
public ElementalType elementalStatsType;
public override void OnInspectorGUI()
{
...
elementalStats = EditorGUILayout.Toggle("Elemental Details", elementalStats);
if (elementalStats)
{
EditorGUILayout.Separator();
elementalStatsType = (ElementalType) EditorGUILayout.EnumPopup("Show:", elementalStatsType);
EditorGUILayout.Separator();
if(elementalStatsType == ElementalType.ElementalStrengths)
{
base.serializedObject.FindProperty("fire_mstr").floatValue = EditorGUILayout.FloatField("Fire Magic Strength :", characterStats.fire_mstr);
base.serializedObject.FindProperty("water_mstr").floatValue = EditorGUILayout.FloatField("Water Magic Strength :", characterStats.water_mstr);
base.serializedObject.FindProperty("wind_mstr").floatValue = EditorGUILayout.FloatField("Wind Magic Strength :", characterStats.wind_mstr);
base.serializedObject.FindProperty("thunder_mstr").floatValue = EditorGUILayout.FloatField("Thunder Magic Strength :", characterStats.thunder_mstr);
}
else if(elementalStatsType == ElementalType.ElementalDefences)
{
base.serializedObject.FindProperty("fire_def").floatValue = EditorGUILayout.FloatField("Fire Defence :", characterStats.fire_def);
base.serializedObject.FindProperty("water_def").floatValue = EditorGUILayout.FloatField("Water Defence :", characterStats.water_def);
base.serializedObject.FindProperty("wind_def").floatValue = EditorGUILayout.FloatField("Wind Defence :", characterStats.wind_def);
base.serializedObject.FindProperty("thunder_def").floatValue = EditorGUILayout.FloatField("Thunder Defence :", characterStats.thunder_def);
}
}
base.serializedObject.ApplyModifiedProperties();
}
}
The parameters shall now be displayed inside their corresponding tabs like so:
Saving user defined fields of specific object types
Up till this moment I have shown how to access fields of primitive / built-in type and save them with custom editors. However, what if we need to include a field of non-primitive (non-built-in) type, such as commonly used GameObject
? Let’s add a new field to CharacterStats
class and see how we can edit and save it using our custom editor.
public class CharacterStats : MonoBehaviour
{
public GameObject player;
...
}
Back in our CharacterStats_CustomEditor
script I would like to display this field in inspector window when “Personal Details” section is on. To achieve this I’ll use a ObjectField method inside EditorGUILayout
built-in class and make a cast to the type of my field (i.e. GameObject
).
[CustomEditor(typeof(CharacterStats))]
public class CharacterStats_CustomEditor : Editor
{
...
public override void OnInspectorGUI()
{
characterStats = (CharacterStats) target;
...
if(personalDetails)
{
....
characterStats.player = (GameObject) EditorGUILayout.ObjectField("Player :", characterStats.player, typeof(GameObject), true);
}
...
}
}
This method can be used for any kind of non-primitive type field such as ScriptableObject or other custom scripts. After adding the above line in our code the result is the following:
3. Custom editors look of the panel and parameters fields
At this point we can go a step further and add even more custom look of our UI elements. In Unity we can do it in two different ways:
- by using IMGUI to simply create our custom UI elements however we want by adding methods defining them
- by using UIElements to build our custom UI panel with specially formatted files defining the layout (uxml) and style (uss). The structure of these files closely resemble the xml and css so if you have an experience with web development you will feel right at home here.
Please note that there exists a rule that prevents us to use both systems simultaneously:
- If we create a custom inspector using UIElements then we have to override the Editor.CreateInspectorGUI on the Editor class.
- If we create a custom inspector using IMGUI then we have to override the Editor.OnInspectorGUI on the Editor class.
For example, if we use UIElements and have Editor.CreateInspectorGUI overwritten, any existing IMGUI implementation using Editor.OnInspectorGUI on the same Editor will be ignored.
The IMGUI way
I’ll start by showing how to add UI element using the IMGUI as it only requires us to write a custom method and modifying the existing code. To keep things simple, I’ll just focus on adding a progress bar to the HP field to better visualise its value. First, let’s write a function that will be responsible for drawing it.
[CustomEditor(typeof(CharacterStats))]
public class CharacterStats_CustomEditor : Editor
{
...
public override void OnInspectorGUI()
{
...
}
void ProgressBar(float value, string label)
{
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, value, label);
EditorGUILayout.Space();
}
}
After that I’ll replace the code responsible for accessing and drawing the field of hp
field. I’ll make it so that now instead of inputting the value to a text box the user will set it with a slider.
public override void OnInspectorGUI()
{
...
if(personalDetails)
{
EditorGUILayout.IntSlider(base.serializedObject.FindProperty("hp"), 0, 100, new GUIContent("HP :"));
ProgressBar(base.serializedObject.FindProperty("hp").intValue / 100.0f, "" + base.serializedObject.FindProperty("hp").intValue);
...
}
...
}
If you have done everything correctly you should now see the progress bar indicating the hp of our character!
The UIElements way
This method requires more work but it also provides most flexibility when it comes to organizing your UI elements in inspector panel. As I mentioned before, while using this method you have to create two additional files: one defining the layout of the panel and one for styling it. Web developers will be familiar with this process as it closely resembles designing and coding a website.
Right-click inside your Assets/Editor folder and create two files: ‘UI Document’ and ‘Style Sheet’. Rename them to indicate their role. I’ll rename the first to CharacterStats_Template.uxml
and the second to CharacterStats_Style.uss
.
Open up your template file. First we are going to define the structure of the first section for our variables.
<UXML xmlns="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements">
<Foldout class="foldout-bar" text="Personal Details" value="false">
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Name"/>
<VisualElement class="input-container">
<TextField class="player-textfield" max-length="15" name="name" binding-path="name" title="Name"/>
</VisualElement>
</VisualElement>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Last Name"/>
<VisualElement class="input-container">
<TextField class="player-textfield" max-length="15" name="name" binding-path="lastname" title="Last Name"/>
</VisualElement>
</VisualElement>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Age"/>
<VisualElement class="input-container">
<editor:IntegerField class="player-textfield" max-length="15" name="age" binding-path="age" title="Age" />
</VisualElement>
</VisualElement>
</VisualElement>
</Foldout>
</UXML>
Allow me to explain the most crucial bits from the above listing:
- On line 1 is the opening UXML tag specifying the namespaces I’d like to use. In this case
UnityEngine.UIElements
andUnityEditor.UIElements
. - On line 3 I’m declaring a collapsible section that will contain our first portion of fields declared in
CharacterStats
class. Thevalue
parameter defines the initial state of the section:true
for the open andfalse
for the closed. - On line 5, I’m declaring a new node that will be added to the root. The
VisualElement
is the base class that all the layout elements, styling and events derive from. - On line 7 I’m placing a label of the field I want to edit.
- On line 9 I’m creating a text field for the user to edit the parameter. In here, the most important parameter is
binding-path
as it links the variable to the field. If you do not provide the variable name here, the value will not be stored. In addition, the tag needs to correspond to the type of the field (e.g. if we want to edit thename
variable of thestring
type, then<TextField>
tag should be used).
I’ve repeated the process for all first three fields. Now let’s open the CharacterStats_Style file and declare the style for our elements. The styling classes are linked to their UI elements via class
parameter.
.foldout-bar {
margin-left: 12px;
margin-top: 5px;
margin-bottom: 5px;
}
.player-textfield{
width: 50px;
flex-direction: row;
justify-content: space-between;
flex:3;
}
.slider-row {
flex-direction: row;
justify-content: space-between;
margin-top: 4px;
}
.input-container {
flex-direction: row;
flex-grow: .6;
margin-right: 4px;
}
.player-property {
margin-bottom: 4px;
}
.player-property-label {
flex:1;
margin-left: 12px;
color:#f00;
}
In order to use the newly created layout and style we have to modify our CharacterStats_CustomEditor
.
- Add a variable specifying the location of both files. In my case I’ll use the first segment of the filenames.
const string resourceFilename = "Assets/Editor/CharacterStats";
- Comment out the contents of the
OnInspectorGUI()
method block and override theCreateInspectorGUI()
function.
[CustomEditor(typeof(CharacterStats))]
public class CharacterStats_CustomEditor : Editor
{
...
const string resourceFilename = "Assets/Editor/CharacterStats";
public override VisualElement CreateInspectorGUI()
{
}
/*public override void OnInspectorGUI()
{
...
}*/
}
- Inside the
CreateInspectorGUI()
function we have load both files, link them together and return as ourVisualElement
of the custom inspector editor. To do this I’ll use the asset loading utility function provided by Unity (i.e. AssetDatabase.LoadAssetAtPath<T>). While providing the parameter with the location of the file, I’ll take the advantage of C# string interpolation to use the first part of the location path I declared earlier with theresourceFilename
variable.
public override VisualElement CreateInspectorGUI()
{
VisualElement customInspector = new VisualElement();
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>($"{resourceFilename}_Template.uxml");
visualTree.CloneTree(customInspector);
StyleSheet stylesheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"{resourceFilename}_Style.uss");
customInspector.styleSheets.Add(stylesheet);
return customInspector;
}
The result you should see in your inspector panel now is this:
The “Personal Details” section can be collapsed, the color of the labels can be manipulated inside the uss file and the overall layout can be adjusted inside your uxml! This greatly decouples the design aspects of your panels from their logic and controls. Of course, Unity provides the whole spectrum of elements you can use to enhance your custom editor.
Repeat the process for the rest of the sections to neatly organize all your fields. In my case I also used two new tags to place sliders (<SliderInt>
) and progress bars (<editor:ProgressBar>
)for my numerical fields. In addition, I’ve added few more classes to my uss file. That way I was able to visualize all the UI elements better in my custom editor!
CharacterStats_Template.uxml
<UXML xmlns="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements">
<Foldout class="foldout-bar" text="Personal Details" value="false">
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Name"/>
<VisualElement class="input-container">
<TextField class="player-textfield" max-length="15" name="name" binding-path="name" title="Name"/>
</VisualElement>
</VisualElement>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Last Name"/>
<VisualElement class="input-container">
<TextField class="player-textfield" max-length="15" name="name" binding-path="lastname" title="Last Name"/>
</VisualElement>
</VisualElement>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Age"/>
<VisualElement class="input-container">
<editor:IntegerField class="player-textfield" max-length="15" name="age" binding-path="age" title="Age" />
</VisualElement>
</VisualElement>
</VisualElement>
</Foldout>
<Foldout class="foldout-bar" text="Basic Details" value="false">
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="HP"/>
<VisualElement class="input-container">
<SliderInt class="player-slider" name="hp-slider" high-value="100" direction="Horizontal" binding-path="hp"/>
<editor:IntegerField class="player-int-field" binding-path="hp"/>
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="hp-progress" binding-path="hp" title="HP"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="MP"/>
<VisualElement class="input-container">
<SliderInt class="player-slider" name="mp-slider" high-value="100" direction="Horizontal" binding-path="mp"/>
<editor:IntegerField class="player-int-field" binding-path="mp"/>
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="mp-progress" binding-path="mp" title="MP"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Level"/>
<VisualElement class="input-container">
<SliderInt class="player-slider" name="lvl-slider" high-value="100" direction="Horizontal" binding-path="lvl"/>
<editor:IntegerField class="player-int-field" binding-path="lvl"/>
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="mp-progress" binding-path="lvl" title="Level"/>
</VisualElement>
</Foldout>
<Foldout class="foldout-bar" text="Battle Details" value="false">
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Strength"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="str" binding-path="str" title="Strength" />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="str-progress" binding-path="str" title="Strength"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Magic Strength"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="str" binding-path="mstr" title="Magic Strength" />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="str-progress" binding-path="mstr" title="Magic Strength"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Defence"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="def" binding-path="def" title="Defence" />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="def-progress" binding-path="def" title="Defence"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Dexterity"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="dex" binding-path="dex" title="Dexterity" />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="dex-progress" binding-path="dex" title="Dexterity"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Vitality"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="vit" binding-path="vit" title="Vitality" />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="vit-progress" binding-path="vit" title="Vitality"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Luck"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="luck" binding-path="luck" title="Luck" />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="luck-progress" binding-path="luck" title="Luck"/>
</VisualElement>
</Foldout>
<Foldout class="foldout-bar" text="Elemental Details" value="false">
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Fire Magic Strength"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="fire_mstr" binding-path="fire_mstr" title="Fire Magic Strength " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="fire_mstr-progress" binding-path="fire_mstr" title="Fire Magic Strength "/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Water Magic Strength"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="water_mstr" binding-path="water_mstr" title="Water Magic Strength " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="water_mstr-progress" binding-path="water_mstr" title="Water Magic Strength "/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Wind Magic Strength"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="wind_mstr" binding-path="wind_mstr" title="Wind Magic Strength " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="wind_mstr-progress" binding-path="wind_mstr" title="Wind Magic Strength "/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Thunder Magic Strength"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="thunder_mstr" binding-path="thunder_mstr" title="Thunder Magic Strength " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="thunder_mstr-progress" binding-path="thunder_mstr" title="Thunder Magic Strength "/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Fire Defence"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="fire_def" binding-path="fire_def" title="Fire Defence " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="fire_def-progress" binding-path="fire_def" title="Fire Defence "/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Water Defence"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="water_def" binding-path="water_def" title="Water Defence " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="water_def-progress" binding-path="water_def" title="Water Defence "/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Wind Defence"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="wind_def" binding-path="wind_def" title="Wind Defence " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="wind_def-progress" binding-path="wind_def" title="Wind Defence "/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Thunder Defence"/>
<VisualElement class="input-container">
<editor:FloatField class="player-textfield" max-length="2" name="thunder_def" binding-path="thunder_def" title="Thunder Defence " />
</VisualElement>
</VisualElement>
<editor:ProgressBar class="player-property-progress-bar" name="thunder_def-progress" binding-path="thunder_def" title="Thunder Defence "/>
</VisualElement>
</Foldout>
</UXML>
CharacterStats_Style.uss
.foldout-bar {
margin-left: 12px;
margin-top: 5px;
margin-bottom: 5px;
}
.player-textfield{
width: 50px;
flex-direction: row;
justify-content: space-between;
flex:3;
}
.slider-row {
flex-direction: row;
justify-content: space-between;
margin-top: 4px;
}
.input-container {
flex-direction: row;
flex-grow: .6;
margin-right: 4px;
}
.player-property {
margin-bottom: 4px;
}
.player-property-label {
flex:1;
margin-left: 12px;
color:#f00;
}
.player-slider {
flex:3;
margin-right: 4px;
}
.player-property-progress-bar {
margin-left: 16px;
margin-right: 4px;
}
.player-int-field {
min-width: 48px;
}
Once you make the final adjustment you should now see a fully-fledged custom editor in your inspector panel!
Conclusion
In this tutorial I showed how developers can enhance the functionality and look of their custom inspector editors panels. I’ve created a special class inside the Editor folder that derived from a base class of the same name. Next I’ve created a custom behaviour by overriding the contents of public OnInspectorGUI()
method. I did so with the help of editor-specific APIs provided by Unity called EditorGUILayout. After that I showed few examples of its utilization in real life cases. I finished by showing two different philosophies of creating custom editors in Unity. One uses a standard coding practice to lay down the foundation and functionality of the inspector panel. The other one follows the principles similar to those found in web development practices. In the latter, I’ve overridden the CreateInspectorGUI
function instead of OnInspectorGUI
as Unity only allows to use one method of creating custom editors at a time.