Unity 自定义编辑器

Unity Selection 类

先说一下 Unity.Selection 类, 这个类可以获取我们在编辑器模式下的输入, 因为编辑器模式下各种方法逻辑的触发更多的是使用按钮的方法, 所以获取输入是排在第一位的.

Selection.activeTransform

仅在 Hierarchy 面板中生效;

这个静态字段会返回在 Hierarchy 面板中选中的游戏物体;

如果选择多个则只返回第一个; 如果没有选择, 则返回 null; null 可以直接在 if 中进行判断, 相等于 false.

Selection.activeGameObject

在 Hierarchy 面板和 Project 面板中都有效;

在 Hierarchy 面板中返回的是当前选中的游戏物体(任意物体);

在 Project 面板中返回的是当前选中的预制体(Prefab);

如果选择多个则只返回第一个; 如果没有选择或者选择的目标不符合上述条件的都会返回 null.

Selection.activeObject

在 Hierarchy 面板和 Project 面板中都有效;

在 Hierarchy 面板中返回的是当前选中的游戏物体(任意物体);

在 Project 面板中返回的是当前选中的任意资源;

如果选择多个则只返回第一个; 如果没有选择或者选择的目标不符合上述条件的都会返回 null.

Selection.transforms

Selection.activeTransform 方法的复数版本, 返回一个数组, 包含所有符合 Selection.activeTransform 条件的物体.

Selection.gameObjects

Selection.activeGameObject 方法的复数版本, 返回一个数组, 包含所有符合 Selection.activeGameObject 条件的物体.

Selection.objects

Selection.activeObject 方法的复数版本, 返回一个数组, 包含所有符合 Selection.activeObject 条件的物体.

Selection.selectionChanged

一个委托, 当选择的东西发生变化的时候会自动调用. 可以用来监测选择的游戏物体是否发生了改变.

C# 特性 [MenuItem("A/B", true, 15)]

MenuItem 特性用于向主菜单 (Unity 标题栏下的一行菜单) 和检视面板上下文菜单 (也就是 Inspector 的设置菜单 CONTEXT 菜单) 添加菜单项.

能够将任何静态函数转变为菜单命令, 仅静态函数可使用 MenuItem 特性, 另外 Unity 还支持创建带有热键的菜单项哦.

代码示例 效果
% Ctrl
# Shift
& Alt
LEFT, RIGHT, UP, DOWN LEFT, RIGHT, UP, DOWN
F1 .. F12 F1 .. F12
HOME, END, PGUP, PGDN HOME, END, PGUP, PGDN
MyMenu/Do Something #&g Shift + Alt + g
MyMenu/Do Something _g g
  • 如果要扩展 Project 面板的右键菜单, 则路径必须以 Assets 开头.
  • 如果要扩展 Hierarchy 面板的右键菜单, 则路径必须以 GameObject 开头.
  • 以其他名称开头时, 创建的菜单会显示在主菜单上.
  • 当被修饰方法返回值不是 bool 类型时, 始终应设置为 false.
  • 当被修饰方法返回值是 bool 类型时:
    • true: 当被修饰方法返回 false 时, 同名的菜单项会隐藏; 当被修饰方法返回 true 时, 同名的菜单项才会显示出来.
    • false: 标记此方法不是同名菜单项的验证方法. (当然此时的 bool 返回值也就没有了意义)

因此这个参数的使用场景为:

当某个菜单项只能在某些特定条件下才能显示的时候使用, 创建两个同名的 MenuItem(), 其中一个设置第二个参数为 true, 逻辑就是菜单项显示条件, 而另一个则是真正的菜单项方法.

  • 优先级打开主菜单查看当前有多少菜单项, 依次向后排序即可.
  • 如果想要让菜单项在 Hierarchy 面板的右键菜单中也显示出来的话, 推荐将优先级设置为大于等于 11 的数值, 因为 0 - 10 优先级会导致自定义的菜单项穿插在 Unity 原生菜单项中间, 具体每个优先级的位置可以参照下面的图片.

顶部菜单

Hierarchy 右键菜单

结论:

  1. 负优先级0 ~ 49, 会同时显示在顶部菜单和右键菜单中.
  2. 顶层菜单项之间相差 11 及以上会产生分割线, 相差 10 不行.
  3. 顶部菜单项的子菜单项之间相差 11 也会产生分割线, 但是右键菜单的子菜单项之间即使相差 100 也不会产生分割线.
  4. 即使是相同优先级, 只要菜单项名称不同则都会显示.
  5. 具有子菜单项的菜单, 他的优先级和优先级最高的子菜单项保持一致.
  6. 从优先级 11 开始, 自定义菜单项就会全部显示在 Unity 原生菜单项的下方.
  7. 如果有一个菜单项的名称刚好和一些子菜单项的顶层菜单名称一致了, 那么这个菜单项会丢失, 仅剩下子菜单项的顶层菜单, 仅作为菜单项的文件夹, 无任何作用.

扩展 Project 视图

扩展 Project 视图的右键菜单

  • 定义静态行为类: 无继承关系, 放到 Editor 文件夹下.
  • 定义菜单项: [MenuItem("Menu Item Path", false, 1)].
  • 扩展 Project 视图时, 菜单项的路径必须以 Assets/ 开头, 原因是 Project 视图的右键菜单就是标题栏中的 Assets 菜单.
  • 如果以标题栏中没有的名称作为路径开头, 则标题栏中会出现一个新的对应名称的菜单.
  • 定义菜单行为方法: 菜单行为方法推荐是 private static void FunctionName() 类型.

扩展 Project 布局

扩展 Project 布局的原理就是 Unity 会监听 EditorApplication.projectWindowItemOnGUI 渲染回调, 因此脚本代码的思路就是写一个方法用来对委托 EditorApplication.projectWindowItemOnGUI 进行注册. 并且这个方法必须在 C# 代码编译完成后立即自动执行.

Unity 中提供了实现这个效果的属性, 使用 [InitializeOnLoadMethod] 属性可以让被修饰的方法在代码编译完成后自动执行.

有了自动执行的方法了, 那么我们就可以在这个方法中, 对 EditorApplication.projectWindowItemOnGUI 委托进行注册. 注册的内容就是我们想要实现的操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/// <summary>
/// 扩展 Project 布局
/// </summary>
// 由于扩展布局的方法无法进行手动调用, 所以使用这个属性进行自动调用
[InitializeOnLoadMethod]
private static void InitializeOnLoadMethod()
{
// 对 projectWindowItemOnGUI 进行注册
EditorApplication.projectWindowItemOnGUI += (guid, selectionRect) =>
{
// 前提是必须选中预制体 Prefab
if (!Selection.activeGameObject)
{
return;
}

if (guid != AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(Selection.activeGameObject)))
{
return;
}

// 定义绘制按钮时需要用到的矩形, selectionRect 原本是当前选中时 Unity 中的蓝色的高亮矩形框.
const float WIDTH = 50f;
Rect projectRect = new Rect(
selectionRect.x += selectionRect.width - WIDTH,
selectionRect.y,
WIDTH,
selectionRect.height);

// 设置接下来绘制按钮时要使用的颜色
GUI.color = Color.red;

// 绘制按钮, 显示文本: click
if (GUI.Button(projectRect, "click"))
{
// 定义按钮点击后的行为
Debug.Log($"click: {Selection.activeObject.name}");
}

// 绘制完成后将颜色重置
GUI.color = Color.white;
};
}

扩展 Hierarchy 视图

扩展 Hierarchy 视图的右键菜单

[] 扩展 Project 视图的脚本和扩展 Hierarchy 视图的脚本不能是一个, 必须分开.

  • 扩展 Hierarchy 视图和扩展 Project 视图的操作相同, 只是菜单项的路径必须以 GameObject/ 开头, 原因是 Hierarchy 视图的右键菜单就是标题栏中的 GameObject 菜单.

Hierarchy 视图的右键菜单的顺序

扩展 Hierarchy 布局

  • 扩展 Hierarchy 布局和扩展 Project 布局是一样的原理, 只不过监听的是另一个委托: EditorApplication.hierarchyWindowItemOnGUI.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[InitializeOnLoadMethod]
private static void InitOnLoad() {
EditorApplication.hierarchyWindowItemOnGUI += (instanceID, selectionRect) => {
if (!Selection.activeObject) return;
if (instanceID != Selection.activeObject.GetInstanceID()) return;
const string PREFAB_PATH = "Assets/1.prefab";
const string TEXTURE_PATH = "Assets/unity.png";
const float WIDTH = 32f;
const float HEIGHT = 16f;
var hierarchyRect = new Rect(
selectionRect.width - WIDTH, selectionRect.y,
WIDTH, HEIGHT);
// 加载资源: AssetDatabase.LoadAssetAtPath
var texture = AssetDatabase.LoadAssetAtPath<Texture>(TEXTURE_PATH);
if (!GUI.Button(hierarchyRect, texture)) return;
// 更新或者创建 Prefab
PrefabUtility.SaveAsPrefabAssetAndConnect(Selection.activeGameObject, PREFAB_PATH, InteractionMode.AutomatedAction);
AssetDatabase.Refresh();
};
}

扩展 Inspector 视图

扩展 Inspector 视图就和上面两种完全不一样了. Inspector 视图是用来显示类 (组件本质就是一个类) 中的信息的, 由于类中会有各种各样的千奇百怪的数据类型, 因此 Unity 提供了大量的控件来帮助开发者自定义 Inspector 视图.

重写 Inspector 面板需要做的事情:

  1. 继承 UnityEditor.Editor 类.
  2. 使用特性 CustomEditor(typeof()) 关联要显示的类.
  3. 重写 OnInspectorGUI() 方法.
  4. 绘制各种组件.

扩展原生类组件

扩展原生类组件的 Inspector 面板就是重写 OnInspectorGUI() 方法, 这个方法就是用来绘制 Inspector 面板的.

如果需要显示原本组件的的 Inspector 面板信息, 可以调用父类的方法: base.OnInspectorGUI();.

1
2
3
4
5
6
7
8
9
10
[CustomEditor(typeof(Camera))]
public class ExpendInspector : Editor {
public override void OnInspectorGUI() {
if (GUILayout.Button("扩展按钮")) {
Debug.Log("测试按钮");
}

base.OnInspectorGUI();
}
}

扩展继承类组件

