一、簡介
最近馬三為公司開發了一款觸發器編輯器,對於這個編輯器策划所要求的質量很高,是模仿暴雪的那個觸發器編輯器來做的,而且之后這款編輯器要作為公司內部的一個通用工具鏈使用。其實,在這款觸發器編輯器之前,已經有一款用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
請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!