Unity裏面比較出色我也很喜歡的一個功能就是它易於拓展的編輯器。
一般來說拓展編輯器對於遊戲運行效率不是有什麼大的幫助,但是有助於開發效率的提高。
畢竟工欲善其事,必先利其器。
這次介紹一共以下這些拓展編輯器的方法:
-
OnDrawGizmos
-
OnInspectorGUI
-
OnSceneGUI
-
MenuItem 與 EditorWindow
-
ScriptableWizard
-
ScriptObject
-
Attributes
-
AssetProcess
OnDrawGizmos
OnDrawGizmos是在MonoBehaviour下的一個方法,通過這個方法可以可以繪製出一些Gizmos來使得其一些參數方便在Scene窗口查看。
比如我們有一個沿着路點移動的平臺,一般的操作可能是生成一堆新的子物體來確定和設置位置,但其實這樣會有點贅餘,我們需要的只是一個Vector2/Vector3數組。而這個時候我們就可以通過OnDrawGizmos方法在編輯器繪製出這些Vector2/Vector3的數組點。
OnDrawGizmos可以繪製出路點
完整代碼如下:
public class DrawGizmoTest : MonoBehaviour { public Vector2[] poses; private void OnDrawGizmos() { Color originColor = Gizmos.color; Gizmos.color = Color.red; if( poses!=null && poses.Length>0 ) { //Draw Sphere for (int i = 0; i < poses.Length; i++) { Gizmos.DrawSphere( poses[i], 0.2f ); } //Draw Line Gizmos.color = Color.yellow; Vector2 lastPos = Vector2.zero; for (int i = 0; i < poses.Length; i++) { if( i > 0 ) { Gizmos.DrawLine( lastPos, poses[i] ); } lastPos = poses[i]; } } Gizmos.color = originColor; } }
OnInspectorGUI
在開發過程中常常需要在編輯器上對某個特定的Component進行一些操作,比如在Inspector界面上有一個按鈕可以觸發一段代碼。
Inspector界面上有一個按鈕可以觸發一段代碼
這種屬於編輯器的,所以一般是在Editor文件夾中新建一個繼承自Editor的腳本:
在Editor文件夾中新建一個繼承自Editor的腳本
之後編輯繼承自UnityEditor.Editor,這裏注意是必須在類上加入[CustomEditor(typeof(編輯器腳本綁定的Monobehavior類)]然後重寫它的OnInspectorGUI方法:
using UnityEditor; [CustomEditor(typeof(InspectorTest))] public class InspectorTestEditor : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); if(GUILayout.Button("Click Me")) { //Logic InspectorTest ctr = target as InspectorTest; } } }
而一般而言在Editor類中操作變量有兩種方式,一種是通過直接訪問或者函數調用改動Monobehaviour的變量,一種是通過Editor類中的serializedObject來改動對應變量。
比如我要把Monobehaviour的一個公開的Name改成Codinggamer
使用方法一,在Editor中可以這樣寫:
if(GUILayout.Button("Click Me")) { //Logic InspectorTest ctr = target as InspectorTest; ctr.Name = "Codinggamer"; }
在編輯器中點擊會發現Hierarchy界面沒有出現一般改動之後會出現的小星星:
點擊按鈕沒有出現一般改動之後會出現的小星星
一般改動是會出現小星星
如果這個時候你重新打開場景,會發現改動的值又便回原來的值,也就是你的改動並沒有生效。
而此時,只需要再調用EditorUtility.SetDirty( Object )方法即可。
如果要使用方法二,則需要在Editor代碼中寫:
if(GUILayout.Button("Click Me")) { //Logic serializedObject.FindProperty("Name").stringValue = "Codinggamer"; serializedObject.ApplyModifiedProperties(); }
這裏不需要調用EditorUtility.SetDirty( Object )方法,場景就已經會出現改動之後的小星星,保存重開場景之後也會發現對應值生效。
這兩個方法孰優孰劣?
一般來說用第二個方法比較好,但實際上涉及邏輯比較多的時候我是用第一個方法。用第二個方法的好處在於它是內置了撤銷功能,也就意味着你調用改動之後是可以直接撤銷掉的,而第一個方法就不能。
OnSceneGUI
這個方法也是在Editor類中的一個方法,是用來在Scene視圖上顯示一個UI元素。其創建也是在Editor文件夾下新建一個繼承自Editor的腳本:
也是在Editor文件夾下新建一個繼承自Editor的腳本
在OnSceneGUI中可以做出和OnDrawGizmo類似的功能,比如繪製出Vector2數組的路點:
OnSceneGUI也可以做出路點功能
其代碼如下:
using UnityEngine; using UnityEditor; [CustomEditor(typeof(SceneGUITest))] public class SceneGUITestEditor : Editor { private void OnSceneGUI() { Draw(); } void Draw() { //Draw a sphere SceneGUITest ctr = target as SceneGUITest; Color originColor = Handles.color; Color circleColor = Color.red; Color lineColor = Color.yellow; Vector2 lastPos = Vector2.zero; for (int i = 0; i < ctr.poses.Length; i++) { var pos = ctr.poses[i]; Vector2 targetPos = ctr.transform.position; //Draw Circle Handles.color = circleColor; Handles.SphereHandleCap( GUIUtility.GetControlID(FocusType.Passive ) , targetPos + pos, Quaternion.identity, 0.2f , EventType.Repaint ); //Draw line if(i > 0) { Handles.color = lineColor; Handles.DrawLine( lastPos, pos ); } lastPos = pos; } Handles.color = originColor; } }
OnDrawGizmos與OnSceneGUI的區別
因爲OnSceneGUI是在Editor上的方法,而Editor一般都是對應Monobehaviour,這意味它是隻能是點擊到對應物體纔會生成的。而OnDrawGizmos則是可以全局可見。
而如果需要事件處理,比如需要在Scene界面可以直接點擊增加或者修改這些路點,就需要在OnSceneGUI上處理事件來進行一些操作。
OnSceneGUI可以進行事件處理
完整的代碼如下,這裏注意的是原來的poses爲了方便改用成了List<Vector2>類型:
using UnityEngine; using UnityEditor; [CustomEditor(typeof(SceneGUITest))] public class SceneGUITestEditor : Editor { protected SceneGUITest ctr; private void OnEnable() { ctr = target as SceneGUITest; } private void OnSceneGUI() { Event _event = Event.current; if( _event.type == EventType.Repaint ) { Draw(); } else if ( _event.type == EventType.Layout ) { HandleUtility.AddDefaultControl( GUIUtility.GetControlID( FocusType.Passive ) ); } else { HandleInput( _event ); HandleUtility.Repaint(); } } void HandleInput( Event guiEvent ) { Ray mouseRay = HandleUtility.GUIPointToWorldRay( guiEvent.mousePosition ); Vector2 mousePosition = mouseRay.origin; if( guiEvent.type == EventType.MouseDown && guiEvent.button == 0 ) { ctr.poses.Add( mousePosition ); } } void Draw() { //Draw a sphere Color originColor = Handles.color; Color circleColor = Color.red; Color lineColor = Color.yellow; Vector2 lastPos = Vector2.zero; for (int i = 0; i < ctr.poses.Count; i++) { var pos = ctr.poses[i]; Vector2 targetPos = ctr.transform.position; //Draw Circle Handles.color = circleColor; Vector2 finalPos = targetPos + new Vector2( pos.x, pos.y); Handles.SphereHandleCap( GUIUtility.GetControlID(FocusType.Passive ) , finalPos , Quaternion.identity, 0.2f , EventType.Repaint ); //Draw line if(i > 0) { Handles.color = lineColor; Handles.DrawLine( lastPos, pos ); } lastPos = pos; } Handles.color = originColor; } }
MenuItem與EditorWindow
MenuItem可以說是用得最多的了,它的作用是編輯器上菜單項,一般用於一些快捷操作,比如交換兩個物體位置:
MenuItem可以做一些快捷操作,比如交換兩個物體位置
由於是涉及編輯器的代碼,所以依然可以放在Editor文件夾下面,具體代碼如下:
using UnityEditor; using UnityEngine; public class MenuCommand { [MenuItem("MenuCommand/SwapGameObject")] protected static void SwapGameObject() { //只有兩個物體才能交換 if( Selection.gameObjects.Length == 2 ) { Vector3 tmpPos = Selection.gameObjects[0].transform.position; Selection.gameObjects[0].transform.position = Selection.gameObjects[1].transform.position; Selection.gameObjects[1].transform.position = tmpPos; //處理兩個以上的場景物體可以使用MarkSceneDirty UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty( UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene() ); } } }
EditorWindow
EditorWindow在Unity引擎中的應用也算是比較多,比如Animation、TileMap和Animitor窗口應該就是用到了EditorWindow。創建方法仍然是在Editor文件夾中創建一個繼承自EditorWindow的腳本。EditorWindow有一個GetWindow的方法,調用之後如果當前沒有這個窗口會返回新的,如果有就返回當前窗口,之後調用Show即可展示這個窗口。可以使用MenuItem來顯示這個EditorWindow,重寫OnGUI方法即可以寫Editor的UI:
using UnityEngine; using UnityEditor; namespace EditorTutorial { public class EditorWindowTest : EditorWindow { [MenuItem("CustomEditorTutorial/WindowTest")] private static void ShowWindow() { var window = GetWindow(); window.titleContent = new GUIContent("WindowTest"); window.Show(); } private void OnGUI() { if(GUILayout.Button("Click Me")) { //Logic } } } }
之後點擊編輯器的Menu就會有這個EditorWindow出來:
點擊編輯器的Menu就會有這個EditorWindow
EditorWindow的UI的寫法跟OnInspectorGUI的寫法差不多,基本是GUILayout和EditorGUILayout這兩個類。
EditorWindow與OnInspectorGUI的差別
最主要的差別是EditorWindow可以停靠的在邊欄上,不會因爲你點擊一個物體就重新生成。而OnInspectorGUI的Editor類在你每次切換點擊時候都會調用OnEnable方法。
EditorWindow如何繪製Scene界面UI
在EditorWindow中如果需要對Scene繪製一些UI,這個時候使用Editor那種OnSceneGUI是無效的,這個時候則需要在Focus或者OnEnable時候加入SceneView的事件回調中,並且在OnDestroy時候去除該回調:
private void OnFocus() { //在2019版本是這個回調 SceneView.duringSceneGui -= OnSceneGUI; SceneView.duringSceneGui += OnSceneGUI; //以前版本回調 // SceneView.onSceneGUIDelegate -= OnSceneGUI // SceneView.onSceneGUIDelegate += OnSceneGUI } private void OnDestroy() { SceneView.duringSceneGui -= OnSceneGUI; } private void OnSceneGUI( SceneView view ) { }
ScriptWizard
Unity引擎的中的BuildSetting窗口(Ctr+Shift+B彈出的窗口)就是使用了SciptWizard,一般來開發過程中作爲比較簡單的生成器和初始化類型的功能來使用,比如美術給我一個序列幀,我需要直接生成一個帶SpriteRenderer的GameObject,而且它還有自帶序列幀的Animator。
ScriptObject默認的顯示樣式
其創建過程還是在Editor文件夾下創建一個繼承自ScriptWizard的腳本,調用ScriptWizard.DisplayWizard方法即可生成並顯示這個窗口,點擊右下角的Create會調用OnWizardCreate方法:
public class TestScriptWizard: ScriptableWizard { [MenuItem("CustomEditorTutorial/TestScriptWizard")] private static void MenuEntryCall() { DisplayWizard("Title"); } private void OnWizardCreate() { } }
ScriptWizard與EditorWindow的區別
在ScriptWizard中如果你聲明一個Public的變量,會發現在窗口可以直接顯示,但是在EditorWindow則是不能顯示。
ScriptObject
對於遊戲中一些數據和配置可以考慮用ScriptObject來保存,雖然XML之流也可以,但是ScriptObject相對比較簡單而且可以保存UnityObject比如Sprite、Material這些。甚至你會發現上面說的幾個類都是繼承自SctriptObject。 因爲其不再是隻適用編輯器,所以不必放在Editor文件夾下 。
與SctiptWizard類似,也是聲明Public可以在窗口上直接看到,自定義繪製GUI也是在OnGUI方法裏面:
[CreateAssetMenu(fileName = "TestScriptObject", menuName = "CustomEditorTutorial/TestScriptObject", order = 0)] public class TestScriptObject : ScriptableObject { public string Name; }
使用CreateAssetMenu的Attribute的作用是使得其可以在Project窗口中右鍵生成:
可以在Project窗口中右鍵生成
ScriptObject與System.Serializable的差別
初學者可能會對這兩個比較困擾(我一開始就比較困擾),一開始我把SctiptObject拖拽到Monobehaviour上面發現其不會顯示出ScriptObject的屬性
SctiptObject拖拽到Monobehaviour上不會顯示出ScriptObject的屬性
然後我在SctiptObject上面加上[System.Serializable],也是沒用:
[CreateAssetMenu(fileName = "TestScriptObject", menuName = "CustomEditorTutorial/TestScriptObject", order = 0)] [System.Serializable] public class TestScriptObject : ScriptableObject { public string Name; }
所以是在ScriptObject上面使用[ System.Serializable ]是不可取的,[ System.Serializable]適合於普普通通的Class,比如:
[System.Serializable] public class Data { string Name; }
ScriptObject上面調用編輯器修改需要調用EditorUtility.SetDirty,不可調用EditorSceneManager.MarkSceneDirty
因爲MarkSceneDirty顧名思義是標記場景爲已修改,但是編輯ScriptObject並不屬於場景內數據,所以如果修改只可調用EditorUtility.SetDirty,不然會造成數據改動未生效。
Attributes
Attributes是C#的一個功能,它可以讓聲明信息與代碼相關聯,其與C#的反射聯繫很緊密。在Unity中諸如[System.Serializable],[Header],[Range]都是其的應用。一般來說他它功能也可以通過Editor來實現,但是可以繪製對應的屬性來說會更好複用。
拓展Attribute相對來說稍微複雜一點,它涉及兩個類:PropertyAttribute和PropertyDrawer,前者是定義它行爲,後者主要是其在編輯器的顯示效果。一般來說Attribute是放在Runtime,而Drawer則是放在Editor文件夾下。這裏的例子是加入[Preview]的Attribute,使得我們拖拽Sprite或者GameObject可以顯示預覽圖:
加入[Preview]的Attribute,使得拖拽Sprite或者GameObject可以顯示預覽圖
使用時候的代碼如下:
public class AttributeSceneController : MonoBehaviour { [Preview] public Sprite sprite; }
我們現在Runtime層的文件夾加入繼承自PropertyAttribute的PreviewAttribute腳本:
public class Preview : PropertyAttribute { public Preview() { } }
然後在Editor文件夾下加入繼承自PropertyDrawer的PreviewDrawer腳本:
using UnityEngine; using UnityEditor; namespace EditorTutorial { [CustomPropertyDrawer(typeof(Preview))] public class PreviewDrawer: PropertyDrawer { //調整整體高度 public override float GetPropertyHeight( SerializedProperty property, GUIContent label ) { return base.GetPropertyHeight( property, label ) + 64f; } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); EditorGUI.PropertyField(position, property, label); // Preview Texture2D previewTexture = GetAssetPreview(property); if( previewTexture != null ) { Rect previewRect = new Rect() { x = position.x + GetIndentLength( position ), y = position.y + EditorGUIUtility.singleLineHeight, width = position.width, height = 64 }; GUI.Label( previewRect, previewTexture ); } EditorGUI.EndProperty(); } public static float GetIndentLength(Rect sourceRect) { Rect indentRect = EditorGUI.IndentedRect(sourceRect); float indentLength = indentRect.x - sourceRect.x; return indentLength; } Texture2D GetAssetPreview( SerializedProperty property ) { if (property.propertyType == SerializedPropertyType.ObjectReference) { if (property.objectReferenceValue != null) { Texture2D previewTexture = AssetPreview.GetAssetPreview(property.objectReferenceValue); return previewTexture; } return null; } return null; } } }
這裏是對屬性的一些繪製,實際開發過程中我們常常需要一些可交互的UI,比如在方法上面加一個[Button]然後在編輯器暴露出一個按鈕出來,具體的例子可以參考NaughtyAttribute
AssetPostprocessor
在開發過程中常常會遇到資源導入問題,比如我製作像素遊戲圖片要求是FilterMode爲Point,圖片不需要壓縮,PixelsPerUnit爲16,如果每次複製到一個圖片到項目再修改會很麻煩。這裏一個解決方案是可以用MenuItem來處理,但還需要多點幾下,而使用AssetPostprocessor則可以自動處理完成。
在Editor文件夾下新建一個繼承自AssetPostprocessor的腳本:
public class TexturePipeLine : AssetPostprocessor { private void OnPreprocessTexture() { TextureImporter importer = assetImporter as TextureImporter; if( importer.filterMode == FilterMode.Point ) return; importer.spriteImportMode = SpriteImportMode.Single; importer.spritePixelsPerUnit = 16; importer.filterMode = FilterMode.Point; importer.maxTextureSize = 2048; importer.textureCompression = TextureImporterCompression.Uncompressed; TextureImporterSettings settings = new TextureImporterSettings(); importer.ReadTextureSettings( settings ); settings.ApplyTextureType( TextureImporterType.Sprite ); importer.SetTextureSettings( settings ) ; } }
之後再導入一個像素圖片發現就已經全部設置好了:
導入圖片自動設置
可以想象的一些使用場景是可以根據XML和SpriteSheet來實現自動生成動畫或者自動切圖,解析PSD或ASE自動導入PNG。
其他一些值得注意的地方
Undo
在之前說過在Editor裏面直接改動原來的Monobehaviour腳本是變量是無法撤銷的,但是使用 serializedObject來修改則可以撤銷。這裏可以自己寫一個Undo來記錄使其可以撤銷,代碼如下:
if(GUILayout.Button("Click Me")) { InspectorTest ctr = target as InspectorTest; //記錄使其可以撤銷 Undo.RecordObject( ctr ,"Change Name" ); ctr.Name = "Codinggamer"; EditorUtility.SetDirty( ctr ); }
SelectionBase
當你的類中使用[SelectionBase]的Attribute時候,如果你點擊其子節點下的物體,其仍然只會聚焦這個父節點。
使用SelectionBase之後點擊子節點仍然選中父節點
不在Editor文件夾裏面寫編輯器代碼
有的時候我們Monobehaviour本身很短小,拓展InspectorGUI的代碼也很短小,沒有必要在Editor上創建一個新的腳本,可以直接使用#UNITY_EDITOR 的宏來創建一個拓展編輯器,比如之前的拓展InspectorGUI可以這樣寫:
using System.Collections; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace EditorTutorial { public class InspectorTest : MonoBehaviour { public string Name = "hello"; } #if UNITY_EDITOR [CustomEditor(typeof(InspectorTest))] public class InspectorTestEditor : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); if(GUILayout.Button("Click Me")) { InspectorTest ctr = target as InspectorTest; //記錄使其可以撤銷 Undo.RecordObject( ctr ,"Change Name" ); ctr.Name = "Codinggamer"; EditorUtility.SetDirty( ctr ); } } } #endif }
EditorWindow和Editor保存數據
這裏需要使用EditorPrefs來保存和讀取數據,在需要保存的數據上面加上[System.SerializeField]的Attribute,然後在OnEnable和OnDisable時候可以保存或者度序列化json:
[SerializeField] public string Name = "Hi"; private void OnEnable() { var data = EditorPrefs.GetString( "WINDOW_KEY", JsonUtility.ToJson( this, false ) ); JsonUtility.FromJsonOverwrite( data, this ); } private void OnDisable() { var data = JsonUtility.ToJson( this, false ); EditorPrefs.SetString("WINDOW_KEY", data); }
感覺這種方法也可以在運行時序列化腳本保存到本地。
在代碼中檢索對應MonoBehaviour的Editor類
上面說過,在編輯器代碼中一般比較多使用SerializedObject,像Editor類中就內置了serializedObject。實際上所有繼承自SctiptObject或者Monobehaviour的腳本都可以生成SerializedObject。其生成方式很簡單只需要new的時候傳入你需要序列化的組件即可:
SerializedObject serialized = new SerializedObject(this);
後記
這篇文章中並沒有涉及比較多的API的使用,更多的是想展現出可以用拓展編輯器來做什麼以及當你想做一些拓展時候需要從哪裏入手。比如如果你想給美術人員做一個快捷生成角色的工具,就可以使用ScriptWizard。如果你需要讓美術人員和設計師更加方便地調整人物屬性,則可以考慮使用Editor。如果你需要給關卡設計師製作一個關卡編輯器,那可以考慮使用EditorWindow。
寫得比較多,以上這個就是我在使用Unity拓展編輯器的總結與遇到的一些問題的經驗。
示例Github倉庫: UnityEditorTutorial