对于我们重写界面而言, 继承类组件原生类组件的不同之处就是不能调用 base.OnInspectorGUI(); 方法, 或者调用 base.OnInspectorGUI(); 时不会出现组件原本的显示效果, 显示效果会变得很糟糕. 因此我们需要使用反射机制来实现调用真正的 OnInspectorGUI() 方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[CustomEditor(typeof(Transform))]
public class ExpendInspector : Editor {
private Editor editor;
private void OnEnable() {
editor = Editor.CreateEditor(target,
Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector", true));
}
public override void OnInspectorGUI() {
if (GUILayout.Button("扩展按钮")) {
Debug.Log("测试按钮");
}
editor.OnInspectorGUI();
}
}

设置 Inspector 面板为只读

设置 Inspector 面板为只读可以让某些设置不被任意修改, 尤其是多人合作开发项目的时候尤其有用.

绘制 Inspector 面板时设置只读

设置自定义 Inspector 面板为只读, 只需要在绘制 Inspector 面板的时候设置 GUI.enabled 为 false 即可将特定的区域设置为只读.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[CustomEditor(typeof(Transform))]
public class ExpendInspector : Editor {
private Editor editor;
private void OnEnable() {
editor = Editor.CreateEditor(target,
Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector", true));
}
public override void OnInspectorGUI() {
if (GUILayout.Button("扩展按钮")) {
Debug.Log("测试按钮");
}
GUI.enabled = false;
editor.OnInspectorGUI();
GUI.enabled = true;
}
}

通过脚本设置只读

即使不直接修改 Inspector 面板, 也可以设置其为只读, 只要设置物体的 hideFlags 属性即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Reflection;
using UnityEditor;
using UnityEngine;

public class ExpendInspector {
private const string MENU_LUCK = "GameObject/3D Object/Lock/Lock";
private const string MENU_UNLUCK = "GameObject/3D Object/Lock/Unlock";

[MenuItem(MENU_LUCK, false, 0)]
private static void GameObjectLock() {
if (Selection.objects != null) {
foreach (var item in Selection.objects) {
item.hideFlags = HideFlags.NotEditable;
}
}
}

[MenuItem(MENU_UNLUCK, false, 0)]
private static void GameObjectUnlock() {
if (Selection.objects != null) {
foreach (Object item in Selection.objects) {
item.hideFlags = HideFlags.None;
}
}
}
}

扩展 Inspector 面板的设置菜单

Inspector 的设置菜单位于 CONTEXT 层级下, 因此路径必须以这个开头. 另外, 这个菜单在扩展时的方法必须有一个 MenuCommand 类型的参数, 里面包含了挂载当前组件的游戏物体的信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Reflection;
using UnityEditor;
using UnityEngine;

public class ExpendInspector {
private const string MENU_1 = "CONTEXT/Transform/PrintName";
private const string MENU_2 = "CONTEXT/Camera/PrintName";

[MenuItem(MENU_1, false, 0)]
private static void Menu1(MenuCommand command) {
Debug.Log(command.context.name);
}

[MenuItem(MENU_2, false, 0)]
private static void Menu2(MenuCommand command) {
Debug.Log(command.context.name);
}
}

扩展 Scene 视图

Scene 视图是开发者搭建游戏场景的视图, 搭建场景的时候可以利用一些辅助元素来更加规范地快捷地搭建游戏场景. 扩展 Scene 视图的时候有两种, 一种是仅作为辅助元素来方便开发者搭建场景, 这一类元素是无法进行交互的, 类似于辅助线的功能, 当时辅助元素不仅仅只有线. 另一种就是绘制可交互的辅助 UI.

不可交互的辅助元素 Gizmos

Unity 中有一个 Gimos 类, 这个类中的 API 就是用来给 Scene 视图绘制辅助元素的. Gizmos 类元素是在 On 类事件中绘制的, 这一类 On 事件的触发需要继承 MonoBehavior, 之后会在编辑器模式下每一帧调用. 也正是因为这个机制导致 Gizmos 绘制脚本不能放到 Editor 文件夹下.

事件方法:

OnDrawGizmosSelected() : 依赖特定的物体, 因此需要将脚本挂载在特定的游戏物体上, 当选中这个游戏物体时, 这个事件会被触发.

OnDrawGizmos() : 不依赖任何游戏物体, 事件会在编辑器模式下的每一帧触发.

绘制方法:

Gizmos.color : 设置绘制元素时画笔的颜色.

Gizmos.DrawLine, Gizmos.DrawCube, Gizmos.DrawSphere 等方法可以用来绘制线段, 长方体, 球体等.

可交互的辅助元素 Handles GUI

第一类是只在特定的物体上让脚本生效, 比如当选择摄像机的时候, Scene 面板中出现几个按钮, 用来打印摄像机的信息.

第二类是全局修改 Scene 窗口, 即任何时候 Scene 窗口上都会显示自定义的内容.

特定物体的 Scene 窗口自定义

这一类的扩展代码编写方法和扩展 Inspector 面板时是一样的.

需要使用特性 CustomEditor(typeof()) 来修饰类, 同时继承 Editor 来绘制 UI.

绘制 UI 的时候, 绘制用的代码放在 OnSceneGUI() 中, 并且必须放在 Handles.BeginGUI()Handles.EndGUI() 之间.

Handles.color 可以修改绘制 UI 时的颜色.

绘制 UI 时使用的 API 依旧是: GUI, GUILayout, EditorGUI, EditorGUILayout 等.

全局的 Scene 窗口自定义

常驻 UI 的绘制方法和依赖物体的 Scene UI 是一样的, 只是这个显示 UI 的方法的调用方式不一样了, 之前的是使用 OnSceneGUI() 方法, 而全部的 UI 是使用 [InitializeOnLoadMethod] 特性来调用自定义方法实现.

创建自定义窗口 (EditorWindow)

定义窗口

继承自 EditorWindow 的类便是窗口本身.

打开窗口

使用继承自 EditorWindow 类中的 GetWindow<MyEditorWindowType>(); 方法便可以打开特定的窗口, 返回打开后的窗口引用.

设置窗口的基本信息

在使用 GetWindow<>(); 之后获得的返回值中有一个 titleContent 可以设置窗口的名称, 图标, 气泡提示.

myWindow.titleContent = new GUIContent("标题名称", textureICON(Texture), "气泡提示内容");

改变风格的两种方式

使用全局 GUI 风格

1
2
3
4
5
6
// 粗体
GUI.skin.label.fontStyle = FontStyle.Bold;
// 24 号
GUI.skin.label.fontSize = 24;
// 居中
GUI.skin.label.alignment = TextAnchor.MiddleCenter;

使用局部 GUI 风格

Unity 规定只能在 OnEnable() 方法中新建局部 GUI 风格.

1
2
3
4
5
6
7
8
9
GUIStyle guiStyleTitle = new GUIStyle
{
// 粗体
fontStyle = FontStyle.Bold,
// 24 号
fontSize = 24,
// 居中
alignment = TextAnchor.MiddleCenter
};

之后将这个新建的风格作为参数, 绘制控件的时候传入即可.

绘制控件

这里可以绘制控件的 API 有: GUI, GUILayout, EditorGUI, EditorGUILayout, 可用于绘制 Inspector 面板, EditorWindow 窗口等.

布局类

