Custom Editors of Parameters Panels in Unity

  • 19th Aug 2020
  • by Pav
  • Reading Time: 13 minutes

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:

  1. What are custom editors and why do we need them
  2. Creating a custom editor
  3. 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.

Exposure of public class member fields inside Unity's editor before custom editors

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.

Custom Editors with toggle functionality

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 and string
  • 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:

Custom Editors panels with grouping tabs

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:

Custom Editors with non-primitive fields

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!

Custom editors IMGUI custom UI elements

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.

Custom Editors add UI Document and Style Sheet

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 and UnityEditor.UIElements.
  • On line 3 I’m declaring a collapsible section that will contain our first portion of fields declared in CharacterStats class. The value parameter defines the initial state of the section: true for the open and false 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 the name variable of the string 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 the CreateInspectorGUI() 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 our VisualElement 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 the resourceFilename 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!

Custom Editor final result achieved with uxml and uss files in Unity

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.