本文介绍如何为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();
}
}
}
}