  • Space: 空白, 空行, 空格

    EditorGUILayout.Space(10);

  • FlexibleSpace: 自适应空格, 自动填充所处布局的全部剩余空间. 自适应空格大多和布局一起使用, 在水平布局中, 假设按照: A, FlexibleSpace, B 的顺序安排控件, 那么效果是: A 靠左, B 靠右, 中间空白会随着水平布局的变宽而变宽.

    GUILayout.FlexibleSpace();

  • BeginVertical, EndVertical: 垂直布局, 可以通过输入参数 "box" 让布局有一个灰色的背景.

    1
    2
    3
    EditorGUILayout.BeginVertical("box");`
    // ...
    EditorGUILayout.EndVertical();`
  • BeginHorizontal, EndHorizontal: 水平布局, 可以通过输入参数 "box" 让布局有一个灰色的背景.

    1
    2
    3
    GUILayout.BeginHorizontal("box");`
    // ...
    GUILayout.EndHorizontal();`
  • BeginArea, EndArea: 自动布局区域

    1
    2
    3
    GUILayout.BeginArea(new Rect(100, 300, 300, 100));
    // ...
    GUILayout.EndArea();
  • Foldout: 折叠区域, 第一个参数为 bool 类型的参数, 表示是否折叠, 第三个参数是表示点击标题时是否可以展开, 默认是 false

    1
    2
    3
    4
    5
    6
    foldoutTitle = new GUIContent("测试折叠菜单", "气泡提示, 好无聊~");

    if (foldout = EditorGUILayout.Foldout(foldout, foldoutTitle, true))
    {
    // ...
    }
  • ScrollView: 附带滑动条的固定显示区域. 第一个参数是滑动条的位置, 是对滑动条滑动量的一个描述, 是一个 Vector2 类型, 分别表示横向和纵向两个滑动条, 每个轴保存的是滑动条的位置, 并不是百分比, 比如滑动区域的总高度为1000, 当纵向滑动条处于中间时, Vector2 的 Y 周值便是 500, 第二, 三个参数是布局选项, 设置滑动区域的显示大小. 滑动区域的实际大小由内容决定.

    1
    scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Width(100), GUILayout.Height(100));

提示用标签类

