本文介绍如何为FSM有限状态机模块实现一个自定义编辑器面板
首先,自定义一个编辑器面板,需要用到Attribute:CustomEditor,参数传入目标类的类型,代码如下:
usingUnityEngine; usingUnityEditor; namespaceSK.Framework{ [CustomEditor(typeof(FSMMaster))] publicclassFSMEditor : Editor { } }
自定义编辑器类继承Editor类后,重写OnInspectorGUI函数来自定义Inspector面板,例如添加一个Label文本:
usingUnityEngine; usingUnityEditor; namespaceSK.Framework{ [CustomEditor(typeof(FSMMaster))] publicclassFSMEditor : Editor { publicoverridevoidOnInspectorGUI() { GUILayout.Label("有限状态机"); } } }
绘制该面板我们需要FSM Master中的状态机列表的信息,是一个私有的StateMachine类型的列表,因此需要通过反射去获取:
usingSystem.Reflection; usingSystem.Collections.Generic; usingUnityEngine; usingUnityEditor; namespaceSK.Framework{ [CustomEditor(typeof(FSMMaster))] publicclassFSMEditor : Editor { privateList<StateMachine>machines; publicoverridevoidOnInspectorGUI() { //程序未在运行状态则退出if (!Application.isPlaying) return; if (machines==null) { //通过反射获取状态机列表machines=typeof(FSMMaster).GetField("machines", BindingFlags.Instance|BindingFlags.NonPublic) .GetValue(FSMMaster.Instance) asList<StateMachine>; } } } }
有了状态机的信息后,通过EditorGUILayout类中的Popup去列举所有的状态机,其中需要传入一个string类型数组,即列举的内容,我们声明一个string类型数组来存储所有状态机的名称,使用一个int类型字段来表示当前选中的状态机的索引:
usingSystem.Linq; usingSystem.Reflection; usingSystem.Collections.Generic; usingUnityEngine; usingUnityEditor; namespaceSK.Framework{ [CustomEditor(typeof(FSMMaster))] publicclassFSMEditor : Editor { privateList<StateMachine>machines; privateintcurrentMachineIndex; privatestring[] machinesName; publicoverridevoidOnInspectorGUI() { //程序未在运行状态则退出if (!Application.isPlaying) return; if (machines==null) { //通过反射获取状态机列表machines=typeof(FSMMaster).GetField("machines", BindingFlags.Instance|BindingFlags.NonPublic) .GetValue(FSMMaster.Instance) asList<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,因此也通过反射去获取:
usingSystem.Linq; usingSystem.Reflection; usingSystem.Collections.Generic; usingUnityEngine; usingUnityEditor; namespaceSK.Framework{ [CustomEditor(typeof(FSMMaster))] publicclassFSMEditor : Editor { privateList<StateMachine>machines; privateFieldInfostatesFieldInfo; privateintcurrentMachineIndex; privatestring[] machinesName; publicoverridevoidOnInspectorGUI() { //程序未在运行状态则退出if (!Application.isPlaying) return; if (machines==null) { //通过反射获取状态机列表machines=typeof(FSMMaster).GetField("machines", BindingFlags.Instance|BindingFlags.NonPublic) .GetValue(FSMMaster.Instance) asList<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); varcurrentMachine=machines[currentMachineIndex]; //获取当前状态机的状态列表varstates=statesFieldInfo.GetValue(currentMachine) asList<IState>; } } } }
有了状态的列表信息后,for循环遍历列表,绘制每一个状态的名称,使用不同的GUIStyle来区分该状态是否为状态机的当前状态,如果不是,则提供一个切换到该状态的Button按钮:
usingSystem.Linq; usingSystem.Reflection; usingSystem.Collections.Generic; usingUnityEngine; usingUnityEditor; namespaceSK.Framework{ [CustomEditor(typeof(FSMMaster))] publicclassFSMEditor : Editor { privateList<StateMachine>machines; privateFieldInfostatesFieldInfo; privateintcurrentMachineIndex; privatestring[] machinesName; publicoverridevoidOnInspectorGUI() { //程序未在运行状态则退出if (!Application.isPlaying) return; if (machines==null) { //通过反射获取状态机列表machines=typeof(FSMMaster).GetField("machines", BindingFlags.Instance|BindingFlags.NonPublic) .GetValue(FSMMaster.Instance) asList<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); varcurrentMachine=machines[currentMachineIndex]; //获取当前状态机的状态列表varstates=statesFieldInfo.GetValue(currentMachine) asList<IState>; GUILayout.BeginVertical("Box"); for (inti=0; i<states.Count; i++) { varstate=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将这三个按钮绘制到一排:
privateclassGUIContents{ publicstaticGUIContentswitch2Next=newGUIContent("Next", "切换到下一状态"); publicstaticGUIContentswitch2Last=newGUIContent("Last", "切换到上一状态"); publicstaticGUIContentswitch2Null=newGUIContent("Null", "切换到空状态 (退出当前状态)"); }
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();
最终完整代码:
usingSystem.Linq; usingSystem.Reflection; usingSystem.Collections.Generic; usingUnityEngine; usingUnityEditor; namespaceSK.Framework{ [CustomEditor(typeof(FSMMaster))] publicclassFSMEditor : Editor { privateclassGUIContents { publicstaticGUIContentswitch2Next=newGUIContent("Next", "切换到下一状态"); publicstaticGUIContentswitch2Last=newGUIContent("Last", "切换到上一状态"); publicstaticGUIContentswitch2Null=newGUIContent("Null", "切换到空状态 (退出当前状态)"); } privateList<StateMachine>machines; privateFieldInfostatesFieldInfo; privateintcurrentMachineIndex; privatestring[] machinesName; publicoverridevoidOnInspectorGUI() { //程序未在运行状态则退出if (!Application.isPlaying) return; if (machines==null) { //通过反射获取状态机列表machines=typeof(FSMMaster).GetField("machines", BindingFlags.Instance|BindingFlags.NonPublic) .GetValue(FSMMaster.Instance) asList<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); varcurrentMachine=machines[currentMachineIndex]; //获取当前状态机的状态列表varstates=statesFieldInfo.GetValue(currentMachine) asList<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 (inti=0; i<states.Count; i++) { varstate=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(); } } } }