Unity编辑器扩展的代码总结

前言

        最近我写了些Unity相关的工具,比如打图集、设定多语音等问题。我想着趁我现在还记得自己在扩展中遇到的问题,我就写一篇文章来记录一下。

环境

    Window10系统
    Unity2022.3.8f1c1

EditorWindow

        Unity中其实有很多方法可以生成一个编辑器窗口,然后我们就按照这些方法的规则去实现我们想要的编辑器扩展。但这其中我觉得继承EditorWindow去实现是很好用的一个方法。EditorWindow官方文档中是这样描述的:从此类派生以创建编辑器窗口。创建自己的自定义编辑器窗口,这些窗口可以自由浮动,也可以作为选项卡停靠,就像 Unity 界面中的原生窗口一样。而且其本身的使用并不困难,在官方的文档中也存在一个使用示例。

        在实现我们想法的过程中,我们会经常用到如EditorGUILayoutEditorGUIEditorGUIUtilityGUIGUILayoutGUIUtility,在OnGUI函数中。

关于布局

        在UI布局方面,EditorWindow默认采用垂直布局。当然我们也可以使用代码来更改布局。比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void OnGUI()
{
// 默认下的垂直布局
EditorGUILayout.LabelField("现在是垂直布局");
EditorGUILayout.LabelField("现在还是垂直布局");

// 使用代码切换为水平布局
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("现在是水平布局了");
EditorGUILayout.LabelField(",现在还是水平布局了");
EditorGUILayout.EndHorizontal(); // BeginHorizontal和EndHorizontal必是成对出现的

EditorGUILayout.LabelField("现在是垂直布局");
EditorGUILayout.LabelField("现在还是垂直布局");
}

其结果如下:

上面我们通过BeginHorizontal来决定后面的UI布局是水平布局,然后再调用EndHorizontal来结束水平布局。如果我们还想在水平布局中加入垂直布局,那么我们可以调用BeginVertical开始垂直布局,然后再使用EndVertical来结束。(这里我只给出了EditorGUILayout的链接,但是其实GUILayout也有这样的功能。)

窗口本身的修改

        在官方的示例中,打开窗口的代码如下:

1
2
3
4
5
6
[MenuItem("Window/My Window")]
static void Init()
{
MyWindow window = (MyWindow)EditorWindow.GetWindow(typeof(MyWindow));
window.Show();
}

但其实我们不调用Show函数也是可以打开窗口的。对于打开的窗口,我们可以使用一些API来修改窗口的标题和大小。比如:titleContent就可以修改窗口的标题,maxSize可以定义窗口最大的大小,等等。具体大家可以查看一下EditorWindow,文档中都有对应的说明。

一些简单的UI

        Unity本身已经帮我们封装了一些UI,我们可以通过这些UI来实现一些功能。比如上面的代码中标签:LabelField;还有我们经常用到的文本输入:TextArea,等等。大多数GUILayout可以实现的,EditorGUILayout也可以实现。除了普通按钮以外。对于普通按钮,我们仍要使用GUILayout.Button去实现一个普通按钮。

        很多实现UI的API中,最后都会带上一个GUILayoutOption[] options这样的参数。当然我们可以不传任何的信息。但是如果你觉得默认效果不好看,那么你就要使用这个来做处理。在GUILayoutOption官方文档中,它也有说明传入怎样的参数。大家看一下文档就好了。

显示我们的变量

        我们在窗口中建立一个输入,本质上我们是想获取到这些输入然后传到我们的变量中。而下面的代码便可以实现我们这样的要求:

1
2
3
4
5
6
private string val;

private void OnGUI()
{
val = EditorGUILayout.TextField(val);
}

我设定了一个string类型名为val的变量,然后我创建了一个文本来获取到用户的输入。如果仅仅是string类型自然简单,但是如果我们要的是int类型呢?那我们不是还要针对用户的输入进行判断来预防一些非数字的输入。针对这样的情况,Unity封装了一些API来帮助我们实现值获取。

        我们可以使用IntFieldDoubleFieldVector2Field等来帮助我们实现我们想要的变量输入(更多信息请移步EditorGUILayout官方文档)。虽然Unity提供了很多的封装,但是它不能完全满足我们个人的需求。比如你不能找到任何一个API调用后直接就帮我们弄好了一个List类型的输入。如果我自定义了一个类,那么Unity又怎么会特别提供一个API去显现我自定义的类呢?

