Unity提供了灵活多变的编辑器拓展API接口,通过代码反射,可以修改一些系统自带的编辑器窗口。此外,丰富的EditorGUI接口也可以拓展出各式各样的编辑器窗口。
拓展Project视图
属于编辑模式下的代码,需要放在Editor文件夹下;属于运行时执行的代码,放在任意非Editor文件夹下即可。Editor文件夹的位置比较灵活,它还可以作为多个目录的子文件夹存在,这样开发者就可以按功能来划分,将不同功能的编辑代码放在不同的Editor目录下。例如可以有多个Editor目录,它们各自处理各自的逻辑。
拓展右键菜单
选中资源1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17using UnityEngine;
using UnityEditor;
public class Script_03_01
{
[ ]
static void MyTools1()
{
Debug.Log(Selection.activeObject.name);
}
[ ]
static void MyTools2()
{
Debug.Log(Selection.activeObject.name);
}
}
Create按钮1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17using UnityEngine;
using UnityEditor;
public class Script_03_02
{
[ ]
static void CreateCube()
{
GameObject.CreatePrimitive(PrimitiveType.Cube);
}
[ ]
static void CreateSphere()
{
GameObject.CreatePrimitive(PrimitiveType.Sphere);
}
}
拓展布局
在右侧拓展自定义按钮,在代码中既可以设置拓展按钮的区域,也可监听按钮的点击事件。
在方法前面添加[InitializeOnLoadMethod]表示此方法会在C#代码每次编译完成后首先调用。监听EditorApplication.projectWindowItemOnGUI委托,即可使用GUI方法来绘制自定义的UI元素。GUI还提供了丰富的元素接口,可以用来添加文本、图片、滚动条和下拉框等复杂元素。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
29using UnityEngine;
using UnityEditor;
public class Script_03_03
{
[ ]
static void InitializeOnLoadMethod()
{
EditorApplication.projectWindowItemOnGUI = delegate (string guid, Rect selectionRect)
{
// 在Project视图中选择一个资源
if (Selection.activeObject && guid == AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(Selection.activeObject)))
{
// 设置拓展按钮区域
float width = 50f;
selectionRect.x += (selectionRect.width - width);
selectionRect.y += 2f;
selectionRect.width = width;
GUI.color = Color.red;
// 点击事件
if (GUI.Button(selectionRect, "click"))
{
Debug.LogFormat("click : {0}", Selection.activeObject.name);
}
GUI.color = Color.white;
}
};
}
}
监听事件
1 | using UnityEngine; |
拓展Hierarchy视图
Hierarchy视图中出现的都是游戏对象,这些对象之间同样具有一定的关联关系。Hierarchy视图中的游戏对象会通过摄像机最终投影在发布的游戏中。
拓展菜单
1 | using UnityEngine; |
拓展布局
1 | using UnityEngine; |
重写菜单
1 | using UnityEngine; |
在上述代码中,使用Event.current来获取当前的事件。当监听到鼠标抬起的事件后,并且满足游戏对象的选中状态,开始执行自定义事件。其中,EditorUtility.DisplayPopupMenu用于弹出自定义菜单,Event.current.Use()的含义是不再执行原有的操作,所以就实现了重写菜单。
此外,Hierarchy视图还可以重写系统自带的菜单行为。例如,觉得Unity创建的Image组件不好,可以复写它的行为。创建Image组件时,会自动勾选RaycastTarget。如果图片不需要处理点击事件,这样会带来一些额外的开销。下面的代码让RaycastTarget默认不勾选。由于重写了菜单,所以需要通过脚本自行创建Image对象和组件。接着,获取到image组件对象,直接设置它的raycastTarget属性即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
public class Script_03_08
{
[ ]
static void CreateImage()
{
if (Selection.activeTransform)
{
if (Selection.activeTransform.GetComponentInParent<Canvas>())
{
Image image = new GameObject("image").AddComponent<Image>();
image.raycastTarget = false;
image.transform.SetParent(Selection.activeTransform, false);
// 设置选中状态
Selection.activeTransform = image.transform;
}
}
}
}
拓展Inspector视图
拓展源生组件
摄像机就是典型的源生组件。可以在摄像机组件的最上面添加一个按钮。它的局限性就是拓展组件只能加在源生组件的最上面或者最下面。1
2
3
4
5
6
7
8
9
10
11
12
13using UnityEngine;
using UnityEditor;
[ ]
public class Script_03_09 : Editor
{
public override void OnInspectorGUI()
{
if (GUILayout.Button("拓展按钮"))
{ }
base.OnInspectorGUI();
}
}
拓展继承组件
1 | using UnityEngine; |
组件不可编辑
1 | using UnityEngine; |
1 | using UnityEngine; |
Context菜单
1 | using UnityEngine; |
1 | using UnityEngine; |
拓展Scene视图
辅助元素
1 | using UnityEngine; |
辅助UI
1 | using UnityEngine; |
常驻辅助UI
1 | using UnityEngine; |
禁用选中对象
1 | using UnityEngine; |
拓展Game视图
MenuItem菜单
面板拓展
脚本挂在游戏对象上时,右侧会出现它的详细信息面板,这些信息是根据脚本中声明的public可序列化变量而来的。此外,也可以通过EditorGUI来对它进行绘制,让面板更具可操作性。
Inspector面板
EditorGUI和GUI的用法几乎完全一致,目前来说前者多用于编辑器开发,后者多用于发布后调试编辑器。总之,它们都是起辅助作用的。EditorGUI提供的组件非常丰富,常用的绘制元素包括文本、按钮、图片和滚动框等。做一个好的编辑器,是离不开EditorGUI的。如下图所示,将EditorGUI拓展在Inspector面板上了。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
38using UnityEngine;
using UnityEditor;
using UnityEditor.Experimental.UIElements;
using UnityEngine.Experimental.UIElements;
using UnityEngine.Experimental.UIElements.StyleEnums;
public class Script_03_33 : EditorWindow
{
[ ]
public static void ShowExample()
{
Script_03_33 window = GetWindow<Script_03_33>();
window.titleContent = new GUIContent("Script_03_33");
}
public void OnEnable()
{
var root = this.GetRootVisualContainer();
// 添加style.uss样式
root.AddStyleSheetPath("style");
var boxes = new VisualContainer();
// 设置自动换行
boxes.style.flexDirection = FlexDirection.Row;
boxes.style.flexWrap = Wrap.Wrap;
for (int i=0;i<20;i++)
{
TextField m_TextField = new TextField();
boxes.Add(m_TextField);
Button button = new Button(delegate () {
Debug.LogFormat("Click");
});
button.text = "我是按钮我要自适应";
boxes.Add(button);
}
root.Add(boxes);
}
}
在上述代码中,将脚本部分和Editor部分的代码合在一个文件中。如果需要拓展的面板比较复杂,建议分成两个文件存放,一个是脚本,另一个是Editor脚本。
EditorWindows窗口
Unity提供编辑器窗口,开发者可以自由拓展自己的窗口。Unity编辑器系统自带的视图窗口其实也是用EditorWindows实现的。如下图所示,它绘制元素时同样适用EditorGUI代码。
使用EditorWindow.GetWindow()方法即可打开自定义窗口,在OnGUI()方法中可以绘制窗口元素。注意代码中EditorWindos窗口的生命周期。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
67using UnityEngine;
using UnityEditor;
public class Script_03_24 : EditorWindow
{
[ ]
static void Init()
{
Script_03_24 window = (Script_03_24)EditorWindow.GetWindow(typeof(Script_03_24));
window.Show();
}
private Texture m_MyTexture = null;
private float m_MyFloat = 0.5f;
private void Awake()
{
Debug.LogFormat("窗口初始化时调用");
m_MyTexture = AssetDatabase.LoadAssetAtPath<Texture>("Assets/unity.png");
}
private void OnGUI()
{
GUILayout.Label("Hello World!!", EditorStyles.boldLabel);
m_MyFloat = EditorGUILayout.Slider("Slider", m_MyFloat, -5, 5);
GUI.DrawTexture(new Rect(0, 30, 100, 100), m_MyTexture);
}
private void OnDestroy()
{
Debug.LogFormat("窗口销毁时调用");
}
private void OnFocus()
{
Debug.LogFormat("窗口拥有焦点时调用");
}
private void OnHierarchyChange()
{
Debug.LogFormat("Hierarchy视图发生改变时调用");
}
private void OnInspectorUpdate()
{
//Debug.LogFormat("Inspector每帧更新");
}
private void OnLostFocus()
{
Debug.LogFormat("失去焦点");
}
private void OnProjectChange()
{
Debug.LogFormat("Project视图发生改变时调用");
}
private void OnSelectionChange()
{
Debug.LogFormat("在Hierarchy或者Project视图中选择一个对象时调用");
}
private void Update()
{
// Debug.LogFormat("每帧更新");
}
}
EditorWindows下拉菜单
在EditorWindows编辑窗口的右上角,有个下拉菜单,也可以对该菜单中的选项进行拓展,不过这里需要实现IHasCustomMenu接口。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
31using UnityEngine;
using UnityEditor;
public class Script_03_25 : EditorWindow, IHasCustomMenu
{
void IHasCustomMenu.AddItemsToMenu(GenericMenu menu)
{
menu.AddDisabledItem(new GUIContent("Disable"));
menu.AddItem(new GUIContent("Test1"), true, () =>
{
Debug.Log("Test1");
});
menu.AddItem(new GUIContent("Test2"), true, () =>
{
Debug.Log("Test2");
});
menu.AddSeparator("Test/");
menu.AddItem(new GUIContent("Test/Test3"), true, () =>
{
Debug.Log("Test3");
});
}
[ ]
static void Init()
{
Script_03_25 window = (Script_03_25)EditorWindow.GetWindow(typeof(Script_03_25));
window.Show();
}
}
上述代码中,通过AddItem()方法来添加列表元素,并且监听选择后的事件。
预览窗口
选择游戏对象或者游戏资源后,Inspector面板下方将会出现它的预览窗口,但是有些资源是没有预览信息的,不过可以监听它的窗口方法来重新绘制它。如下图所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17using UnityEngine;
using UnityEditor;
[ ]
public class Script_03_26 : ObjectPreview
{
public override bool HasPreviewGUI()
{
return true;
}
public override void OnPreviewGUI(Rect r, GUIStyle background)
{
GUI.DrawTexture(r, AssetDatabase.LoadAssetAtPath<Texture>("Assets/unity.png"));
GUILayout.Label("Hello World!!!");
}
}
这段代码的原理就是继承ObjectPreview并且重写OnPreviewGUI()方法,接着就可以通过代码进行绘制了。[CustomPreview(typeof(GameObject))]中的GameObject代表需要重新绘制的预览对象,也可以换成别的系统对象或自定义的脚本对象。
获取预览信息
有些资源是有预览信息的,比如模型资源。在预览窗口中,可以看到它的样式。如果需要在自定义窗口中显示它,就需要获取它的预览信息。如下图所示,选择一个游戏对象后,会在自定义窗口中显示它。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
32using UnityEngine;
using UnityEditor;
public class Script_03_27 : EditorWindow
{
private GameObject m_MyGo;
private Editor m_MyEditor;
[ ]
static void Init()
{
Script_03_27 window = (Script_03_27)EditorWindow.GetWindow(typeof(Script_03_27));
window.Show();
}
private void OnGUI()
{
// 设置一个游戏对象
m_MyGo = (GameObject)EditorGUILayout.ObjectField(m_MyGo, typeof(GameObject), true);
if (m_MyGo != null)
{
if (m_MyEditor == null)
{
// 创建Editor实例
m_MyEditor = Editor.CreateEditor(m_MyGo);
}
// 预览它
m_MyEditor.OnPreviewGUI(GUILayoutUtility.GetRect(500, 500), EditorStyles.whiteLabel);
}
}
}
在上述代码中,预览对象首先需要通过Editor.CreateEditor()拿到它的Editor实例对象,接着调用OnPreviewGUI()方法传入窗口的显示区域。
Unity编辑器的源码
Unity编辑器几乎都是用C#编写而成,视图中也大量使用EditorGUI来编辑布局。例如,对于常见的5大布局视图,所有的代码都放在UnityEditor.dll中。打开Unity安装目录,在Unity\Editor\Data\Managed子目录中存放着引擎所需要用到的所有DLL文件。
查看DLL
拿到UnityEditor.dll以后,就可以通过第三方工具来分析和查看了。常用的工具包括.NET Reflector以及ILSpy。其实Unity的C#版API接口都在UnityEngine.dll里,只是源码的核心功能都是在C/C++中完成的,DLL只负责中间调用接口而已。
清空控制台日志
系统日志以及Debug.Log()产出的日志都输出在Console窗口中。在Console窗口的左上角,有个Clean按钮,它用于清空控制台日志。如果希望脚本可以灵活自动清空日志,就必须使用反射了。首先找到控制台的窗口类(ConsoleWindows.cs),接着在OnGUI()方法中可以看到:点击Clean按钮后,Unity会执行LogEntries.Clear()方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23using UnityEngine;
using UnityEditor;
using System.Reflection;
public class Script_03_28
{
[ ]
static void CreateConsole()
{
Debug.Log("CreateConsole");
}
[ ]
static void CleanConsole()
{
// 获取assembly
Assembly assembly = Assembly.GetAssembly(typeof(Editor));
// 反射获取LogEntries对象
MethodInfo methodInfo = assembly.GetType("UnityEditor.LogEntries").GetMethod("Clear");
// 反射调用它的Clear方法
methodInfo.Invoke(new object(), null);
}
}
获取EditorStyles样式
1 | using UnityEngine; |
获取内置图标样式
1 | using UnityEngine; |
拓展默认面板
1 | using UnityEngine; |
UIElements
1 | TextField |
1 | using UnityEngine; |