  • LabelField, 文字提示, 不可交互, 不可选中, 不可复制

    EditorGUILayout.LabelField("第一个编辑器窗口", guiStyleTitle);

  • SelectableLabel, 可选择的标签, 右键可复制

    EditorGUILayout.SelectableLabel("可选择的标签");

输入框类

  • TextField 文本输入框, 可输入少量文本

    accountName = EditorGUILayout.TextField("账号:", accountName);

  • TextArea, 文字区域, 可输入大量文本

    description = EditorGUILayout.TextArea(description, GUILayout.MaxHeight(75));

  • PasswordField, 密码输入框, 可输入密码

    password = EditorGUILayout.PasswordField("密码:", password);

  • IntField, 整型数字输入框, 只允许输入整数

    intField = EditorGUILayout.IntField("输入框名称", intField);

  • FloatField, 浮点数输入框, 只允许输入小数

    floatField = EditorGUILayout.FloatField("输入框名称", floatField);

滑动条类 (Slider)

  • Slider, 普通(浮点数)滑动条, 滑动时的数值为小数

    sliderValue = EditorGUILayout.Slider("滑动条名称", sliderValue, 0.3f, 30.3f);

  • IntSlider, 整数滑动条, 滑动时的数值为整数

    intSliderValue = EditorGUILayout.IntSlider("整形滑动条名称", intSliderValue, 1, 25);

  • MinMaxSlider, 范围滑动条, 滑动条上左右两侧均可滑动, 数字为小数

    EditorGUILayout.MinMaxSlider(ref sliderMinValue, ref sliderMaxValue, sliderMinLimit, sliderMaxLimit);

弹出菜单选择类

  • Popup, 普通弹出菜单, selectedNameIndex 默认的选择项和当前的选择项, 从 0 开始计数. playerNames 则是一维的字符串数组, 作为选择菜单项的来源数据.

    selectedNameIndex = EditorGUILayout.Popup("弹出菜单的名称: ", selectedNameIndex, playerNames);

  • IntPopup, 整型数字选择菜单, 与 Popup 的区别是索引为自定义的整型数字, 不再是固定的 从 0 开始. 因此需要两个数组, selectedAgeIndex 是默认的选择项和当前的选择项, playerAgesInfo 则是一维的字符串数组, 作为选择菜单项的来源数据, playerAges 则是自定义的索引数组, 其长度必须和前面的数据数组保持一致.

    selectedAgeIndex = EditorGUILayout.IntPopup("整型数字弹出菜单", selectedAgeIndex, playerAgesInfo, playerAges);

  • EnumPopup, 枚举式弹出菜单, 与 Popup 的区别是不再使用索引, 而是使用枚举作为选择的标志. 前面强制类型转换的 PlayerType 就是枚举类型. 后面的 playerType 则是默认的选择项和当前的选择项, 当然这就是 PlayerType 类型的变量.

    playerType = (PlayerType)EditorGUILayout.EnumPopup("枚举式弹出菜单", playerType);

资源选择框

  • ObjectField, 预制件选择框, 可选择资源中的预制件, go 是默认值以及当前的选择值.

    go = EditorGUILayout.ObjectField("物体名", go, typeof(GameObject), false) as GameObject;

交互按钮

  • GUILayout.Button, 点击后返回 true

    1
    2
    3
    4
    if (GUILayout.Button("保存数据", GUILayout.MaxWidth(80)))
    {
    Debug.Log("保存数据成功!");
    }

勾选框

  • Toggle, 第一个参数是显示的提示文本, 第二个选项时默认的勾选状态以及当前的勾选状态

    1
    2
    3
    4
    if (toggleValue = EditorGUILayout.Toggle("多选控件显示的文本, 勾选后进行三维向量检测", toggleValue))
    {
    myv3value = EditorGUILayout.Vector3Field("请输入一个三维向量", myv3value);
    }

文件选择

  • EditorUtility.OpenFilePanel(), 第一个参数为弹出窗口的标题, 第二个为默认路径, 第三个为文件类型条件

