Unity 编辑器开发实战【Custom Editor】- FSM Editor

2022-08-29 16:27:59 浏览数 (1)

本文介绍如何为FSM有限状态机模块实现一个自定义编辑器面板,FSM的代码在如下链接中有详细介绍:

https://blog.csdn.net/qq_42139931/article/details/122410779?spm=1001.2014.3001.5501

下面是最终效果:

首先,自定义一个编辑器面板,需要用到Attribute:CustomEditor,参数传入目标类的类型,代码如下:

代码语言:javascript复制
using UnityEngine;
using UnityEditor;

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

    }
}

自定义编辑器类继承Editor类后,重写OnInspectorGUI函数来自定义Inspector面板,例如添加一个Label文本:

代码语言:javascript复制
using UnityEngine;
using UnityEditor;

namespace SK.Framework
{
    [CustomEditor(typeof(FSMMaster))]
    public class FSMEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            GUILayout.Label("有限状态机");
        }
    }
}

绘制该面板我们需要FSM Master中的状态机列表的信息,是一个私有的StateMachine类型的列表,因此需要通过反射去获取:

代码语言:javascript复制
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()
        {
            //程序未在运行状态则退出
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //通过反射获取状态机列表
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
            }
        }
    }
}

有了状态机的信息后,通过EditorGUILayout类中的Popup去列举所有的状态机,其中需要传入一个string类型数组,即列举的内容,我们声明一个string类型数组来存储所有状态机的名称,使用一个int类型字段来表示当前选中的状态机的索引:

代码语言:javascript复制
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()
        {
            //程序未在运行状态则退出
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //通过反射获取状态机列表
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
            }

            //当状态机名称数组为空(初始化) 或数量与状态机数量不等时(状态机列表发生变化)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //重置当前状态机索引数值
                currentMachineIndex = 0;
                //重新获取状态机名称数组
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("状态机:", currentMachineIndex, machinesName);
            }
        }
    }
}

接下来获取状态机中的所有状态信息, 状态使用一个IState类型的列表存储,修饰符为protected,因此也通过反射去获取:

代码语言:javascript复制
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()
        {
            //程序未在运行状态则退出
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //通过反射获取状态机列表
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
                //获取状态列表字段
                statesFieldInfo = typeof(StateMachine).GetField("states", BindingFlags.Instance | BindingFlags.NonPublic);
            }

            //当状态机名称数组为空(初始化) 或数量与状态机数量不等时(状态机列表发生变化)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //重置当前状态机索引数值
                currentMachineIndex = 0;
                //重新获取状态机名称数组
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("状态机:", currentMachineIndex, machinesName);
                var currentMachine = machines[currentMachineIndex];
                //获取当前状态机的状态列表
                var states = statesFieldInfo.GetValue(currentMachine) as List<IState>;
            }
        }
    }
}

有了状态的列表信息后,for循环遍历列表,绘制每一个状态的名称,使用不同的GUIStyle来区分该状态是否为状态机的当前状态,如果不是,则提供一个切换到该状态的Button按钮:

代码语言:javascript复制
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()
        {
            //程序未在运行状态则退出
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //通过反射获取状态机列表
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
                //获取状态列表字段
                statesFieldInfo = typeof(StateMachine).GetField("states", BindingFlags.Instance | BindingFlags.NonPublic);
            }

            //当状态机名称数组为空(初始化) 或数量与状态机数量不等时(状态机列表发生变化)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //重置当前状态机索引数值
                currentMachineIndex = 0;
                //重新获取状态机名称数组
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("状态机:", currentMachineIndex, machinesName);
                var currentMachine = machines[currentMachineIndex];
                //获取当前状态机的状态列表
                var states = statesFieldInfo.GetValue(currentMachine) as List<IState>;

                GUILayout.BeginVertical("Box");
                for (int i = 0; i < states.Count; i  )
                {
                    var state = states[i];
                    //如果状态为当前状态 使用SelectionRect Style 否则使用IN Title Style进行区分
                    GUILayout.BeginHorizontal(currentMachine.CurrentState == state ? "SelectionRect" : "IN Title");
                    GUILayout.Label(state.Name);

                    //如果状态不是当前状态 提供切换到该状态的Button按钮
                    if(currentMachine.CurrentState != state)
                    {
                        if (GUILayout.Button("Switch", GUILayout.Width(50f)))
                        {
                            currentMachine.Switch(state);
                        }
                    }
                    GUILayout.EndHorizontal();
                }
                GUILayout.EndVertical();
            }
        }
    }
}

