Unity编辑器扩展的代码总结(二)

前言

        本来这些应该在Unity编辑器扩展的代码总结中一并写了。但是我突然觉得我最近写的文章篇幅都太长了。以至于当我写到后面的时候就感觉:我都写了这么多了,还要写,最终导致我敷衍了事。所以这次我索性就分文章了。

环境

    Window10系统
    Unity2022.3.8f1c1

选择文件

        我们可以调用EditorUtility.OpenFilePanel来打开选择文件的面板。这个最好使用一个Button来判断是否打开。如果你直接调用,那么就会一直打开这个面板直到你关闭Unity编辑器。代码如下:

1
2
3
4
5
6
7
private void OnGUI()
{
if(GUILayout.Button("..."))
{
var path = EditorUtility.OpenFilePanel("选择文件", Application.dataPath, "json");
}
}

我们还可以通过EditorUtility.OpenFolderPanel来打开文件夹。(更多信息请查看官方的EditorUtility文档

获取我们在编辑器中的选择

        在游戏开发中,我们经常会选中编辑器中的一些资源然后将其拖到一些变量上。然后变量就自动填充上我们选定的资源。而在编辑器扩展中,我们可以使用DragAndDrop类来帮助我们实现这一点。如果你使用过EditorGUILayout.PropertyField,你会发现Unity已经帮我们实现了这样的功能。但是这些只针对简单的情况,比如List,List等。如果对象是我们自定义的类,那么你就不能直接拖了。所以这部分逻辑就要我们自己来写了。

        这里我们要解决两个问题:一个是如何获取到我们在编辑器中的选择,另一个则是我如何知道用户将其选择拖入到变量对应的UI中。第一个问题我们可以使用DragAndDrop类来解决。第二个问题我们要拆分一下,首先我们要先知道我们变量对应的UI位置在哪里和大小是多大,然后我们再解决获取鼠标坐标的问题。最后我们只要知道一下何时算用户结束操作。这样我们就可以解决第二个问题了。

        我们先来解决第一个问题。在GUILayoutUtility类中有一个函数GetLastRect,它会返回Rect。我们可以通过这个来得到UI的大小和位置。但是我们要注意一点就是这个函数只有在触发EventType.Repaint事件后才有用,否则我们得到的就是错误的信息。

        要获取用户输入事件,我们可以使用Event。所以我们就能够知道何时用户输入开始,何时用户输入结束。并且通过它,我们还可以知道鼠标的位置。这样我们所有的问题都解决了。我这边也给出一个示例,具体代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace Blog
{
public class EditorBlog : EditorWindow
{
[MenuItem("Blog/Unity编辑器扩展的代码总结")]
static void ShowWindow()
{
var win = EditorWindow.GetWindow<EditorBlog>();
win.titleContent = new GUIContent("Test");
}

[Serializable]
public class SelfClass
{
public string name;
public Texture tex;
}

[SerializeField]
private SelfClass selfparam;
[SerializeField]
private List<SelfClass> list;

private SerializedObject serObj;
private SerializedProperty listPro;
private Rect rect;

private void OnEnable()
{
serObj = new SerializedObject(this);
listPro = serObj.FindProperty("list");
list = new List<SelfClass>();
}

private void OnGUI()
{
serObj.Update();
var curPos = Event.current.mousePosition;
// 判断是否拖拽到了我们想要的区域
if ((Event.current.type == EventType.DragUpdated || Event.current.type == EventType.DragExited) && rect.Contains(curPos))
{
// 改变鼠标样式
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
// 判断玩家是否进行操作
if (Event.current.type == EventType.DragExited)
{
// 获取我们想要的信息
foreach(var i in DragAndDrop.objectReferences)
{
if(i.GetType() == typeof(Texture2D))
{
var item = new SelfClass();
item.name = i.name;
item.tex = i as Texture2D;
list.Add(item);
}
}
}
}
EditorGUILayout.PropertyField(listPro);
if(Event.current.type == EventType.Repaint)
{
rect = GUILayoutUtility.GetLastRect();
}
serObj.ApplyModifiedProperties();
}
}
}

这里我使用了EventType.DragUpdatedEventType.DragExited来判断用户的输入。显然当触发了EventType.DragExited时,用户输入结束。我也就在这个时候对用户选中的物体进行判断。通过DragAndDrop.objectReferences,我们可以拿到用户选中的物体。然后我判断其类型是否为我想要的,最后进行加入操作。这里你可能有一些疑问,为什么我要将事件判断放到EditorGUILayout.PropertyField前面?这是因为如果你不放在它前面的话,EditorGUILayout.PropertyField会将EventType.DragExited事件使用掉。这样事件到其执行后就变成了EventType.Used。这样我们判断不了用户何时结束操作,所以我将这个判断提到EditorGUILayout.PropertyField前面。这里我还做了改变鼠标样式的操作即这段代码:

1
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;

其文档为:DragAndDrop.visualModeDragAndDropVisualMode

Selection

        如果你有在网上找过关于这些Unity编辑器扩展获取拖拽的信息的话,你就会看到关于Selection去获取拖拽的信息。但我个人没搞懂这个Selection和DragAndDrop间的区别。我使用下来感觉差不多。如果你在使用的过程中发现了DragAndDrop有问题,那么你可以去试试看Selection。

Unity编辑器扩展使用协程

        协程其实是一个很常见的功能。而协程也可以在编辑器扩展中进行使用(在我使用的Unity版本中Editor Coroutines插件是直接安装好了,但是我不确定其他的版本也会安装好这个插件)。其官方文档链接:Editor Coroutines。下面我就简单做一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void OnGUI()
{
if(GUILayout.Button("开始协程"))
{
EditorCoroutineUtility.StartCoroutine(TestFun(),this);
}
}

private IEnumerator TestFun()
{
int count = 0;
while(count < 5)
{
Debug.Log("现在的数量为:" + count);
yield return new EditorWaitForSeconds(1f);
count++;
}
yield break;
}

这里需要注意一下,如果你使用的是WaitForSeconds,那么这个会不起作用的。所以我们只能使用EditorWaitForSeconds

Unity编辑器进度条

        有了协程了,我们就可以来制作编辑器进度条了。虽然不用协程也可以做,但是我认为用协程会方便一些。以下是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void OnGUI()
{
if(GUILayout.Button("开始协程"))
{
EditorCoroutineUtility.StartCoroutine(TestFun(),this);
}
}

private IEnumerator TestFun()
{
int count = 0;
while(count < 5)
{
// 显示进度条
EditorUtility.DisplayProgressBar("测试", "现在的数量为:" + count,count * 1.0f / 5);
yield return new EditorWaitForSeconds(1f);
count++;
// 清除进度条
EditorUtility.ClearProgressBar();
}
yield break;
}

有关进度条的更多信息可以查阅:EditorUtilityEditorUtility.DisplayProgressBarEditorUtility.ClearProgressBar

提示

        关于做提示,你可以直接使用Debug来给出提示。但是我们既然都做编辑器扩展了,那还是看看编辑器相关的提示API。这其实可以说的不多,所以我就展示一下相关的API和其样子。

帮助盒子

1
EditorGUILayout.HelpBox("这是一条提示信息", MessageType.Info);

文档链接:EditorGUILayout.HelpBoxMessageType

提示窗口

1
2
3
4
5
6
7
private void OnGUI()
{
if(GUILayout.Button("点击提示"))
{
EditorUtility.DisplayDialog("提示窗口", "提示信息", "提示的应答");
}
}

文档链接:EditorUtility.DisplayDialog。除了这种提示窗口外,还有其他的提示窗口。但我认为这种应该最常用,所以我只展示了这个。更多信息可以查阅EditorUtility的官方说明。

编辑器对GameObject进行操作

        我们有时候也需要对场景中的对象进行操作。那么如何获取到场景中的对象就是我们要解决的问题了。一般而言我们选取对象给窗口都是使用拖拽的方法。那么之前的提到的SelectionDragAndDrop都是可以的。但是我这里更加推荐使用Selection。因为它可以不用拖拽,你可以直接选中这些物体然后点击编辑器扩展窗口中的按钮。

        这里我们仍然会遇到一个问题。我们其实可以在选中Scene下的物体同时选中Asset下的物体。那么我们就要分别一下哪些是Scene下的物体。你在Selection可以找到这样的API:Selection.GetFilteredSelectionMode.Assets。你或许觉得用他们就可以实现我们想要的功能了。但是如果你选中的是Assets文件夹下的预制体,你用这样的组合仍然会将这个预制体计算进去。经过一些尝试,我找到了下面这样的方法来判断我们选中的物体是否是在场景下的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void OnGUI()
{
if(GUILayout.Button("显示选择"))
{
var objs = Selection.GetFiltered<GameObject>(SelectionMode.Unfiltered);
List<GameObject> lst = new List<GameObject>();
foreach(GameObject obj in objs)
{
// 判断是否是有效场景
if(obj.scene.IsValid())
{
lst.Add(obj);
}
}
foreach(var i in lst)
{
Debug.Log(i.name);
}
}
}

创建空物体

        创建空物体的方法很简单就是下面这段代码:

1
new GameObject();

新创建空对象会直接在场景中。这段代码虽然简单,但是网上一直没有说明的文章。我也是找了很久没有文章后试了一下才发现了,所以我就做一个记录。

关于预制体

        Unity有提供一个处理预制体的类:PrefabUtility。我个人觉得这个没什么可以说明的,至少我现在的需求也没有遇到什么坑点。如果大家遇到需要处理预制体的时候可以去查看一下这个类的API。

关于资源加载和保存

        一般而言,我们对一个资源的修改只要调用AssetDatabase.Refresh函数就可以了。但是最近我在帮同事做一些工具的时候,我发现我使用代码创建出来的资源在修改后无法被保存。即使我使用了上面的函数也不可以。

        虽然最终我没有找到原因,但是我找到了另外的方法让我的修改后的资源被强制保存。我们先对我们想要保存的资源对象使用EditorUtility.SetDirty方法,然后我们在使用AssetDatabase.SaveAssetIfDirty保存。