    1
    2
    3
    4
    5
    6
    7
    if (GUILayout.Button("Select File", GUILayout.Height(25), GUILayout.Width(120))) {
    filePath = EditorUtility.OpenFilePanel("Select File", filePath, "");
    if (!string.IsNullOrEmpty(filePath)) {
    // 具体的行为
    filePath = null;
    }
    }

目录选择

  • EditorUtility.OpenFolderPanel(), 第一个参数为弹出窗口的标题, 第二个为默认路径, 第三个为默认文件夹名称

    1
    2
    3
    4
    5
    6
    7
    if (GUILayout.Button("Select Folder", GUILayout.Height(25), GUILayout.Width(120))) {
    folderPath = EditorUtility.OpenFolderPanel("Select Folder", folderPath, "");
    if (!string.IsNullOrEmpty(folderPath)) {
    // 具体的行为
    folderPath = null;
    }
    }

目录选择

进度条

进度条有两种, 第一种是附带了一个 "取消" 按钮的进度条, 这个是可以中途取消的, 另外一种就是没有取消按钮的进度条.

可中途取消的进度条

可取消的进度条

使用下方的 DisplayCancelableProgressBar 方法便可以生成一个带有取消按钮的进度条, 返回值为 bool 类型. 当点击 "取消" 按钮时, 返回值为 false, 否则返回值始终为 true, 即使第三个参数的数值已经超过了 1 (进度条数值溢出, 外观上不会溢出) 返回值也是 true. 需要注意的是: 取消按钮并不能删除当前显示的进度条, 这个按钮只能让 DisplayCancelableProgressBar() 方法返回 false, 需要取消进度条必须使用 EditorUtility.ClearProgressBar(); 方法.

EditorUtility.DisplayCancelableProgressBar("进度条窗口的标题", "进度条下方的描述", 浮点型的进度条百分比(0-1))

因此大多数在循环中展示进度条, 在一个将要处理大量数据的循环中加一个进度条, 这样每次循环的时候, 外部都会有一个进度值实时告诉用户进度, 用户就不容易以为设备卡住了.

不可中途取消的进度条

不可取消的进度条

这个就更简单了, 使用 DisplayProgressBar() 方法便可以生成不可中途取消的进度条, 方法的返回值为 void, 因此不需要过多的处理.

EditorUtility.DisplayProgressBar("进度条窗口的标题", "进度条下方的描述", 浮点型的进度条百分比(0-1))

[] 这两种进度条都必须使用 EditorUtility.ClearProgressBar(); 方法才能取消进度条的显示.

自定义进度条

Unity 的进度条有一个不好的地方就是必须自己去 Clear 进度条, 经常会出现忘记 Clear 导致 Unity 卡住的情况, 所以就自己写了一个可以自动 Clear 的进度条.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEditor;

namespace KurohaUtility {
public static class CommonProgressBar {
public static void ProgressBar(string title, string info, float progress) {
if (progress >= 1) {
EditorUtility.ClearProgressBar();
} else {
EditorUtility.DisplayProgressBar(title, info, progress);
}
}
public static bool ProgressBarCancel(string title, string info, float progress) {
var result = false;
if (progress >= 1) {
EditorUtility.ClearProgressBar();
} else {
result = EditorUtility.DisplayCancelableProgressBar(title, info, progress);
if (result) {
EditorUtility.ClearProgressBar();
}
}
return result;
}
}
}

附录

常用 API

EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();

如果用户当前修改了场景, 并且还没有对场景进行保存, 则会询问用户是否保存场景; 如果当前场景没有被修改, 则无事发生.

unity_editor_api

路径开头总结

路径内容 开头 原因
AssetDatabase 访问资源路径 Assets Unity 中资源的顶级目录为: Assets
Resources 动态加载资源路径 Resources.Load() 方法中参数是相对路径的文件名, 所以不能加任何开头
Project 面板菜单 Assets Project 面板菜单就是标题栏中的 Assets 菜单
Hierarchy 面板菜单 GameObject Hierarchy 面板菜单就是标题栏中的 GameObject 菜单
Inspector 面板中单个组件的设置菜单 CONTEXT Inspector 面板中的组件设置菜单就是在 CONTEXT 目录下
Inspector 面板中所有组件的设置菜单 CONTEXT/Component 设置特定组件就在 CONTEXT 后加特定组件, 如果要设置全部组件, 就加 Component

如何高亮选中物体

Hierarchy 面板

1
2
3
4
5
6
// 搜索物体
GameObject go = GameObject.Find("Main Camera");
// 高亮物体, 同时将定位 Hierarchy 面板中高亮物体的位置
EditorGUIUtility.PingObject(go);
// 选中物体, 显示 Inspector 面板 (目前 Unity 存在 Bug, 选中后直接变成 "重命名" 状态)
Selection.activeGameObject = go;

Project 面板

1
2
3
4
5
6
// 读取资源
GameObject go = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cube.prefab");
// 高亮物体, 同时将定位 Project 面板中高亮物体的位置
EditorGUIUtility.PingObject(go);
// 选中物体, 显示 Inspector 面板
Selection.activeObject = go;

EditorWindow 中变量的初始化

窗口类中的变量通常是需要在 OnGUI() 方法中使用, 根据方法的生命周期来看, 在 OnGUI() 方法中使用的变量都是可以直接在 OnEnable() 方法中进行初始化的. 同时由于这类变量通常需要保存数据到本地, 所以大多是使用文件中读取的数值进行初始化.

另外需要注意的一个点是: 之前说过 OnEnable()OnFocus() 是在 GetWindow() 方法返回之前执行的, 因此这两个方法不能用来对 GetWindow() 的返回值进行设置, 所以对窗口的外观设置任务就只能交给 AfterGetWindow 区域了, 或者直接独立出一个方法来进行窗口的外观设置.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using UnityEditor;
using UnityEngine;

public class MyWindow : EditorWindow
{
string windowTitle;

// static 特性: 在编辑器模式下的静态变量一旦赋值, 除非 "Unity 重新编译脚本" 或者 "Unity 退出", 否则变量的值一直保留, 不会重置.
private static MyWindow self;
private static GUIContent windowTitleContent;

public static void Open()
{
self = GetWindow<MyWindow>();

// 对窗口初始化的任务就只能交给 AfterGetWindow 区域了.
self.titleContent = windowTitleContent;
self.minSize = new Vector2(500f, 500f);
self.maxSize = self.minSize;
}

private void OnEnable()
{
// OnEnable 中不能对 self 做任何操作, 因为此时 self 还是空值.
// 虽然由于 static 的特性, 仅第 1 次设置会报错, 但是既然报错就必须要禁止!😂

// 对 OnGUI 中需要用到的变量初始化
windowTitle = "This is a string inside a Scroll view!";
windowTitleContent ??= new GUIContent("测试窗口1", "气泡测试1");
}

private void OnGUI()
{
GUILayout.Label(windowTitle);
}
}

获取场景中全部游戏物体

使用普通的方法只能获取非隐藏的游戏物体, 而我们大多数时候都是需要获取全部游戏物体的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// 获取 Hierarchy 中所有的游戏物体
/// 包括被禁用的游戏物体
/// </summary>
/// <returns></returns>
private List<GameObject> GetAllSceneObjectsWithInactive()
{
var allTransforms = Resources.FindObjectsOfTypeAll(typeof(Transform));
var previousSelection = Selection.objects;
Selection.objects = allTransforms.Cast<Transform>()
.Where(x => x != null)
.Select(x => x.gameObject)
//.Where(x => x != null && !x.activeInHierarchy)
//.Where(x => x != null && x.activeInHierarchy)
.Cast<UnityEngine.Object>().ToArray();

var selectedTransforms = Selection.GetTransforms(SelectionMode.Editable | SelectionMode.ExcludePrefab);
Selection.objects = previousSelection;

return selectedTransforms.Select(transform => transform.gameObject).ToList();
}