【Unity编辑器】UnityEditor多重弹出窗体与编辑器窗⼝层级管理 ⼀、简介
最近马三为公司开发了⼀款触发器编辑器,对于这个编辑器策划所要求的质量很⾼,是模仿暴雪的那个触发器编辑器来做的,⽽且之后这款编辑器要作为公司内部的⼀个通⽤⼯具链使⽤。其实,在这款触发器编辑器之前,已经有⼀款⽤WinForm开发的1.0版触发器编辑器了,不过由于界⾯不太友好、操作繁琐以及学习使⽤成本较⾼,所以也饱受策划们的吐槽。⽽新研发的这款编辑器是直接嵌⼊在Unity中,作为Unity的拓展编辑器来使⽤的。当然在开发中,马三也遇到了种种的问题,不过还好,在同事的帮助下都⼀⼀解决了。本篇博客,马三就来和⼤家分享⼀下其中⼀个⽐较有趣的需求,RT,“UnityEditor多重弹出窗体与编辑器窗⼝层级管理”。
针对⼀些逻辑和数据部分的代码,由于是公司机密⽽且与本⽂的内容联系不⼤,马三就不和⼤家探讨了,本⽂中我们只关注UI的表现部分。(本⽂中所有的样例代码均经过重写,只⽤了原来的思想,代码结构已经和公司的编辑器完全不⼀样了,因此不涉及保密协议,完全开源,⼤家可以放⼼使⽤)先来说下今天我们要探讨的这个需求吧: 针对表达式进⾏解析,然后弹出可编辑的嵌套窗体。表达式有可能是嵌套的结构,因此弹出的窗体也要是多重弹出且嵌套的。
对于多重弹出的窗体,均为模态窗⼝,要有UI排序,新弹出的窗体要在原来的窗体的上⾯,且要有⼀定的⾃动偏移。上层窗体打开的状态下不能对下⾯的窗体进⾏操作(拖拽窗体是允许的,只是不能点击界⾯上的按钮,输⼊⽂字等等⾏为)。
界⾯⾃动聚焦,新创建窗体的时候,焦点会⾃动转移到新的窗体上,焦点⼀直保持在最上层的UI上⾯。 主界⾯关闭的时候,⾃动关闭其他打开的⼦界⾯。
所以策划要求的其实就是类似下⾯的这个样⼦的⼀个效果:
图1:最终效果图
这其中有两个⽐较值得注意的点:1.如何在Unity编辑器中创建可重复的弹出界⾯;2.界⾯的层级如何管理。下⾯我们将围绕这两个点逐⼀讨论。
⼆、如何在Unity编辑器中创建可重复的弹出窗体
众所周知,如果想要在Unity中创建出⼀个窗体,⼀般需要新建⼀个窗体类并继承⾃EditorWindow,然后调⽤EditorWindow.GetWindow()⽅法返回⼀个本类型的窗体,然后再对这个窗体进⾏show操作,这个窗体就显⽰出来了,总共算起来也就是下⾯两⾏代码:
window = EditorWindow.GetWindow(typeof(MainWindow), true, "多重窗⼝编辑器") as MainWindow;
window.Show();
我们可以把上⾯的操作封装到⼀个名叫Popup的静态⽅法中,这样在外部每次⼀调⽤Popup⽅法,我们的窗体就创建出来了。但是⽆论如何我们调⽤多少次Popup,在界⾯上始终只会有⼀个窗体出现,并不能出现多个同样的窗体存在。其原因我们可以在API⽂档中得到:
图2:官⽹API解释
如果界⾯上没有该窗体的实例,会创建、显⽰并返回该窗体的实例。否则,每次会返回第⼀个该窗体实例。这就不难解释为什么不能创建多个相同窗体的原因了,我们可以把他类⽐为⼀个单例模式的存在,如果没有就创建,如果有就返回当前的实例。再进⼀步我们可以通过反编译UnityEditor.dll来查看⼀下,他在底层是怎样实现的。UnityEditor.dll⼀般位于: X:\Program Files\Unity\Editor\Data\Managed\UnityEditor.dll 路径下⾯。
图3:反编译结果1
重载的⼏个 GetWindow ⽅法在最后都调⽤了 GetWindowPrivate 这个⽅法,我们再看⼀下对于 GetWindowPrivate 这个⽅法,Unity是如何实现它的:
图4:反编译结果2
结果⼀⽬了然,⾸先会调⽤Resources.FindObjectsOfTypeAll(t) 返回Unity中所有已经加载了的类型为 t 的实例并存储到array数组中,然后对editorWindow 进⾏赋值,如果array数据没有数据则赋值为null,否则取数组中的第⼀个元素。接着,如果发现内存中没有该类型的实例,通过editorWindow = (ScriptableObject.CreateInstance(t) as EditorWindow);创建⼀个类型为EditorWindow的实例,也就是⼀个新的窗体,对他进⾏了⼀系列的初始化以后,将其显⽰出来,并返回该类型的实例。如果内存中有该类型的实例,则调⽤show⽅法,并且把焦点聚焦到该窗体上,然后返回该类型的实例。
我们从源码的层⾯了解到了不能创建多个重复窗体的原因,并且搞清了他的创建原理,这样创建多个相同重复窗体的功能就不难写出来了,我们只要
将 GetWindowPrivate ⽅法中的前两⾏代码替换为EditorWindow editorWindow = null 改造为我们⾃⼰的⽅法;⽤我们⾃⼰的 GetWindowPrivate ⽅法去创建,就可以得到⽆限多的重复窗体了。尽管通过 RepeateWindow window = new RepeateWindow() 的⽅法,我们也可以很轻松地得到⽆限多的重复窗体,但是这样操作会在Unity中报出警告信息,因为我们的EditorWindow都是继承⾃ ScriptableObject,⾃然要通过ScriptableObject.CreateInstance来创建实例,⽽不是直接通过构造器来创建。
三、编辑器UI的具体实现与层级管理
为了管理我们的编辑器窗⼝,马三引⼊了⼀个Priority的属性,它代表了界⾯的优先级。因为我们的所有的编辑器窗⼝都要参与管理,因此我们不妨直接先定义⼀个EditorWindowBase编辑器窗⼝基类,然后我们的后续的编辑器窗⼝类都继承⾃它,并且EditorWindowMgr编辑器窗⼝管理类也直接对该类型及其派⽣类型的窗体进⾏管理与操作。EditorWindowBase编辑器窗⼝基类代码如下:
1using System.Collections;
2using System.Collections.Generic;
3using UnityEditor;
4using UnityEngine;
5
6///<summary>
7///编辑器窗⼝基类
8///</summary>
9public class EditorWindowBase : EditorWindow
10 {
11///<summary>
12///界⾯层级管理,根据界⾯优先级访问界⾯焦点
13///</summary>
14public int Priority { get; set; }
15
16private void OnFocus()
17 {
18//重写OnFocus⽅法,让EditorWindowMgr去⾃动排序汇聚焦点
19 EditorWindowMgr.FoucusWindow();
20 }
21 }
再来看看EditorWindowMgr编辑器窗⼝管理类是如何实现的:
1using System.Collections;
2using System.Collections.Generic;
3using UnityEngine;
4
5///<summary>
6///编辑器窗⼝管理类
7///</summary>
8public class EditorWindowMgr
9 {
10///<summary>
11///所有打开的编辑器窗⼝的缓存列表
12///</summary>
13private static List<EditorWindowBase> windowList = new List<EditorWindowBase>();
14
15///<summary>
16///重复弹出的窗⼝的优先级
17///</summary>
18private static int repeateWindowPriroty = 10;
19
20///<summary>
21///添加⼀个重复弹出的编辑器窗⼝到缓存中
22///</summary>
23///<param name="window"></param>
24public static void AddRepeateWindow(EditorWindowBase window)
25 {
打印头校准
26 repeateWindowPriroty++;
27 window.Priority = repeateWindowPriroty;
28 AddEditorWindow(window);
29 }
30
31///<summary>
32///从缓存中移除⼀个重复弹出的编辑器窗⼝
33///</summary>
34///<param name="window"></param>
35public static void RemoveRepeateWindow(EditorWindowBase window)
36 {
37 repeateWindowPriroty--;
38 window.Priority = repeateWindowPriroty;
39 RemoveEditorWindow(window);
40 }
41
42///<summary>
43///添加⼀个编辑器窗⼝到缓存中
44///</summary>
45///<param name="window"></param>
46public static void AddEditorWindow(EditorWindowBase window)
47 {
48if (!windowList.Contains(window))
49 {
50 windowList.Add(window);
54
55///<summary>
56///从缓存中移除⼀个编辑器窗⼝
57///</summary>
58///<param name="window"></param>
59public static void RemoveEditorWindow(EditorWindowBase window)
60 {
生物化粪池61if (windowList.Contains(window))
62 {
63 windowList.Remove(window);
64 SortWinList();
65 }
66 }
67
68///<summary>
69///管理器强制刷新Window焦点
70///</summary>
71public static void FoucusWindow()
72 {
73if (windowList.Count > 0)
74 {
75 windowList[windowList.Count - 1].Focus();
76 }
77 }
78
79///<summary>
80///关闭所有界⾯,并清理WindowList缓存
81///</summary>
82public static void DestoryAllWindow()
83 {
84foreach (EditorWindowBase window in windowList)
85 {
86if (window != null)
87 {
88 window.Close();
糖浆罐
89 }
90 }
91 windowList.Clear();
92 }
93
94///<summary>
95///对当前缓存窗⼝列表中的窗⼝按优先级升序排序
96///</summary>
97private static void SortWinList()
98 {
99 windowList.Sort((x, y) =>
100 {
101return x.Priority.CompareTo(y.Priority);
102 });
103 }
104 }
对每个打开的窗体我们都通过AddEditorWindow操作将其加⼊到windowList缓存列表中,每个关闭的窗体我们会执⾏RemoveEditorWindow⽅法,将其从缓存列表中移除,每当增加或者删除窗体的时候,都会执⾏SortWinList⽅法,对缓存列表中的窗体按照Priority进⾏升序排列。⽽对于可重复弹出的窗⼝,我们提供了AddRepeateWindow 和 RemoveRepeateWindow这两个特殊接⼝,主要是对可重复弹出的窗⼝的优先级进⾏⾃动管理。DestoryAllWindow⽅法提供了在主界⾯关闭的时候,强制关闭所有的⼦界⾯的功能。最后还有⼀个⽐较重要的FoucusWindow⽅法,它是管理器强制刷新Window焦点,每次会把焦点强制聚焦到缓存列表中的最后⼀个元素,即优先级最⼤的界⾯上⾯,其实也就是最后创建的界⾯上⾯。通过重写每个界⾯的OnFocus函数为如下形式,⼿动调⽤EditorWindowMgr.FoucusWindow()让管理器去⾃动管理界⾯层级:
private void OnFocus()
{
EditorWindowMgr.FoucusWindow();
}
接下来让我们看⼀下我们的编辑器主界⾯部分的代码,就是绘制了⼀些Label和按钮,没有什么太需要注意的地⽅,只要记得设置⼀下Priority的值即可: 1using System.Collections;
2using System.Collections.Generic;
3using UnityEditor;
4using UnityEngine;
5
6///<summary>
7///编辑器主界⾯
8///</summary>
9public class MainWindow : EditorWindowBase
10 {
11private static MainWindow window;
12private static Vector2 minResolution = new Vector2(800, 600);
13private static Rect middleCenterRect = new Rect(200, 100, 400, 400);
14private GUIStyle labelStyle;
15
16///<summary>
17///对外的访问接⼝
18///</summary>
19 [MenuItem("Tools/RepeateWindow")]
20public static void Popup()
21 {耳机延长线
22 window = EditorWindow.GetWindow(typeof(MainWindow), true, "多重窗⼝编辑器") as MainWindow;
23 window.minSize = minResolution;
24 window.Init();
25 EditorWindowMgr.AddEditorWindow(window);
29///<summary>
30///在这⾥可以做⼀些初始化⼯作
31///</summary>
32private void Init()
33 {
34 Priority = 1;
35
36 labelStyle = new GUIStyle();
37 Color = d;
38 labelStyle.alignment = TextAnchor.MiddleCenter;
39 labelStyle.fontSize = 14;
40 labelStyle.border = new RectOffset(1, 1, 2, 2);
41 }
42
43private void OnGUI()
44 {
龙芯一体机45 ShowEditorGUI();
46 }
47
48///<summary>
49///绘制编辑器界⾯
50///</summary>
51private void ShowEditorGUI()
52 {
53 GUILayout.BeginArea(middleCenterRect);
54 GUILayout.BeginVertical();
55 EditorGUILayout.LabelField("点击下⾯的按钮创建重复弹出窗⼝", labelStyle, GUILayout.Width(220));
56if (GUILayout.Button("创建窗⼝", GUILayout.Width(80)))
57 {
58 RepeateWindow.Popup(window.position.position);
59 }
60 GUILayout.EndVertical();
61 GUILayout.EndArea();
62 }
63
64private void OnDestroy()
65 {
66//主界⾯销毁的时候,附带销毁创建出来的⼦界⾯
67 EditorWindowMgr.RemoveEditorWindow(window);
68 EditorWindowMgr.DestoryAllWindow();
混凝土模板
69 }
70
71private void OnFocus()
72 {
73//重写OnFocus⽅法,让EditorWindowMgr去⾃动排序汇聚焦点
74 EditorWindowMgr.FoucusWindow();
75 }
76 }
最后让我们看⼀下可重复弹出窗⼝是如何实现的,代码如下,有了前⾯的铺垫和代码中的注释相信⼤家⼀看就会明⽩,这⾥就不再逐条进⾏解释了: 1using System;
2using UnityEditor;
3using UnityEngine;
4
5///<summary>
6///重复弹出的编辑器窗⼝
7///</summary>
8public class RepeateWindow : EditorWindowBase
9 {
10
11private static Vector2 minResolution = new Vector2(300, 200);
12private static Rect leftUpRect = new Rect(new Vector2(0, 0), minResolution);
13
14public static void Popup(Vector3 position)
15 {
16// RepeateWindow window = new RepeateWindow();
17 RepeateWindow window = GetWindowWithRectPrivate(typeof(RepeateWindow), leftUpRect, true, "重复弹出窗⼝") as RepeateWindow;
18 window.minSize = minResolution;
19//要在设置位置之前,先把窗体注册到管理器中,以便更新窗体的优先级
20 EditorWindowMgr.AddRepeateWindow(window);
21//刷新界⾯偏移量
22int offset = (window.Priority - 10) * 30;
23 window.position = new Rect(new Vector2(position.x + offset, position.y + offset), new Vector2(800, 400));
24 window.Show();
25//⼿动聚焦
26 window.Focus();
27 }
28
29///<summary>
30///重写EditorWindow⽗类的创建窗⼝函数
31///</summary>
32///<param name="t"></param>
33///<param name="rect"></param>
34///<param name="utility"></param>
35///<param name="title"></param>
36///<returns></returns>
37private static EditorWindow GetWindowWithRectPrivate(Type t, Rect rect, bool utility, string title)
38 {
39//UnityEngine.Object[] array = Resources.FindObjectsOfTypeAll(t);
40 EditorWindow editorWindow = null;/*= (array.Length <= 0) ? null : ((EditorWindow)array[0]);*/
41if (!(bool)editorWindow)