Unity editor development practice [Custom Editor] - FSM Editor

Posted by moonshaden on Fri, 14 Jan 2022 04:45:25 +0100

This paper introduces how to implement a custom editor panel for FSM finite state machine module. The detailed code of FSM is introduced in the previous article. Link address: Building FSM finite state machine in Unity Here is the final effect:

First, customize an editor panel. You need to use Attribute: CustomEditor. The parameter is passed into the type of the target class. The code is as follows:

using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {

    }
}

After the custom Editor class inherits the Editor class, override the OnInspectorGUI function to define the Inspector panel, for example, add a Label text:

using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            GUILayout.Label("Finite state machine");
        }
    }
}

To draw this panel, we need the information of the state machine list in the FSM Master, which is a private StateMachine type list. Therefore, we need to obtain it through reflection:

using System.Reflection;
using System.Collections.Generic;

using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {
        private List<StateMachine> machines;

        public override void OnInspectorGUI()
        {
            //Exit if the program is not running
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //Get state machine list through reflection
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
            }
        }
    }
}

After having the information of the state machine, list all the state machines through the Popup in the EditorGUILayout class. You need to pass in a string type array, that is, the listed content. We declare a string type array to store the names of all the state machines, and use an int type field to represent the index of the currently selected state machine:

using System.Linq;
using System.Reflection;
using System.Collections.Generic;

using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {
        private List<StateMachine> machines;
        private int currentMachineIndex;
        private string[] machinesName;

        public override void OnInspectorGUI()
        {
            //Exit if the program is not running
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //Get state machine list through reflection
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
            }

            //When the state machine name array is empty (initialization) or the number is different from the number of state machines (the state machine list changes)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //Reset current state machine index value
                currentMachineIndex = 0;
                //Retrieve the state machine name array
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("State machine:", currentMachineIndex, machinesName);
            }
        }
    }
}

Next, all state information in the state machine is obtained. The state is stored in an IState type list with the modifier protected. Therefore, it is also obtained through reflection:

using System.Linq;
using System.Reflection;
using System.Collections.Generic;

using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {
        private List<StateMachine> machines;
        private FieldInfo statesFieldInfo;
        private int currentMachineIndex;
        private string[] machinesName;

        public override void OnInspectorGUI()
        {
            //Exit if the program is not running
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //Get state machine list through reflection
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
                //Get status list fields
                statesFieldInfo = typeof(StateMachine).GetField("states", BindingFlags.Instance | BindingFlags.NonPublic);
            }

            //When the state machine name array is empty (initialization) or the number is different from the number of state machines (the state machine list changes)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //Reset current state machine index value
                currentMachineIndex = 0;
                //Retrieve the state machine name array
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("State machine:", currentMachineIndex, machinesName);
                var currentMachine = machines[currentMachineIndex];
                //Gets the state list of the current state machine
                var states = statesFieldInfo.GetValue(currentMachine) as List<IState>;
            }
        }
    }
}

After having the list information of States, the for loop traverses the list, draws the name of each state, and uses different GUIStyle to distinguish whether the state is the current state of the state machine. If not, a Button to switch to the state is provided:

using System.Linq;
using System.Reflection;
using System.Collections.Generic;

using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {
        private List<StateMachine> machines;
        private FieldInfo statesFieldInfo;
        private int currentMachineIndex;
        private string[] machinesName;

        public override void OnInspectorGUI()
        {
            //Exit if the program is not running
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //Get state machine list through reflection
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
                //Get status list fields
                statesFieldInfo = typeof(StateMachine).GetField("states", BindingFlags.Instance | BindingFlags.NonPublic);
            }

            //When the state machine name array is empty (initialization) or the number is different from the number of state machines (the state machine list changes)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //Reset current state machine index value
                currentMachineIndex = 0;
                //Retrieve the state machine name array
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("State machine:", currentMachineIndex, machinesName);
                var currentMachine = machines[currentMachineIndex];
                //Gets the state list of the current state machine
                var states = statesFieldInfo.GetValue(currentMachine) as List<IState>;

                GUILayout.BeginVertical("Box");
                for (int i = 0; i < states.Count; i++)
                {
                    var state = states[i];
                    //If the status is current, SelectionRect Style is used; otherwise, IN Title Style is used to distinguish
                    GUILayout.BeginHorizontal(currentMachine.CurrentState == state ? "SelectionRect" : "IN Title");
                    GUILayout.Label(state.Name);

                    //If the state is not the current state, a Button button to switch to the state is provided
                    if(currentMachine.CurrentState != state)
                    {
                        if (GUILayout.Button("Switch", GUILayout.Width(50f)))
                        {
                            currentMachine.Switch(state);
                        }
                    }
                    GUILayout.EndHorizontal();
                }
                GUILayout.EndVertical();
            }
        }
    }
}