除此之外,我们还希望在状态机下面添加一排菜单,绘制三个按钮,分别实现状态机中的切换到下一状态、切换到上一状态、切换到空状态的功能,通过GUILayout类中的BeginHorizontal和EndHorizontal将这三个按钮绘制到一排:

代码语言:javascript复制
private class GUIContents
{
    public static GUIContent switch2Next = new GUIContent("Next", "切换到下一状态");
    public static GUIContent switch2Last = new GUIContent("Last", "切换到上一状态");
    public static GUIContent switch2Null = new GUIContent("Null", "切换到空状态 (退出当前状态)");
}
代码语言:javascript复制
GUILayout.BeginHorizontal();
//提供切换到上一状态的Button按钮
if (GUILayout.Button(GUIContents.switch2Last, "ButtonLeft"))
{
    currentMachine.Switch2Last();
}
//提供切换到下一状态的Button按钮
if (GUILayout.Button(GUIContents.switch2Next, "ButtonMid"))
{
    currentMachine.Switch2Next();
}
//提供切换到空状态的Button按钮
if (GUILayout.Button(GUIContents.switch2Null, "ButtonRight"))
{
    currentMachine.Switch2Null();
}
GUILayout.EndHorizontal();

最终完整代码:

代码语言:javascript复制
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", "切换到下一状态");
            public static GUIContent switch2Last = new GUIContent("Last", "切换到上一状态");
            public static GUIContent switch2Null = new GUIContent("Null", "切换到空状态 (退出当前状态)");
        }
        private List<StateMachine> machines;
        private FieldInfo statesFieldInfo;
        private int currentMachineIndex;
        private string[] machinesName;

        public override void OnInspectorGUI()
        {
            //程序未在运行状态则退出
            if (!Application.isPlaying) return;
            
            if (machines == null)
            {
                //通过反射获取状态机列表
                machines = typeof(FSMMaster).GetField("machines", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(FSMMaster.Instance) as List<StateMachine>;
                //获取状态列表字段
                statesFieldInfo = typeof(StateMachine).GetField("states", BindingFlags.Instance | BindingFlags.NonPublic);
            }

            //当状态机名称数组为空(初始化) 或数量与状态机数量不等时(状态机列表发生变化)
            if (machinesName == null || machines.Count != machinesName.Length)
            {
                //重置当前状态机索引数值
                currentMachineIndex = 0;
                //重新获取状态机名称数组
                machinesName = machines.Select(m => m.Name).ToArray();
            }

            if (machines.Count > 0)
            {
                currentMachineIndex = EditorGUILayout.Popup("状态机:", currentMachineIndex, machinesName);
                var currentMachine = machines[currentMachineIndex];
                //获取当前状态机的状态列表
                var states = statesFieldInfo.GetValue(currentMachine) as List<IState>;

                GUILayout.BeginHorizontal();
                //提供切换到上一状态的Button按钮
                if (GUILayout.Button(GUIContents.switch2Last, "ButtonLeft"))
                {
                    currentMachine.Switch2Last();
                }
                //提供切换到下一状态的Button按钮
                if (GUILayout.Button(GUIContents.switch2Next, "ButtonMid"))
                {
                    currentMachine.Switch2Next();
                }
                //提供切换到空状态的Button按钮
                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];
                    //如果状态为当前状态 使用SelectionRect Style 否则使用IN Title Style进行区分
                    GUILayout.BeginHorizontal(currentMachine.CurrentState == state ? "SelectionRect" : "IN Title");
                    GUILayout.Label(state.Name);

                    //如果状态不是当前状态 提供切换到该状态的Button按钮
                    if(currentMachine.CurrentState != state)
                    {
                        if (GUILayout.Button("Switch", GUILayout.Width(50f)))
                        {
                            currentMachine.Switch(state);
                        }
                    }
                    GUILayout.EndHorizontal();
                }
                GUILayout.EndVertical();
            }
        }
    }
}

0 人点赞