Unity Custom Editor
Unity中可以通過編寫腳本實現自定義的Editor。好用Editor可以使開發事半功倍。 Asset Store上大部分第三方插件如NGUI、PlayMaker等都包含自定義Editor的部分。 自定義Editor主要包含兩部分:Editor Window和Custom Editor。
顧名思義就是一個單獨窗口的Editor。
簡單的例子在官方網站上可以找到,這裏不再贅述。
這裏討論幾個比較重要的部分:
1, 區域劃分
Unity提供了EditorGUILayout.BeginHorizontal()
、EditorGUILayaout.BeginVertical()
這樣的方法,用於聲明一個元素是橫向/縱向排列的新block。 其用法類似於HTML中的<table><tr>
、<table><td>
。 當然,不要忘記在結束一個區域的時候調用EditorGUILayout.EndHorizontal()
和EditorGUILayaout.EndVertical()
。 特別是在代碼中有break, return的時候要注意。 區域不閉合Unity會報錯的。
2, 格式調整與樣式定義
一般有兩種方法:GUILayoutOption和GUIStyle
- Height
- Width
- MaxHeight
- MaxWidth
- MinHeight
- MinWidth
- ExpandHeight
- ExpandWidth
基本上就是以上這幾種。涵蓋了默認長寬,最大/最小長寬,是否可以拉伸長寬等等設定。 如果對於格式要求不多的話以上基本夠用。 用法如下:
EditorGUILayout.BeginVertical(
GUILayout.Width(blockWidth),
GUILayout.ExpandHeight(true)
);
如果一定要像在HTML裏一樣自定義Margin, Border, Padding, OnHover, OnFocus…那麼推薦使用GUIStyle。 使用方法是新建一個GUISkin,然後編輯,最後在代碼中載入編輯好的GUISkin中對應的樣式。 GUIStyle可以滿足對於自定義樣式的種種需求,但是缺點是需要很多“準備工作”,並不像GUILayoutOption一樣拿來就能用。 例子如下:
GUISkin editorGUISkin = (GUISkin)Resources.Load("EditorGUISkin");
GUIStyle blockStyle = editorGUISkin.customStyles[0];
EditorGUILayout.BeginVertical(blockStyle, GUILayout.Width(stateListBlockWidth));
3, 事件處理
這裏主要討論鼠標事件處理。
Event.current.Type
當前事件的類型,典型的有
- EventType.mouseDown
- EventType.mouseUp
- EventType.mouseDrag
- EventType.scrollWheel
Event.current.button
當前按鍵類型,默認值爲
- 0 => 鼠標左鍵
- 1 => 鼠標右鍵
- 2 => 鼠標中鍵
Event.current.delta
這是一個Vector2的變量,可以用在處理鼠標滾輪事件上。 判斷Event.current.delta.y的值即可。
以上的變量結合起來使用基本上可以編寫出大部分處理鼠標事件的代碼。 當然Editor Window也可以處理鍵盤事件,但是用的不是很廣泛這裏不復贅述。
重繪的時機
Editor Window的窗口繪製基本上在OnGUI()函數中進行,而OnGUI函數並不是每一幀都被調用的。 通常只有滿足了重繪條件時纔會被調用,比如將鍵盤焦點彙集到窗口上或者元素被更新等等。 也可以在腳本中通過this.Repaint()手動觸發重繪操作。
因此想要使得Editor Window擁有更加流暢的體驗則必須注意觸發重繪函數的時機。 實際上Unity本身提供的組件如SlideBar, ScrollView等等都不需要進行特殊的設置即可滿足我們的要求。 問題在於一些自定義的UI組件。 如果一個自定義的UI組件在相應各類事件時有卡頓的現象,則說明其重繪的時機可能有問題。 一般而言,是重繪沒有跟上UI元素的更新,解決方法是在想要更新UI組件表現的時候使用Event.current.Use()函數。
Event.current.Use()
這個函數的作用是“喫掉”當前的事件,使得在這行代碼之下的代碼無法繼續使用這個事件。(會看到Event.current.Type == EventType.Used) 同時,在本次OnGUI執行完成後會立即進行重繪,及時更新UI的信息。 很多自定義組件的Drag操作,如果不使用Event.current.Use()函數都會變的很不自然,就是因爲更新了UI信息(比如位置)之後沒有及時觸發重繪操作導致的。
4,例子——可變尺寸區域
按照上述方法基本可以很快的劃分好窗口的區域並將各個元素放到指定的位置。 相比於Unity的默認窗口,使用Unity提供的默認組件無法簡單的構建一個可以自動調節大小的區域。 以下的例子實現了一個可以調節大小的區域。
void drawVerticalResizeBlock(ref float parameterToResize)
{
float blockWidth = 0f;
Rect blockRect = EditorGUILayout.BeginVertical(
GUILayout.Width(blockWidth),
GUILayout.ExpandHeight(true)
);
EditorGUILayout.EndVertical();
Rect resizeBlockRect =
new Rect(
blockRect.xMin - resizeDetectSize,
blockRect.yMin,
blockRect.width + 2 * resizeDetectSize,
blockRect.height
);
EditorGUIUtility.AddCursorRect(resizeBlockRect, MouseCursor.ResizeHorizontal);
if (resizeBlockRect.Contains(Event.current.mousePosition))
{
if (Event.current.type == EventType.mouseDown)
{
mouseOffset = Event.current.mousePosition.x - parameterToResize;
Event.current.Use();
}
if (Event.current.type == EventType.mouseDrag)
{
parameterToResize = Event.current.mousePosition.x - mouseOffset;
Event.current.Use();
}
}
}
void drawHorizontalResizeBlock(ref float parameterToResize)
{
float blockHeight = 0f;
Rect blockRect = EditorGUILayout.BeginHorizontal(
GUILayout.Height(blockHeight),
GUILayout.ExpandWidth(true)
);
EditorGUILayout.EndHorizontal();
Rect resizeBlockRect =
new Rect(
blockRect.xMin,
blockRect.yMin - resizeDetectSize,
blockRect.width,
blockRect.height + 2 * resizeDetectSize
);
EditorGUIUtility.AddCursorRect(resizeBlockRect, MouseCursor.ResizeVertical);
if (resizeBlockRect.Contains(Event.current.mousePosition))
{
if (Event.current.type == EventType.mouseDown)
{
mouseOffset = Event.current.mousePosition.y - parameterToResize;
Event.current.Use();
}
if (Event.current.type == EventType.mouseDrag)
{
parameterToResize = Event.current.mousePosition.y - mouseOffset;
Event.current.Use();
}
}
}
Custom Editor包含Inspector、Scene等部分,這裏主要討論Custom Inspector的部分。
1, OnInspectorGUI與DrawDefaultInspector
Custom Editor的使用方法與Editor Window大同小異。 需要注意的是Inspector的繪製是在OnInspectorGUI函數中完成的。 此外,所繼承的基類不是EditorWindow而是UnityEditor.Editor。 這個類中提供了一個DrawDefaultInspector的函數,調用即可繪製默認的Inspector。
2,serializedObject與Target
Inspector的主要用途在於編輯其所依附的MonoBehavior的數據。 編輯的方法通常有兩種。
OnInspectorGUI中可以操作一個名爲serializedObject的對象。 這個對象即爲MonoBehavior序列化之後的實例。 通過調用serializedObject.FindProperty(“PropertyName”)可以得到一個類型爲SerializedProperty的屬性。 可以通過把該屬性綁定到編輯框裏(一般是PropertyField)來實現在Inspector上編輯該屬性的效果。 實例如下:
// 更新serializedObject的值
serializedObject.Update();
SerializedProperty sp = serializedObject.FindProperty("intField");
EditorGUILayout.PropertyField(sp, new GUIContent("IntField"), GUILayout.Width("100"));
// 將更新的值寫回
serializedObject.ApplyModifiedProperties();
target
一般的默認類型屬性都可以通過serializedObject + PropertyField搞定。 但是對於自定義類型的屬性有些時候行不通,這時候需要依靠target對象。 例子如下:
CustomClass cc = target as CustomClass;
cc.IntField = 30;
cc.CustomMethod("Hello World");
//提示Unity更新target信息
EditorUtility.SetDirty(target);
自定義Editor的其他玩法
除了可以自定義Editor Window和Inspector,還可以在UnityEditor.Editor類裏實現OnSceneGUI函數自定義SceneView中的表現。 篇幅所限,這裏不加贅述,有興趣的讀者可以參考文檔。