自创类的展示方法

        其实Unity还提供了另外一种显示变量的方法,来满足那些由我们自定义出来了类。这个方法本身有一个限制,那就是类型本身可支持序列化。简单点来说就是当我们写脚本时,那些添加[SerializeField]后可以在Inspector窗口中显示的那些类型才可以支持(所以Dictionary类型是不受支持的)。虽然其本身仍然有所限制,但是他已经能完成我们打部分的需求了。具体代码如下:

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

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

private SerializedObject serObj;
private SerializedProperty selfparamPro;
private SerializedProperty listPro;

private void OnEnable()
{
serObj = new SerializedObject(this);
selfparamPro = serObj.FindProperty("selfparam");
listPro = serObj.FindProperty("list");
}

private void OnGUI()
{
serObj.Update();
EditorGUILayout.PropertyField(selfparamPro);
EditorGUILayout.PropertyField(listPro);
serObj.ApplyModifiedProperties();
}
}
}

运行结果如下:

        SerializedObjectSerializedProperty需要只初始化一次。所以我这边放在了OnEnable这个特殊的函数中,你可以理解它的作用和脚本生命周期里的OnEnable函数一致。我们要先初始化SerializedObject,然后再使用SerializedObject.FindProperty让SerializedProperty和我们的定义的变量对应上。

        在OnGUI函数中,我们要先使用SerializedObject.Update去更新序列化对象,而在末尾时要使用SerializedObject.ApplyModifiedProperties让我们的输入得以传给变量。而这些变量的显示就要使用EditorGUILayout.PropertyField

变量过多导致UI占用窗口过长的解决方案

        我本来认为这样实现的效果和脚本在Inspector窗口里的差不多。特别是像List这样,一个变量会有很多的子变量。当其超过某个数量的时候就会出现滚轮。但是在编辑器扩展中,类似List这样的变量它只会不断的扩大。这样导致了你想操作某个List元素时,你只能扩大窗口然后得到后面的元素。可是窗口的大小一般不足以展示超过100个的变量。我在网上找了很久都没有解决的方法。所以我自己就想到了下面这样的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Vector2 pos;

private void OnGUI()
{
serObj.Update();
var h = EditorGUI.GetPropertyHeight(listPro);
if(h > 200)
{
pos = EditorGUILayout.BeginScrollView(pos,GUILayout.MaxHeight(200));
EditorGUILayout.PropertyField(listPro);
EditorGUILayout.EndScrollView();
}
else EditorGUILayout.PropertyField(listPro);
serObj.ApplyModifiedProperties();
}

我们可以使用EditorGUI.GetPropertyHeight来获取到属性区域的高度。当这个高度超过某个值时,我们就使用一个滚轮区将其包围起来。这里所用到的API为EditorGUILayout.BeginScrollViewEditorGUILayout.EndScrollView。因为我只想这个属性高度占据200,所以我给滚轮区设定的最大高度也是200。不然滚轮区会一致延伸到窗口底部。

自定义变量显示的效果

        在项目开发中,我就遇到一个效果是使用Unity自身的API完不成的。所以这就需要我们自己去做这种变量的UI显示。其核心就是PropertyDrawer这个类。这里我找到一个博主写的文章,大家直接看他的文章就好了。文章链接。文章中出现的Unity的API所对应的相关文档PropertyAttributePropertyDrawerCustomPropertyDrawer。这个文章是教如何自定义变量的名称显示。但是其本身也可以改为自定义类型显示,就像是这个PropertyDrawer文档中显示描述的那样。

        为了方便我自己查阅,这里我将代码贴上。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;

namespace SelfEditorTool
{
/// <summary>
/// 使字段在Inspector中显示自定义的名称。
/// </summary>
public class CustomLabelAttribute : PropertyAttribute
{
public string name;

/// <summary>
/// 使字段在Inspector中显示自定义的名称。
/// </summary>
/// <param name="name">自定义名称</param>
public CustomLabelAttribute(string name)
{
this.name = name;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEditor;
using UnityEngine;

namespace SelfEditorTool
{
/// <summary>
/// 定义对带有 `CustomLabelAttribute` 特性的字段的面板内容的绘制行为。
/// </summary>
[CustomPropertyDrawer(typeof(CustomLabelAttribute))]
public class CustomLabelDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
label.text = (attribute as CustomLabelAttribute).name;
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
EditorGUI.PropertyField(position, property, GUIContent.none);
}
}
}

使用:

1
2
3
4
5
6
7
private class Data
{
[CustomLabel("数据类型")]
public DataType dataType;
[CustomLabel("数据信息")]
public string dataInfo;
}