In addition, we also want to add a row of menus under the state machine and draw three buttons to realize the functions of switching to the next state, switching to the previous state and switching to the empty state in the state machine. These three buttons are drawn to a row through BeginHorizontal and EndHorizontal in the GUILayout class:

private class GUIContents
{
    public static GUIContent switch2Next = new GUIContent("Next", "Switch to the next state");
    public static GUIContent switch2Last = new GUIContent("Last", "Switch to previous state");
    public static GUIContent switch2Null = new GUIContent("Null", "Switch to empty state (exit current state)");
}
GUILayout.BeginHorizontal();
//Provides a Button to switch to the previous state
if (GUILayout.Button(GUIContents.switch2Last, "ButtonLeft"))
{
    currentMachine.Switch2Last();
}
//Provides a Button to switch to the next state
if (GUILayout.Button(GUIContents.switch2Next, "ButtonMid"))
{
    currentMachine.Switch2Next();
}
//Provides a Button to switch to the empty state
if (GUILayout.Button(GUIContents.switch2Null, "ButtonRight"))
{
    currentMachine.Switch2Null();
}
GUILayout.EndHorizontal();

Final complete code:

using System.Linq;
using System.Reflection;
using System.Collections.Generic;

using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {
        private class GUIContents
        {
            public static GUIContent switch2Next = new GUIContent("Next", "Switch to the next state");
            public static GUIContent switch2Last = new GUIContent("Last", "Switch to previous state");
            public static GUIContent switch2Null = new GUIContent("Null", "Switch to empty state (exit current state)");
        }
        private List<StateMachine> machines;
        private FieldInfo statesFieldInfo;
        private int currentMachineIndex;
        private string[] machinesName;

        public override void OnInspectorGUI()
        {
            //Exit if the program is not running
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //Get state machine list through reflection
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
                //Get status list fields
                statesFieldInfo = typeof(StateMachine).GetField("states", BindingFlags.Instance | BindingFlags.NonPublic);
            }

            //When the state machine name array is empty (initialization) or the number is different from the number of state machines (the state machine list changes)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //Reset current state machine index value
                currentMachineIndex = 0;
                //Retrieve the state machine name array
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("State machine:", currentMachineIndex, machinesName);
                var currentMachine = machines[currentMachineIndex];
                //Gets the state list of the current state machine
                var states = statesFieldInfo.GetValue(currentMachine) as List<IState>;

                GUILayout.BeginHorizontal();
                //Provides a Button to switch to the previous state
                if (GUILayout.Button(GUIContents.switch2Last, "ButtonLeft"))
                {
                    currentMachine.Switch2Last();
                }
                //Provides a Button to switch to the next state
                if (GUILayout.Button(GUIContents.switch2Next, "ButtonMid"))
                {
                    currentMachine.Switch2Next();
                }
                //Provides a Button to switch to the empty state
                if (GUILayout.Button(GUIContents.switch2Null, "ButtonRight"))
                {
                    currentMachine.Switch2Null();
                }
                GUILayout.EndHorizontal();

                GUILayout.BeginVertical("Box");
                for (int i = 0; i < states.Count; i++)
                {
                    var state = states[i];
                    //If the status is current, SelectionRect Style is used; otherwise, IN Title Style is used to distinguish
                    GUILayout.BeginHorizontal(currentMachine.CurrentState == state ? "SelectionRect" : "IN Title");
                    GUILayout.Label(state.Name);

                    //If the state is not the current state, a Button button to switch to the state is provided
                    if(currentMachine.CurrentState != state)
                    {
                        if (GUILayout.Button("Switch", GUILayout.Width(50f)))
                        {
                            currentMachine.Switch(state);
                        }
                    }
                    GUILayout.EndHorizontal();
                }
                GUILayout.EndVertical();
            }
        }
    }
}

Welcome to the official account of "contemporary wild ape".

Topics: Unity