UEDL7——Inspector窗口拓展
UEDL7——Inspector窗口拓展
本章代码关键字
1 | [//如要让类为另外一个类拓展在Inspector窗口的显示,需要添加该特性 ] |
Inspector窗口自定义显示
我们可以完全自定义某一个脚本在Inspector窗口的相关显示
我们为继承Editor
的脚本,添加[CustomEditor(typeof(想要自定义Inspector窗口的脚本))]
特性
在该脚本中按照一定的规则进行编写,便可为Inspector窗口中的某个脚本自定义窗口布局
[CustomEditor()]
特性还可用于拓展Scene窗口的显示,具体可看:Handles
SerializedObject和SerializedProperty的作用
SerializedObject
和 SerializedProperty
主要用于在 Unity 编辑器中操作和修改序列化对象的属性。
它们通常在自定义编辑器中使用,以创建更灵活、可定制的属性面板
我们只需要记住简单的规则:
-
SerializedObject
代表被拓展的脚本对象,我们可以从该属性获取被拓展的脚本对象中的属性,或者修改其在Inspector窗口的显示内容 -
SerializedProperty
代表被拓展的脚本对象中的属性(成员变量),我们可以通过SerializedProperty
来修改被拓展的脚本对象中的属性,
也需要通过这个序列化属性,来自定义被拓展的脚本对象的属性在Inspector窗口上的显示
关于它们的详细内容:
-
SerializedObject
的详细内容:SerializedObject - Unity 脚本 API -
SerializedProperty
的详细内容:SerializedProperty - Unity 脚本 API
自定义脚本在Inspector窗口中显示的内容
关键步骤:
-
单独为某一个脚本实现一个自定义脚本,并且脚本需要继承
Editor
一般该脚本命名为 自定义脚本名 +Editor
假设我们要对下面这个脚本进行Inspector窗口的拓展:
1
2
3
4
5
6
7
8using UnityEngine;
public class Lesson22 : MonoBehaviour
{
public int atk; //攻击力
public float def; //防御力
public GameObject obj; //敌对目标对象依附的GameObject
} -
在该脚本前加上特性
- 命名空间:
UnityEditor
- 特性名:
[CustomEditor(想要自定义脚本类名的Type)]
因此,声明一个这样的类,继承
Editor
并加上特性[CustomEditor()]
1
2
3
4using UnityEditor;
[ ]
public class Lesson22Editor : Editor { } - 命名空间:
-
声明对应
SerializedProperty
序列化属性 对象,主要通过它和自定义脚本中的成员进行关联可以利用继承
Editor
后的父类成员serializedObject
中的FindProperty("成员变量名")
方法关联对应成员;
比如:SerializedProperty mySerializedProperty;
mySerializedProperty = serializedObject.FindProperty("自定义脚本中的成员名");
,一般在OnEnable
函数中初始化因此,对于
Lesson22
这个脚本的成员关联写法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16using UnityEditor;
[ ]
public class Lesson22Editor : Editor
{
private SerializedProperty atk;
private SerializedProperty def;
private SerializedProperty obj;
private void OnEnable()
{
atk = serializedObject.FindProperty("atk");
def = serializedObject.FindProperty("def");
obj = serializedObject.FindProperty("obj");
}
} -
重写
OnInspectorGUI()
函数,该函数控制了Inspector窗口中显示的内容
只需要在其中重写绘制逻辑,便可以自定义窗口,重写的逻辑和编写编辑器窗口逻辑很相似注意:重写的绘制各个控件的逻辑需要包裹在下面两句代码之间
更新序列化对象的表示形式serializedObject.Update();
,在调用该方法后就要编写自定义的Inspector窗口上显示的逻辑
应用属性修改serializedObject.ApplyModifiedProperties();
,执行了自定义的Inspector窗口上显示的逻辑后,最后就要执行该方法例如我们要使用各种
EditorGUI
的控件去控制脚本的属性: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
36using UnityEditor;
using UnityEngine;
[ ]
public class Lesson22Editor : Editor
{
private SerializedProperty atk;
private SerializedProperty def;
private SerializedProperty obj;
private bool foldOut;
private void OnEnable()
{
atk = serializedObject.FindProperty("atk");
def = serializedObject.FindProperty("def");
obj = serializedObject.FindProperty("obj");
}
public override void OnInspectorGUI()
{
//base.OnInspectorGUI(); //这是原本的绘制脚本在Inspector窗口内的方法
serializedObject.Update();
//在这里写自定义Inspector窗口的内容
foldOut = EditorGUILayout.BeginFoldoutHeaderGroup(foldOut, "基础属性");
if (foldOut)
{
GUILayout.Button("测试自定义Inspector窗口");
EditorGUILayout.IntSlider(atk, 0, 100, "攻击力");
def.floatValue = EditorGUILayout.FloatField("防御力", def.floatValue);
EditorGUILayout.ObjectField(obj, new GUIContent("敌对对象"));
}
EditorGUILayout.EndFoldoutHeaderGroup();
serializedObject.ApplyModifiedProperties();
}
}
显示效果:
获取脚本依附的对象(被拓展显示内容的脚本对象)
注意:此处的target
成员得到的并不是依附的GameObject
对象,而是被拓展显示的组件对象
这里的target
其实是依附在GameObject
上的组件对象
1 | public class Lesson22Editor : Editor |
按下"测试自定义Inspector窗口"
按钮后,输出的内容:
数组和List属性的显示方式
如果想要自定义数组和List属性在Inspector窗口上的显示,就需要在获取到自定义数组和List属性的同时,
获取其中所有的元素的序列化属性,这样,我们才可以自定义数组或者列表属性中的各个元素的显示
同时,我们可能还需要根据用户在Inspector窗口上的输入,动态的调整数组和List属性的元素数量
数组、List属性在Inspector窗口显示的基础方式
主要知识点:EditorGUILayout.PropertyField(SerializedProperty对象, 标题);
,该API会按照属性类型自己去处理控件绘制的逻辑,
- 参数一:要显示的
SerializedProperty
对象 - 参数二:
GUIContent
标题
对于如何绘制自定义属性 ——> 自定义类属性在Inspector窗口显示的基础方式
假设我们要在Inspector窗口上显示如下的属性:
1 | public class Lesson22 : MonoBehaviour |
我们就需要在对应的显示拓展类里这样写:
1 | using UnityEditor; |
显示效果如下:
数组、List属性在Inspector窗口显示的自定义方式
如果我们不想要Unity默认的绘制方式去显示数组、List
相关内容,想要使用自己的逻辑绘制各个元素
我们也可以完全自定义布局方式
主要知识点:利用SerializedProperty
中数组相关的API来完成自定义
-
arraySize
获取序列化属性对应的数组或List
实际容量在遍历各个元素时,需要通过这里获取的实际容量,判断数组或者列表是否需要扩容,或者判断缩减数组或者列表多少元素
1
count = listObjs.arraySize;
-
InsertArrayElementAtIndex(索引)
为数组或者列表在指定索引插入默认元素(容量arraySize
会变化)我们通过这个方法来为序列化属性对应的数组或者列表扩充容量
1
2if (listObjs.arraySize <= i)
listObjs.InsertArrayElementAtIndex(i); -
DeleteArrayElementAtIndex(索引)
为数组或者列表在指定索引删除元素(容量arraySize
会变化)我们通过这个方法来为序列化属性对应的数组或者列表缩减容量
1
2
3
4for (int i = listObjs.arraySize - 1; i >= count; i--)
{
listObjs.DeleteArrayElementAtIndex(i);
} -
GetArrayElementAtIndex(索引)
获取数组或者列表中指定索引位置的SerializedProperty
对象通过该方法,我们可以获取到数组或者列表内的元素,我们可以通过获取到的元素来绘制自定义控件,
使得各个数组或者列表的各个元素在Inspector窗口上的显示可以更加的自定义化1
2
3
4
5
6//根据容量绘制需要设置的每一个索引位置的对象
for (int i = 0; i < count; i++)
{
SerializedProperty indexProperty = listObjs.GetArrayElementAtIndex(i);
EditorGUILayout.ObjectField(indexProperty, new GUIContent($"索引{i}"));
}
假设我们要让listObjs
在Inspector窗口上这样显示:
List容量,数组或列表内的各元素的显示使用IntField
或者objectField
控件显示
修改List容量可以修改元素控件绘制的数量
我们需要这样实现:
- 要显示List容量,我们需要使用
EditorGUILayout.IntField
来绘制一个输入整数控件,并配套一个count
变量 - 每次在Inspector窗口上显示脚本内容时,都需要初始化
count
变量,使用arraySize
这个数组或者列表的实际容量来赋值
否则当我们重新显示脚本内容时,就会因为count
的值被清零,而导致实际存在的元素被隐藏,不被绘制出来 - 当我们将List容量的数字改小时,我们需要从尾遍历List,调用
DeleteArrayElementAtIndex
将那些多余的元素删除掉
直到arraySize
的值等于List容量设置的count
值,这样就实现了修改List容量来缩减数组或者列表容量 - 绘制List容量控件
- 由于最开始通过
serializedObject
得到的数组和list元素数量是0,因此直接根据count
变量遍历会抛出空引用错误
因此当arraySize
的值小于count
变量值时,我们需要进行扩容,从count
开始,调用InsertArrayElementAtIndex
添加元素
这样我们同时实现了修改List容量来扩充数组或者列表容量 - 通过
count
变量,遍历获取列表内各个元素的SerializedProperty
对象,根据这个对象来绘制控件
1 | private SerializedProperty listObjs; |
我们可以根据以上代码,修改元素控件的绘制逻辑,达到自定义数组和List显示方式的目的
自定义类属性的显示方式
这里的自定义类属性,是指的是用户自己声明的类对应的变量的显示方式
如果想要自定义类属性在Inspector窗口上的显示,就需要在获取到自定义类属性的同时,
获取该类对象所有的属性(成员变量)的序列化属性,这样,我们才可以自定义数组或者列表属性中的各个元素的显示
自定义类属性在Inspector窗口显示的基础方式
主要知识点:
EditorGUILayout.PropertyField(SerializedProperty对象, 标题);
该API会按照属性类型自己去处理控件绘制的逻辑
假设我们要在Inspector窗口上显示如下的属性:
1 | [ ] |
我们就需要在对应的显示拓展类里这样写:
1 | using UnityEditor; |
显示效果如下:
自定义类属性在Inspector窗口显示的自定义方式
如果我们不想要Unity默认的绘制方式去显示 自定义数据结构类 相关内容,想要通过自己的逻辑来绘制自定义数据结构类的显示
我们也可以完全自定义布局方式
主要知识点:
-
SerializedProperty.FindPropertyRelative("属性")
:通过属性的序列化对象来寻找相对属性 -
serializedObject.FindProperty("属性.子属性")
:直接获取属性的子属性
以上两个方法都可以获取某个自定义类属性的子属性的序列化对象,
只是一个需要先获取自定义类属性,另一个可以直接通过serializedObject
获取
得到自定义类属性的子属性的序列化对象后,我们就可以自己编写绘制逻辑,达到自定义显示的效果
假设我们还是要在Inspector窗口上显示如下的属性:
1 | [ ] |
我们就需要在对应的显示拓展类里这样写(两种方法都使用):
1 | using UnityEditor; |
显示效果如下:
字典属性的显示方式
[SerializeField]
特性
让私有字段可以被序列化(能够在Unity的Inspector窗口被看到),回顾请看:[SerializeField]
在Inspector窗口编辑字典成员
Unity默认是不支持Dictionary
在Inspector窗口被显示的,我们只有利用两个List
(或数组)成员来间接设置Dictionary
ISerializationCallbackReceiver接口
ISerializationCallbackReceiver
接口是Unity提供的用于序列化和反序列化时执行自定义逻辑的接口
实现该接口的类能够在对象被序列化到磁盘或从磁盘反序列化时执行一些额外代码
接口中包括如下函数:
-
OnBeforeSerialize()
:在对象被序列化之前调用,当Inspector窗口上显示类对象的属性时,就会每帧执行序列化操作 -
OnAfterDeserialize()
:在对象从磁盘反序列化后调用,当我们通过Inspector窗口修改类对象的属性时,就会执行反序列化操作
由于我们需要用两个List
存储Dictionary
的具体值,相当于字典中的真正内容是存储在两个List
中的,所以我们需要在
-
OnBeforeSerialize()
序列化之前:将Dictionary
里的数据存入List
中进行序列化 -
OnAfterDeserialize()
反序列化之后:将List
中反序列化出来的数据存储到Dictionary
中
假设要让被拓展Inspector显示的脚本中的字典属性与两个列表之间相互关联,就需要继承该接口并实现方法,方法内关联列表与字典
1 | using System; |
以上的代码就将keys
和values
这两个列表与myDic
这个字典关联起来了,具体关联的原理是:
在Inspector窗口上显示的字典相关内容,实际上读取和修改的是keys
和values
这两个列表的序列化数据,因此:
- 当在Inspector窗口上修改
keys
和values
这两个列表的数据时,或者运行Unity程序时,会触发反序列化,
反序列化后会执行OnAfterDeserialize()
,此时就会将两个列表的反序列化出来的数据,写入到字典内 - Inspector窗口上显示该类的对象时,会执行序列化,序列化前会执行
OnBeforeSerialize()
将字典内存储的数据写入到两个列表内,这样字典的数据就可以通过列表序列化存储起来
这样,不可序列化的字典就可以通过两个可序列化的列表,存储数据,在Inspector窗口上显示,或者通过Inspector窗口修改内容
接下来,我们只需要在实现自定义显示的类中根据这两个List
的序列化属性,自定义显示方式,使其看起来像是在编辑字典
利用两个List在Inspector窗口中自定义Dictionary显示
由于我们在Inspector窗口中显示的信息的数据来源是两个List
因此我们只需要利用List
在Inspector窗口中自定义显示的内容即可
与自定义List
在Inspector窗口上的显示相比,
与字典相关联的两个List
只需要一个int
变量表示长度即可,当int
变量变化时,需要同时修改两个List的元素
再根据两个List的元素,绘制显示键值对的控件
1 | using PlasticPipe.Certificates; |
显示效果如下: