学习自定义 Unity 编辑器扩展(入门)
自定义功能菜单
所有的的编辑器扩展的代码都要放在 Assets/Editor 目录下面
Editor 下面的代码和资源不会打包在安装包里面
添加自己的功能菜单
在 Editor 目录新建一个脚本,名字可以按照自己的喜好。
using UnityEditor; // 编辑器扩展需要引入 UnityEditor 命名空间
using UnityEngine;
public class Tools
{
// 通过 MenuItem 可以添加自定义菜单
// Tools/Build 代表在路径 Tools 添加一个 Build 的菜单,如果 Tools 不存在则自动创建
// 添加 MenuItem 的方法必须是静态方法
// 方法是公开还是私有的无所谓
[MenuItem("Tools/Build")]
public static void Build()
{
Debug.Log("Build");
}
}
关于 MenuItem
对于不同的两个 MenuItem 的编辑器方法,路径不能一样。
我们可以通过使用和系统一样的路径来放到系统菜单里面比如Window/Tools/Build
每一个 MenuItem 默认的优先级为 1000,优先级越小添加的功能越是可以添加在菜单的最上面。
如果两个相邻的 MenuItem 优先级相差大于等于11 则会进行分组
比如 【MenuItem1 优先级1】 【MenuItem2 优先级2 】 【MenuItem3 优先级13】
则 【MenuItem1 MenuItem2】分为一组,【MenuItem3】分为一组
Hierachy 添加右键的功能菜单
可以添加到GameObject分组下面,设置的优先级请再[0-22]范围内
| GameObject 子菜单 | 优先级 |
|---|---|
| Create Empty | 0 |
| Create Empty Child | 0 |
| Create Empty Parent | 0 |
| 3D Object | 1 |
| Effects | 2 |
| Light | 3 |
| Audio | 4 |
| Video | 5 |
| UI | 6 |
| UI Tools | 8 |
| Camera | 10 |
| Visual Scriping Scene Variables | 11 |
Project 的右键菜单添加功能菜单
需要添加在 Assets 的目录下面,优先级范围没有限定
给挂在的脚本添加功能菜单
[MenuItem("CONTEXT/PlayerHealth/InitPlayer")]
static void InitPlayer()
{
Debug.Log("InitPlayer");
}
路径为CONTEXT/脚本名/菜单名
CONTEXT是固定的路径,PlayerHealth代表是想给那个脚本添加菜单。
MenuCommand
这个有什么作用呢?假设我们通过MenuItem给一个脚本添加了一个功能菜单,但是怎么才能获取到那个脚本的实例做一下操作呢,比如自动实例化我们脚本的一些设置呢?此时 MenuCommand 就派上了用场。
我们看一下官方的一个例子
// 给 Rigibody 添加一个菜单项 "Do Something"
using UnityEngine;
using UnityEditor;
public class Something : EditorWindow
{
// Add menu item
[MenuItem("CONTEXT/Rigidbody/Do Something")]
static void DoSomething(MenuCommand command)
{
Rigidbody body = (Rigidbody)command.context;
body.mass = 5;
Debug.Log("Changed Rigidbody's Mass to " + body.mass + " from Context Menu...");
}
}
那么我们初始化玩家的血量为200,射击的速度为10的代码可以写成下面。
[MenuItem("CONTEXT/PlayerHealth/InitPlayer")]
static void InitPlayer(MenuCommand command)
{
CompleteProject.PlayerHealth playerHealth = command.context as CompleteProject.PlayerHealth;
playerHealth.startingHealth = 200;
playerHealth.flashSpeed = 10;
}
Selection
我们可以获取到我们目前选中的游戏物体对象
activeGameObject
Selection.activeGameObject
如果选中一个则输出对应游戏物体的名称
如果选中多个则输出最后一个选中游戏物体的名称
activeObject
Selection.activeObject
这个和 Selection.activeGameObject 区别还包含在资源里面的预制体
activeTransform
Selection.activeTransform
获取当前选中的 Transform(获取场景里面的游戏物体推荐用这个方法)
count
Selection.count
获取选中的个数
gameObjects
Selection.gameObjects
获取选中的游戏物体对象数组 包括预制体还有不可修改的对象
objects
Selection.objects
获取场景里面所有选中的游戏物体
编写可以撤销删除操作的功能
[MenuItem("GameObject/CustomDelete", false, 1)]
public static void DeleteSelectionObject()
{
foreach (var obj in Selection.objects)
{
// 循环遍历选中的游戏物体
// 进行删除操作
Undo.DestroyObjectImmediate(obj);
}
}
我们为什么要使用 Undo.DestroyObjectImmediate 这个方法而不直接使用 DestroyObjectImmediate 这个方法进行删除呢?因为通过操作 Undo 的操作可以进行正常的撤销操作。
给菜单设置快捷键
设置单个字母的快捷键
通过 MenuItem("xxx/xxx _t") 的方式进行添加
跟在路径的后面中间添加一个空格,
_开头带上快捷键的小字母。
比如给一个菜单按钮添加一个快捷键为 T 的
[MenuItem("Tools/Tool Manager _t")]
public static void ToolManager()
{
Debug.Log("Tool Manager");
}
当我们按下
T的时候,可能会提示和目前已经存在的快捷键冲突,没关系,这已经代表我们设置的快捷键已经生效了。
设置组个的快捷键
| 快捷键 | 字符 |
|---|---|
| Ctrl | % |
| Shift | # |
| Alt | & |
我们把上面的菜单组合为 Ctrl+Shift+Alt+T
[MenuItem("Tools/Tool Manager %#&t")]
public static void ToolManager()
{
Debug.Log("Tool Manager");
}
控制菜单项是否启用
假设我们需要要求只有选中了游戏物体才能启用删除,我们的代码可以写成如下。
[MenuItem("GameObject/CustomDelete", true, 1)]
public static bool CanDeleteSelectionObject()
{
return Selection.count > 0;
}
[MenuItem("GameObject/CustomDelete", false, 1)]
public static void DeleteSelectionObject()
{
foreach (var obj in Selection.objects)
{
// 循环遍历选中的游戏物体
// 进行删除操作
Undo.DestroyObjectImmediate(obj);
}
}
为了能做到可以启动或者关闭对应的菜单功能,我们需要新增一个方法,和之前的方法拥有一样的菜单路径,并且返回值返回一个 Bool,如果返回 true 代表启用,如果 false 代表不启用。
[MenuItem("<Menu Path>", true, <优先级>)] bool EnableMyMenu() { // 是否启用的判断逻辑 } [MenuItem("<Menu Path>", false, <优先级>)] bool MyMenu() { // 菜单按钮的操作逻辑 }
没有选中游戏物体
选中了游戏物体
ContextMenu
ContextMenu VS MenuItem
| 对比项 | ContextMenu | MenuItem |
|---|---|---|
| 给系统组件添加菜单 | 不能 | 可以 |
| 给可以操作源码添加菜单 | 可以 | 可以 |
| 菜单执行操作复杂程度 | 简单 | 复杂 |
| 是否支持操作私有变量和方法 | 支持 | 不支持 |
我们通过 ContextMenu 操作 PlayerHealth 来初始化血量和射击的速度
namespace CompleteProject
{
public class PlayerHealth : MonoBehaviour
{
......
[ContextMenu("Init Player Health")]
void InitPlayerHealth()
{
startingHealth = 200;
flashSpeed = 10;
}
}
}
ContextMenuItem
可以对于编辑器的属性进行扩展设置
public class PlayerHealth : MonoBehaviour
{
[ContextMenuItem("Add Health", "AddHealth")]
public int startingHealth = 100; // 初始化的血量
void AddHealth()
{
startingHealth += 100;
}
}
ContextMenuItem 函数的第一个参数代表给属性添加菜单的名称,第二个代表需要执行的方法。
我们同样可以按照上面添加快捷键的方式添加快捷键,但是我试过是没有反应的,不知道是不是不支持。
显示对话框
如何弹出一个对话框
[MenuItem("Tools/Show Editor Dialog")]
public static void ShowEditorDialog()
{
/*
DisplayWizard 方法有三个参数分别是
title: 对话框标题
createButtonName: 创建按钮显示的文本
otherButtonName: 其他按钮显示的文本,不设置则不会显示出来
*/
ScriptableWizard.DisplayWizard<EditorDialog>("Show Editor Dialog");
}
我们 EditorDialog 写的所有的参数都会像属性面板一样在对话框一样被解析展示出来。
监听点击 Create/Others 按钮的点击事件
只需要实现下面的对应函数即可
// 当点击了 [CreateButton] 的点击方法则会调用 [OnWizardCreate] 方法
private void OnWizardCreate()
{
Debug.Log("OnWizardCreate");
}
// 当点击了 [OtherButton] 的点击方法则会调用 [OnWizardOtherButton] 方法
private void OnWizardOtherButton()
{
Debug.Log("OnWizardOtherButton");
}
点击了
Create按钮会调用OnWizardCreate方法,对话框会自动消失点击了
Others按钮会调用OnWizardOtherButton方法,对话框不会自动消失
通过输入框统一给敌人新增 100% 的血量
private void OnWizardCreate()
{
// 遍历当前选中的游戏物体
foreach (var prefab in Selection.gameObjects)
{
// 从游戏物体获取 [CompleteProject.EnemyHealth] 组件
CompleteProject.EnemyHealth enemyHealth = prefab.GetComponent<CompleteProject.EnemyHealth>();
// 如果组件为空,则代表不是敌人的预制体,跳过。
if (enemyHealth == null) continue;
// 让当前敌人的初始化的血量增加一倍
enemyHealth.startingHealth *= 2;
}
}
自定义撤销
对于上面我们修改了敌人的血量 我们怎么才能自定义撤销呢,则需要用上 Undo.RecordObject
......
// 在修改 [enemyHealth] 对象之前进行记录 撤销则将 [enemyHealth] 恢复到记录的状态了
Undo.RecordObject(enemyHealth, "change enemy health");
// 让当前敌人的初始化的血量增加一倍
enemyHealth.startingHealth *= 2;
OnWizardUpdate
/*
当对话框被创建出来的时候会被调用
当对话框里面的属性值被修改的时候会被调用
这个方法可以干什么呢?比如初始化一些值,当修改一个值的时候,会自动修改其他的值
*/
private void OnWizardUpdate()
{
Debug.Log("OnWizardUpdate");
}
帮助信息/错误信息
对于现实帮助信息和错误信息就需要用到 OnWizardUpdate 方法
private void OnWizardUpdate()
{
if (Selection.count == 0)
{
errorString = "你没有选中任何物体";
}
else
{
helpString = "你当前选中了 " + Selection.gameObjects.Length + " 个物体";
}
}
没有选择一个物体的报错
当选择了游戏物体
但是此时有一个问题,当弹出框已经弹出,改动选中的物体之后我们的逻辑不会再次触发,我们可以选择将逻辑放在 Update 方法里面。
private void Update()
{
if (Selection.count == 0)
{
errorString = "你没有选中任何物体";
}
else
{
helpString = "你当前选中了 " + Selection.gameObjects.Length + " 个物体";
}
}
我们会发现我们的提示信息和错误信息回同时的出现,我们只需要显示错误信息的时候讲提示信息为空,或者现实提示信息的时候讲错误信息为空。
private void Update()
{
if (Selection.count == 0)
{
errorString = "你没有选中任何物体";
// 如果当前没有选中游戏物体 则不显示当前选中多少个游戏物体的提示
helpString = "";
}
else
{
helpString = "你当前选中了 " + Selection.gameObjects.Length + " 个物体";
// 如果当前有选中的游戏物体 则不显示错误提示
errorString = "";
}
}
显示提示信息
private void OnWizardOtherButton()
{
// 展示一条文本提示信息
ShowNotification(new GUIContent("你点击了 Others 按钮"));
// RemoveNotification(); 可以立即移除提示信息 UI
}
使用 EditorPrefs 保存数据
设置值
// 设置一个布尔值
EditorPrefs.SetBool("EditorDialog", true);
// 设置一个浮点数
EditorPrefs.SetFloat("EditorDialogX", Input.mousePosition.x);
// 设置一个整数
EditorPrefs.SetInt("Age", 18);
// 设置一个字符串
EditorPrefs.SetString("Name", "张三");
获取值
// 获取一个布尔值
EditorPrefs.GetBool("EditorDialog");
// 获取一个浮点数
EditorPrefs.GetFloat("EditorDialogX");
// 获取一个整数
EditorPrefs.GetInt("Age");
// 获取一个字符串
EditorPrefs.GetString("Name");
显示进度条
创建代码控制隐藏的进度条
private async void ShowAutoDismissProgressBar()
{
// 通过 [EditorUtility.DisplayProgressBar] 创建不能点击 [cancel] 按钮进行取消 需要通过 [EditorUtility.ClearProgressBar] 进行取消
float progress = 0.0f;
while (progress < 1)
{
EditorUtility.DisplayProgressBar("Creating Progress Bar", "Current Progress", progress);
// 每 1 秒让进度条增加 20%
await Task.Delay(1_000);
progress += 0.2f;
}
// 当进度条完成 100% 的时候就自动关闭进度条
EditorUtility.ClearProgressBar();
}
创建用户主动取消的进度条
private async void UserCanClearProgressBar()
{
float progress = 0.0f;
// 如果当前进度没有到 100% 则继续展示进度
while (progress < 1)
{
// 获取到用户是否点击了 [cancel] 按钮
bool result = EditorUtility.DisplayCancelableProgressBar("Creating Progress Bar", "Current Progress", progress);
// 如果点击了 [cancel] 按钮 则可以立马取消进度条
if (result)
{
break;
}
// 延时 1 秒
await Task.Delay(1_000);
// 当前进度增加 20%
progress += 0.2f;
}
// 关闭进度条
EditorUtility.ClearProgressBar();
}
自定义窗口
自定义窗口需要继承 EditorWindow 这个类
展示自定义窗口
// [MyCustomWindow] 不需要我们进行创建,只需要我们通过 [GetWindow] 方法获取即可
MyCustomWindow window = EditorWindow.GetWindow<MyCustomWindow>();
window.Show();
自定义 UI 显示
private void OnGUI()
{
// 通过 [GUILayout.Label] 显示一个文本
GUILayout.Label("Hello World!");
// 通过 [GUILayout.TextField] 显示一个输入框 通过 [inputText] 显示一个文本内容
// 通过返回值获取输入框输入的内容
// ⚠️ 为什么要用一个变量接收,因为每一次输入都会重新调用 [OnGUI] 方法,那么我们内容就会被重置,导致输入框没有任何的变化
inputText = GUILayout.TextField(inputText);
// 通过 [GUILayout.Button] 显示一个按钮
// 通过返回值判断按钮是否被点击
if (GUILayout.Button("OK"))
{
Debug.Log("按钮被点击了,输入框的内容是:" + inputText);
}
}