IMGUI 介紹
所有關於 Editor 的相關 UI,包括 Inspector、Hierarchy、Window、Game 視圖上動態創建的那些半透明 UI、還有 Scene 視圖上可添加的輔助顯示 UI,叫做 IMGUI,全稱 Immediate Mode GUI
。該名字來源於兩類型的 UI 系統:immediate
和 retained
。
- retained:當你設置好各種組件如 Text、Button 等的信息,或修改它們的相關屬性后,這些組件的相關信息和改動就被保存(retained)下來了,系統會根據這些新的信息來繪制響應事件等,你可以隨時去查詢如 Text 文本內容或顏色等信息。UGUI 即是典型的 retained mode GUI。
-
immediate:跟上面的相反,系統不會自動保存 UI 控件上的各種信息,不會用上次的狀態繼續工作,而是反復的詢問你這些控件應當是處於什么位置什么文本等狀態信息。因此任何的用戶交互結果是立即呈現返回給用戶,而不是當用戶需要的時候自行查詢。如:
1 bool selected = false; 2 void OnGUI() 3 { 4 selected = GUILayout.Toggle(selected, "A Toggle text"); 5 if (selected) 6 { 7 DoSomething() 8 } 9 10 //if (GUILayout.Toggle(selected, "A Toggle text");) 11 //{ 12 // DoSomething() 13 //} 14 }
OnGUI 會被反復調用以更新繪制 UI,通常控件的返回值需要自行保存下來再傳入控件中以更新控件狀態,如果像注釋中的代碼那樣則 Toggle 的狀態改變后下一次更新則又變為舊的了,感受就像功能失效了一下。
IMGUI 是十分低效的,它是純代碼驅動的,對於美術而言基本無法使用(非可視化的,稍復雜點的 UI 程序寫起來也很蛋疼...)。但是對於非實時交互的情況下卻是一種可選的方式,比如 Inspector 上的 UI,它本身就是對代碼腳本的擴展,通常不是美術人員所寫腳本,控件可立即展示對應的腳本狀態的修改。
關於IMGUI的基本介紹請看官方文檔
相關類介紹
Editor 類和 EditorWindow 類都繼承自同一個基類:ScriptableObject,因此他們都可以針對某種腳本類來進行操作。
Editor 類只能定制針對腳本的擴展,從腳本內容在 Inspector 里的顯示布局,到變量在 Scene 視圖的可視化編輯
EditorWindow 主要是擴展編輯器的功能,不必針對某種腳本(雖然可以做到),而且它有獨立的窗口,使用 OnGUI 函數來繪制 2D 的 UI。
能在 Game 視圖上顯示的 ingame GUI 主要是 GUI 和 GUILayout 兩個類,另外與之對應的 editor-only 的類是 EditorGUI 和 EditorGUILayout 兩個類,兩套類提供的控件功能都差不多,可以混合搭配一起使用。
GUI 和 EditorGUI 提供的接口為 Fixed Layout 的,基本上都需要傳入一個 Rect 變量來指定控件的位置和大小,當窗口大小改時控件會保持不變。可以將代碼放到 GUI.BeginGroup() 和 GUI.EndGroup() 之間將控件進行分組或划分子區域進行布局。
GUILayout 和 EditorGUILayout 則是與前面兩個對應的 Auto Layout 類,不需要指定控件位置和大小,會根據當前顯示區域的大小自動調整布局適應變化。多個控件默認是從上往下的順序排列,可以用 GUILayout.BeginHoriztontal(), GUILayout.EndHorizontal(), GUILayout.BeginVertical(), GUILayout.EndVertical()(或者對應的 EditorGUILayout 類)將代碼寫到這些調用之間進行水平或者垂直排列控件,將這些布局互相組合或嵌套即可排布出復雜的 UI 界面。
GUIUtility 類提供了一些工具方法,如獲取控件 id、轉換 Screen 和 GUI 之間的坐標等。
EditorGUIUtility 類是針對 Editor 提供一些工具方法,除了 GUIUtility 那些方法外還增加了很多有用的方法,如獲取內建資源圖標、高亮選中某個物體、產生復制粘貼命令等。
Event:該類包含了所有的用戶輸入如按鈕或鼠標點擊等和 UI 相關布局和繪制事件。調用 Event.current
獲得當前事件信息,查看 EventType
枚舉定義可查詢全部可用類型事件。這個信息在 OnGUI,OnInspector,OnSceneGUI 里都可以使用以處理一些特定邏輯。
自定義控件
GUILayout.Button(GUIContent content, GUIStyle style, params GUILayoutOption[] options)
大部分控件都可以傳入 GUIContent
、GUIStyle
來指定控件的風格外觀,自動布局的控件還可以傳入多個 GUILayoutOption
組合來設定大小。
- GUIContent:該類定義了控件需要顯示什么(what to render),包含三個基本要素:圖片 image、文本 text、鼠標停留的提示信息 tooltip(play mode 運行時 tooltip 無效)。也可以用控件的其它重載分別傳入這幾項中的一個或多個內容。可以用
EditorGUIUtility.IconContent(name)
獲得一個內置圖標,如下可獲得一個播放按鈕:GUILayout.Button(EditorGUIUtility.IconContent("PlayButton")
- GUIStyle:該類定義了控件要如何顯示(how to render),包括 Normal Hover Active 等狀態切換顯示、文字大小顏色、指定圖標顯示位置等各種信息。每種類型的控件都會有默認的外觀風格,通常可以在現有的控件風格上進行修改:
//默認控件文本會顯示在圖標之后,下面可獲得圖上字下的按鈕風格 GUIStyle style = new GUIStyle(GUI.skin.button); //或者傳入 unity 的默認風格名稱 new GUIStyle("button") style.imagePosition = ImagePosition.ImageAbove;
- GUILayoutOption:該類為自動布局 GUILayout 和 EditorGUILayout 的控件提供一系列預定條件,如最小寬度、最大高度、是否橫向拉伸等。
-
GUISkin:是一系列GUIStyle的集合,可對每種控件分別指定樣式,可設置一整套風格統一完全不同於默認風格的UI。通過 Assets->Create->GUI Skin 可創建,代碼中
GUI.skin = customSkin;
即可一次應用所有的 GUIStyle。
當要實現一個自定義功能的控件,大體的處理流程為以下所示代碼,該代碼為 GUILayout.RepeatButton
的主要代碼,該代碼在 OnGUI
中被調用:
1 private static bool DoRepeatButton( 2 Rect position, 3 GUIContent content, 4 GUIStyle style, 5 FocusType focusType) 6 { 7 GUIUtility.CheckOnGUI(); 8 //分配一個唯一 id 值給該控件,傳入的第一個參數為任意唯一的值,此處為一個 string 的 hash。 9 int controlId = GUIUtility.GetControlID(GUI.s_RepeatButtonHash, focusType, position); 10 //獲得對應的各種事件並處理關心的。 11 switch (Event.current.GetTypeForControl(controlId)) 12 { 13 case EventType.MouseDown: 14 //鼠標的點擊位置在當前控件上。 15 if (position.Contains(Event.current.mousePosition)) 16 { 17 //保存當前的 id 為 hot 的控件,全局只能有一個為 hot 控件。 18 GUIUtility.hotControl = controlId; 19 //消耗掉當前事件防止后續控件處理無效邏輯。(故所有在該控件之后的控件全部無法再判斷點擊事件) 20 Event.current.Use(); 21 } 22 return false; 23 case EventType.MouseUp: 24 //僅當前的 hot 控件為本控件時才處理對應邏輯,忽略掉其它。 25 if (GUIUtility.hotControl != controlId) 26 return false; 27 //當前 hot 控件功能結束后一定要置 0 恢復,否則當前 UI 是凍結的,其它控件全部無法響應。 28 GUIUtility.hotControl = 0; 29 Event.current.Use(); 30 return position.Contains(Event.current.mousePosition); 31 case EventType.Repaint: 32 //該事件處理顯示相關。 33 style.Draw(position, content, controlId); 34 return controlId == GUIUtility.hotControl && position.Contains(Event.current.mousePosition); 35 default: 36 return false; 37 } 38 }
關於 ControlID 相關概念,我個人理解感覺作用不大,GetControlID
獲得的 id 與控件其實並沒有直接的聯系,連續多次調用便能獲得多個不同的值,在每次 OnGUI 結束后 id 棧信息就會清空以便每次重入時能產生與之前一致的 id。id 與控件的關系為手動關聯起來的,因代碼的順序執行,當前環境的后續以該 id 相關的處理 “認為” 即是對應該控件。如以上代碼,GUI.xxx 等內置的控件中全部都有對應的一個 id,該 id 外部是無法訪問的,故在 GUI.Button 之后立即調用 GetControlID 是得不到 Button 的 id 的,僅是又產生了一個新的值,同樣不能再拿到控件內部已處理過的事件。
GetTypeForControl
獲得傳入 id 對應的事件類型,該信息與實際控件同樣是無直接關聯的,必須同時判斷 controlPosition.Contains(Event.current.mousePosition) 才能得知為當前控件上的事件,經測試發現它與 Event.current.type
並沒有什么區別,即使當前 hotControl 或 keyboardControl 不是當前 id,它好像沒有針對傳入的 id 作任何有效過濾。
比較有用的可能是 GUIUtility.GetStateObject
,它給 id 綁定了一個自定義的數據信息以供后續邏輯處理,從而不需要自己維護與各控件相關的數據。
如果有同學對 ControlID 有更深入的理解望可以討論一下~
Tips
Getting control 0’s position in a group with only 0 controls when doing Repaint.
OnGUI 循環實際上是被一系列的 Event
所調用,如,IMGUI 會在 EventType.Layout
中收集所有控件的包含關系及占用的空間大小位置等信息,然后在 EventType.Repaint
事件中才實際以 Layout 中統計的信息來分配空間繪制顯示。
如果某邏輯在 Layout 與 Repaint 之間導致了 UI 數據不一致時就會出現上面類似的報錯,有時也會看到一個 if 判斷肯定是進不去的但實際卻進去了等現象也是在不同的事件中情況會不一樣。
此時需要仔細檢查邏輯是否有意外導致 Layout 布局在即將 Repaint 顯示時不一致。
可以將可能有問題的代碼寫在下面代碼塊中:
if (Event.current.type == EventType.Repaint) { // } 或 if (Event.current.type == EventType.Layout) { // }
GUIUtility.ExitGUI
也可處理該報錯,但后續代碼也得不到執行了……該接口在 2018 上有文檔說明,在 5.x 上面搜不到,但實際上仍是可調用的。
NullReferenceException: Object reference not set to an instance of an object
UnityEngine.GUILayoutUtility.BeginLayoutGroup (UnityEngine.GUIStyle style, UnityEngine.GUILayoutOption[] options, System.Type layoutType) (at /Users/builduser/buildslave/unity/build/Runtime/IMGUI/Managed/GUILayoutUtility.cs:296)
該報錯有一種情況是當調用了 EditorUtility.DisplayProgressBar
顯示進度條時會出現,它會主動調用到 OnGUI,猜測是當前的 OnGUI 還未結束就再次進入 OnGUI 導致的類似上一個問題中的在函數重入時數據不一致布局信息錯亂。
暫時未找到解決方法,只有在卡頓死等與進度展示伴隨報錯二者中選擇了。
GUIContent 中設置的 tooltip 功能只在非運行起來時可用,將編輯器運行后即失效,最后查到該問題是 By Design...
任一個 GUIStyle 可用於任一類似的控件,在美術不提供圖的情況下混用 style 即可搭出較好的效果:
1 //傳入默認風格名稱即可將整塊垂直顯示區域添加一個類似文本框的背景以區分其他區域 2 EditorGUILayout.BeginVertical("textfield"); 3 ... 4 EditorGUILayout.EndVertical(); 5 6 //將按鈕風格顯示成工具欄按鈕的樣式有時可能會更美觀 7 if (GUILayout.Button("My Button", EditorStyles.toolbarButton)) 8 { 9 } 10 11 //把單選的組按鈕 button 改成選框 toggle 樣式 12 GUILayout.SelectionGrid(m_selectedIndex, m_Names, 1, EditorStyles.toggleGroup)
獲得一個內置的默認風格有三種方式:"textfield"
、GUI.skin.textField
、EditorStyles.textField
。
文本內容想要其中部分文字添加一個顏色以突出顯示,需要開啟富文本支持 myGUIStyle.richText = true;
由於 Unity 編譯順序決定了 Runtime 腳本是無法調用 Editor 代碼的,有些邏輯因歷史原因不方便修改,但非要運行時調用編輯器腳本,有個辦法是在編輯器腳本初始化時去綁定運行時腳本中的靜態委托或事件:
1 public class MyRuntimeScript : MonoBehaviour 2 { 3 #if UNITY_EDITOR 4 public static System.Action<GameObject> onEvent; 5 #endif 6 ... 7 #if UNITY_EDITOR 8 if (onEvent != null) 9 onEvent(gameObject); 10 #endif 11 } 12 13 public class MyEditorScript 14 { 15 [InitializeOnLoadMethod] 16 static void Init() 17 { 18 MyRuntimeScript.onEvent = go => 19 { 20 ... 21 }; 22 }
有時需要在自動布局(GUILayout/EditorGUILayout)中插入固定布局(GUI/EditorGUI)控件,如以 UV 坐標顯示一張圖片時只有固定布局接口 GUI.DrawTextureWithTexCoords,需要傳入固定布局接口一個 Rect,但此時很難確定自動布局下當前位置坐標與大小,可調用 Rect rect = GUILayoutUtility.GetRect(new GUIContent(), GUIStyle.none);
或 Rect rect = GUILayoutUtility.GetRect(0f, 10f, GUILayout.ExpandWidth(true));
獲得一片當前空間可用的一塊區域,后續可基於該 rect 進行坐標計算。另 rect.Contains(Event.current.mousePosition)
可判斷鼠標是否在某區域內。
GUILayout.FlexibleSpace()
可以將空白區域全部占滿。自動布局 Layout.XXX 控件默認是會占據盡量大的空間(通常是整個窗口的寬度),連續兩個控件想一個在最左邊一個最右邊時,在之間插入該調用即可,同理三個控件之間插入即可實現平均占據整行空間的排列,這僅靠 BeginHorizontal() 或 BeginVertical() 組合是比較難實現的。