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

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

本文介绍如何为FSM有限状态机模块实现一个自定义编辑器面板

image.gif

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

usingUnityEngine;
usingUnityEditor;
namespaceSK.Framework{
    [CustomEditor(typeof(FSMMaster))]
publicclassFSMEditor : Editor    {
    }
}

image.gif

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

usingUnityEngine;
usingUnityEditor;
namespaceSK.Framework{
    [CustomEditor(typeof(FSMMaster))]
publicclassFSMEditor : Editor    {
publicoverridevoidOnInspectorGUI()
        {
GUILayout.Label("有限状态机");
        }
    }
}

image.gif

image.gif

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

image.gif

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>;
            }
        }
    }
}

image.gif

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

image.gif

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);
            }
        }
    }
}

image.gif

image.gif

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

image.gif

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>;
            }
        }
    }
}

image.gif

有了状态的列表信息后,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();
            }
        }
    }
}

image.gif

image.gif

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

privateclassGUIContents{
publicstaticGUIContentswitch2Next=newGUIContent("Next", "切换到下一状态");
publicstaticGUIContentswitch2Last=newGUIContent("Last", "切换到上一状态");
publicstaticGUIContentswitch2Null=newGUIContent("Null", "切换到空状态 (退出当前状态)");
}

image.gif

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();

image.gif

最终完整代码:

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();
            }
        }
    }
}

image.gif

目录
相关文章
|
7月前
|
图形学 开发者
Unity编辑器脚本(添加/删除)碰撞盒
这段代码提供了两个Unity编辑器工具,用于批量处理模型的碰撞盒。一是“一键添加所有碰撞盒”,通过选择模型的父物体,自动为其子物体添加`MeshCollider`。二是“一键清理所有Collider碰撞盒”,同样选择父物体后,递归删除子物体上的`BoxCollider`组件。两者均通过Unity的菜单项实现便捷操作,方便开发者快速调整场景中的物理属性。
|
10月前
|
前端开发
业余时间开发了个海报编辑器
为了满足撰写博客或录制教程视频时对高质量海报的需求,我利用业余时间开发了一款海报编辑器。第一版功能简单,支持固定尺寸、黑底白字的标题。后来经过优化,增加了背景图、模糊效果、文字样式调整等功能,使海报更具吸引力。目前该编辑器已上线,欢迎大家试用并反馈。[访问海报编辑器](https://tool.share888.top/#/poster)
166 6
业余时间开发了个海报编辑器
|
10月前
|
缓存 API 开发工具
有关Unity使用Rider编辑器无法弹出代码提示的有效解决方法
【11月更文挑战第13天】在 Unity 中使用 Rider 编辑器时,若遇到代码提示无法弹出的问题,可以通过检查 Rider 设置(如自动补全选项、Unity 插件安装、索引设置)、Unity 项目设置(如解决方案正确关联、脚本导入设置)以及环境和依赖关系(如 .NET SDK 版本兼容性、Unity 和 Rider 版本兼容性)等方面进行排查和解决。
1566 5
|
12月前
|
JavaScript 前端开发 API
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
本文介绍了如何在Vue 3项目中使用v-md-editor组件库来创建markdown编辑器和预览组件。文章提供了安装步骤、如何在main.js中进行全局配置、以及如何在页面中使用VMdEditor和VMdPreview组件的示例代码。此外,还提供了一个完整示例的链接,包括编辑器和预览组件的使用效果和代码。
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
|
11月前
|
运维 Java Linux
【运维基础知识】掌握VI编辑器:提升你的Java开发效率
本文详细介绍了VI编辑器的常用命令,包括模式切换、文本编辑、搜索替换及退出操作,帮助Java开发者提高在Linux环境下的编码效率。掌握这些命令,将使你在开发过程中更加得心应手。
131 2
|
图形学 开发者 存储
超越基础教程:深度拆解Unity地形编辑器的每一个隐藏角落,让你的游戏世界既浩瀚无垠又细节满满——从新手到高手的全面技巧升级秘籍
【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具,可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术,详细介绍如何利用其功能构建广阔且精细的游戏世界,并提供具体示例代码,展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧,开发者能显著提升游戏画面质量和玩家体验。
757 3
一款非常棒的十六进制编辑器 —— 010 Editor
一款非常棒的十六进制编辑器 —— 010 Editor
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
1115 0
|
开发者 图形学 API
从零起步,深度揭秘:运用Unity引擎及网络编程技术,一步步搭建属于你的实时多人在线对战游戏平台——详尽指南与实战代码解析,带你轻松掌握网络化游戏开发的核心要领与最佳实践路径
【8月更文挑战第31天】构建实时多人对战平台是技术与创意的结合。本文使用成熟的Unity游戏开发引擎,从零开始指导读者搭建简单的实时对战平台。内容涵盖网络架构设计、Unity网络API应用及客户端与服务器通信。首先,创建新项目并选择适合多人游戏的模板,使用推荐的网络传输层。接着,定义基本玩法,如2D多人射击游戏,创建角色预制件并添加Rigidbody2D组件。然后,引入网络身份组件以同步对象状态。通过示例代码展示玩家控制逻辑,包括移动和发射子弹功能。最后,设置服务器端逻辑,处理客户端连接和断开。本文帮助读者掌握构建Unity多人对战平台的核心知识,为进一步开发打下基础。
551 0
|
开发者 图形学 C#
揭秘游戏沉浸感的秘密武器:深度解析Unity中的音频设计技巧,从背景音乐到动态音效,全面提升你的游戏氛围艺术——附实战代码示例与应用场景指导
【8月更文挑战第31天】音频设计在游戏开发中至关重要,不仅能增强沉浸感,还能传递信息,构建氛围。Unity作为跨平台游戏引擎,提供了丰富的音频处理功能,助力开发者轻松实现复杂音效。本文将探讨如何利用Unity的音频设计提升游戏氛围,并通过具体示例代码展示实现过程。例如,在恐怖游戏中,阴森的背景音乐和突然的脚步声能增加紧张感;在休闲游戏中,轻快的旋律则让玩家感到愉悦。
588 0