一、簡介
最近馬三為公司開發了一款觸發器編輯器,對于這個編輯器策劃所要求的品質很高,是模仿暴雪的那個觸發器編輯器來做的,而且之後這款編輯器要作為公司内部的一個通用工具鍊使用。其實,在這款觸發器編輯器之前,已經有一款用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編輯器視窗基類代碼如下:
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEditor;
4 using UnityEngine;
5
6 /// <summary>
7 /// 編輯器視窗基類
8 /// </summary>
9 public class EditorWindowBase : EditorWindow
10 {
11 /// <summary>
12 /// 界面層級管理,根據界面優先級通路界面焦點
13 /// </summary>
14 public int Priority { get; set; }
15
16 private void OnFocus()
17 {
18 //重寫OnFocus方法,讓EditorWindowMgr去自動排序彙聚焦點
19 EditorWindowMgr.FoucusWindow();
20 }
21 }
再來看看EditorWindowMgr編輯器視窗管理類是如何實作的:
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 /// <summary>
6 /// 編輯器視窗管理類
7 /// </summary>
8 public class EditorWindowMgr
9 {
10 /// <summary>
11 /// 所有打開的編輯器視窗的緩存清單
12 /// </summary>
13 private static List<EditorWindowBase> windowList = new List<EditorWindowBase>();
14
15 /// <summary>
16 /// 重複彈出的視窗的優先級
17 /// </summary>
18 private static int repeateWindowPriroty = 10;
19
20 /// <summary>
21 /// 添加一個重複彈出的編輯器視窗到緩存中
22 /// </summary>
23 /// <param name="window"></param>
24 public 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>
35 public 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>
46 public static void AddEditorWindow(EditorWindowBase window)
47 {
48 if (!windowList.Contains(window))
49 {
50 windowList.Add(window);
51 SortWinList();
52 }
53 }
54
55 /// <summary>
56 /// 從緩存中移除一個編輯器視窗
57 /// </summary>
58 /// <param name="window"></param>
59 public static void RemoveEditorWindow(EditorWindowBase window)
60 {
61 if (windowList.Contains(window))
62 {
63 windowList.Remove(window);
64 SortWinList();
65 }
66 }
67
68 /// <summary>
69 /// 管理器強制重新整理Window焦點
70 /// </summary>
71 public static void FoucusWindow()
72 {
73 if (windowList.Count > 0)
74 {
75 windowList[windowList.Count - 1].Focus();
76 }
77 }
78
79 /// <summary>
80 /// 關閉所有界面,并清理WindowList緩存
81 /// </summary>
82 public static void DestoryAllWindow()
83 {
84 foreach (EditorWindowBase window in windowList)
85 {
86 if (window != null)
87 {
88 window.Close();
89 }
90 }
91 windowList.Clear();
92 }
93
94 /// <summary>
95 /// 對目前緩存視窗清單中的視窗按優先級升序排序
96 /// </summary>
97 private static void SortWinList()
98 {
99 windowList.Sort((x, y) =>
100 {
101 return 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的值即可:
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEditor;
4 using UnityEngine;
5
6 /// <summary>
7 /// 編輯器主界面
8 /// </summary>
9 public class MainWindow : EditorWindowBase
10 {
11 private static MainWindow window;
12 private static Vector2 minResolution = new Vector2(800, 600);
13 private static Rect middleCenterRect = new Rect(200, 100, 400, 400);
14 private GUIStyle labelStyle;
15
16 /// <summary>
17 /// 對外的通路接口
18 /// </summary>
19 [MenuItem("Tools/RepeateWindow")]
20 public static void Popup()
21 {
22 window = EditorWindow.GetWindow(typeof(MainWindow), true, "多重視窗編輯器") as MainWindow;
23 window.minSize = minResolution;
24 window.Init();
25 EditorWindowMgr.AddEditorWindow(window);
26 window.Show();
27 }
28
29 /// <summary>
30 /// 在這裡可以做一些初始化工作
31 /// </summary>
32 private void Init()
33 {
34 Priority = 1;
35
36 labelStyle = new GUIStyle();
37 labelStyle.normal.textColor = Color.red;
38 labelStyle.alignment = TextAnchor.MiddleCenter;
39 labelStyle.fontSize = 14;
40 labelStyle.border = new RectOffset(1, 1, 2, 2);
41 }
42
43 private void OnGUI()
44 {
45 ShowEditorGUI();
46 }
47
48 /// <summary>
49 /// 繪制編輯器界面
50 /// </summary>
51 private void ShowEditorGUI()
52 {
53 GUILayout.BeginArea(middleCenterRect);
54 GUILayout.BeginVertical();
55 EditorGUILayout.LabelField("點選下面的按鈕建立重複彈出視窗", labelStyle, GUILayout.Width(220));
56 if (GUILayout.Button("建立視窗", GUILayout.Width(80)))
57 {
58 RepeateWindow.Popup(window.position.position);
59 }
60 GUILayout.EndVertical();
61 GUILayout.EndArea();
62 }
63
64 private void OnDestroy()
65 {
66 //主界面銷毀的時候,附帶銷毀建立出來的子界面
67 EditorWindowMgr.RemoveEditorWindow(window);
68 EditorWindowMgr.DestoryAllWindow();
69 }
70
71 private void OnFocus()
72 {
73 //重寫OnFocus方法,讓EditorWindowMgr去自動排序彙聚焦點
74 EditorWindowMgr.FoucusWindow();
75 }
76 }
最後讓我們看一下可重複彈出視窗是如何實作的,代碼如下,有了前面的鋪墊和代碼中的注釋相信大家一看就會明白,這裡就不再逐條進行解釋了:
1 using System;
2 using UnityEditor;
3 using UnityEngine;
4
5 /// <summary>
6 /// 重複彈出的編輯器視窗
7 /// </summary>
8 public class RepeateWindow : EditorWindowBase
9 {
10
11 private static Vector2 minResolution = new Vector2(300, 200);
12 private static Rect leftUpRect = new Rect(new Vector2(0, 0), minResolution);
13
14 public 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 //重新整理界面偏移量
22 int 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>
37 private 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]);*/
41 if (!(bool)editorWindow)
42 {
43 editorWindow = (ScriptableObject.CreateInstance(t) as EditorWindow);
44 editorWindow.minSize = new Vector2(rect.width, rect.height);
45 editorWindow.maxSize = new Vector2(rect.width, rect.height);
46 editorWindow.position = rect;
47 if (title != null)
48 {
49 editorWindow.titleContent = new GUIContent(title);
50 }
51 if (utility)
52 {
53 editorWindow.ShowUtility();
54 }
55 else
56 {
57 editorWindow.Show();
58 }
59 }
60 else
61 {
62 editorWindow.Focus();
63 }
64 return editorWindow;
65 }
66
67
68 private void OnGUI()
69 {
70 OnEditorGUI();
71 }
72
73 private void OnEditorGUI()
74 {
75 GUILayout.Space(12);
76 GUILayout.BeginVertical();
77 EditorGUILayout.LabelField("我是重複彈出的窗體", GUILayout.Width(200));
78 if (GUILayout.Button("建立窗體", GUILayout.Width(100)))
79 {
80 //重複建立自己
81 Popup(this.position.position);
82 }
83 GUILayout.Space(12);
84 if (GUILayout.Button("關閉窗體", GUILayout.Width(100)))
85 {
86 this.Close();
87 }
88 GUILayout.EndVertical();
89 }
90
91 private void OnDestroy()
92 {
93 //銷毀窗體的時候,從管理器中移除該窗體的緩存,并且重新重新整理焦點
94 EditorWindowMgr.RemoveRepeateWindow(this);
95 EditorWindowMgr.FoucusWindow();
96 }
97
98 private void OnFocus()
99 {
100 EditorWindowMgr.FoucusWindow();
101 }
102 }
四、總結
通過本篇部落格,我們一起學習了如何在Unity編輯器中建立可重複的彈出界面與編輯器界面的層級如何管理。由于時間匆忙,本篇部落格中的DEMO在所難免會有一些纰漏,歡迎大家共同完善。希望本文能夠為大家的工作中帶來一些啟發與提示。
本篇部落格中的所有代碼已經托管到Github,開源位址:https://github.com/XINCGer/Unity3DTraining/tree/master/UnityEditorExtension/MultiEditorWindow
作者:馬三小夥兒
出處:https://www.cnblogs.com/msxh/p/9215015.html
請尊重别人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!