Unity3D提供了強大的編輯器擴展機制,在項目開發中,如果可以將一些繁瑣的工作放在編輯器擴展中進行,則會大大提高效率。本文對編輯器擴展進行了一些總結,希望對有興趣編寫編輯器擴展的開發人員有所幫助。當我們編寫一個編輯器擴展時,一般可以從以下四個類繼承:
1 . ScriptableObject
最常見的小功能擴展,一般不用窗口的編輯擴展,可以從這個類中繼承,如以下代碼所示:
using UnityEngine; using UnityEditor; using System.Collections; public class AddChild : ScriptableObject { [MenuItem ("GameObject/Add Child ^n")] static void MenuAddChild() { Transform[] transforms = Selection.GetTransforms(SelectionMode.TopLevel | SelectionMode.OnlyUserModifiable); foreach(Transform transform in transforms) { GameObject newChild = new GameObject("_Child"); newChild.transform.parent = transform; } } }
這個擴展腳本從菜單的“GameObject->Add Child”啟動,功能是給Hierarchy窗口中選中的對GameObject添加一個名字為“_Child”的子GameObject,這樣可以免去從Hierarchy窗口的根節點拖拽新創建的GameObject到當前選中節點的麻煩,因為在Unity3D編輯器中,創建一個EmptyObject會在Hierarchy窗口的根節點出現,無論當前選中的節點對象是哪個。
2 .ScriptableWizard
需要對擴展的參數進行設置,然后再進行功能觸發的,可以從這個類進行派生。它已經定制好了四個消息響應函數,開發者對其進行填充即可。
(1) OnWizardUpdate
當擴展窗口打開時或用戶對窗口的內容進行改動時,會調用此函數。一般會在這里面顯示幫助文字和進行內容有效性驗證;
(2)OnWizardCreate
這是用戶點擊窗口的Create按鈕時進行的操作,從ScriptableWizard的名字可以看出,這是一種類似向導的窗口 ,而這種窗口我們在Visual Studio中經常會使用到,如下圖:
只不過Unity3D中的ScriptableWizard窗口只能進行小於或等於兩個按鈕的定制,一個就是所謂的Create按鈕,另外一個則籠統稱之為Other按鈕。ScriptableWizard.DisplayWizard這個靜態函數用於對ScriptableWizard窗口標題和按鈕名字的定制。
(3) OnDrawGizmos
在窗口可見時,每一幀都會調用這個函數。在其中進行Gizmos的繪制,也就是輔助編輯的線框體。Unity的Gizmos類提供了DrawRayDrawLine ,DrawWireSphere ,DrawSphere ,DrawWireCube ,DrawCubeDrawIcon ,DrawGUITexture 功能。這個功能在Unity3D 的3.4版本中測試了一下,發現沒有任何Gizmos繪制出來
(4) OnWizardOtherButton
本文在(2) 中已經提及,ScriptableWizard窗口最多可以定制兩個按鈕,一個是Create,另外一個稱之為Other,這個函數會在other按鈕被點擊時調用。下面是一個使用ScriptableWizard進行編輯擴展的例子:
<span style="font-size: 18px;">using UnityEditor; using UnityEngine; using System.Collections; /// <summary> /// 對於選定GameObject,進行指定component的批量添加 /// </summary> public class AddRemoveComponentsRecursively : ScriptableWizard { public string componentType = null; /// <summary> /// 當沒有任何GameObject被選中的時候,將菜單disable(注意,這個函數名可以隨意取) /// </summary> /// <returns></returns> [MenuItem("GameObject/Add or remove components recursively...", true)] static bool CreateWindowDisabled() { return Selection.activeTransform; } /// <summary> /// 創建編輯窗口(注意,這個函數名可以隨意取) /// </summary> [MenuItem("GameObject/Add or remove components recursively...")] static void CreateWindow() { // 定制窗口標題和按鈕,其中第二個參數是Create按鈕,第三個則屬於other按鈕 // 如果不想使用other按鈕,則可調用DisplayWizard的兩參數版本 ScriptableWizard.DisplayWizard<AddRemoveComponentsRecursively>( "Add or remove components recursivly", "Add", "Remove"); } /// <summary> /// 窗口創建或窗口內容更改時調用 /// </summary> void OnWizardUpdate() { helpString = "Note: Duplicates are not created"; if (string.IsNullOrEmpty(componentType)) { errorString = "Please enter component class name"; isValid = false; } else { errorString = ""; isValid = true; } } /// <summary> /// 點擊Add按鈕(即Create按鈕)調用 /// </summary> void OnWizardCreate() { int c = 0; Transform[] ts = Selection.GetTransforms(SelectionMode.Deep); foreach (Transform t in ts) { if (t.gameObject.GetComponent(componentType) == null) { if (t.gameObject.AddComponent(componentType) == null) { Debug.LogWarning("Component of type " + componentType + " does not exist"); return; } c++; } } Debug.Log("Added " + c + " components of type " + componentType); } /// <summary> /// 點擊Remove(即other按鈕)調用 /// </summary> void OnWizardOtherButton() { int c = 0; Transform[] ts = Selection.GetTransforms(SelectionMode.Deep); foreach (Transform t in ts) { if (t.GetComponent(componentType) != null) { DestroyImmediate(t.GetComponent(componentType)); c++; } } Debug.Log("Removed " + c + " components of type " + componentType); Close(); } }</span>
其運行窗口如下所示:
3 . EditorWindow
較復雜的功能,需要多個靈活的控件,實現自由浮動和加入其他窗口的tab,可以從這個類派生,這種窗口的窗體功能和Scene,Hierarchy等窗口完全一致。下面這個例子實現了GameObject的空間對齊和拷貝(也就是將GameObject A作為基准,選中其他的GameObject進行對准或空間位置拷貝),對齊和拷貝提高了了開發者擺放物件的效率;另外還有隨機和噪聲,后兩者用於擺放大量同類物件的時候可以使用,比如一大堆散落的瓶子。
<span style="font-size: 18px;">// ///////////////////////////////////////////////////////////////////////////////////////////////////////// // // Transform Utilities. // // This window contains four useful tools for asset placing and manipulation: Align, Copy, Randomize and Add noise. // // Put this into Assets/Editor and once compiled by Unity you find // the new functionality in Window -> TransformUtilities, or simply press Ctrl+t (Cmd+t for Mac users) // // Developed by Daniel // http://www.silentkraken.com // e-mail: seth@silentkraken.com // // ///////////////////////////////////////////////////////////////////////////////////////////////////////// using UnityEngine; using UnityEditor; public class TransformUtilitiesWindow : EditorWindow { //Window control values public int toolbarOption = 0; public string[] toolbarTexts = {"Align", "Copy", "Randomize", "Add noise"}; private bool xCheckbox = true; private bool yCheckbox = true; private bool zCheckbox = true; private Transform source; private float randomRangeMin = 0f; private float randomRangeMax = 1f; private int alignSelectionOption = 0; private int alignSourceOption = 0; /// <summary> /// Retrives the TransformUtilities window or creates a new one /// </summary> [MenuItem("Window/TransformUtilities %t")] static void Init() { TransformUtilitiesWindow window = (TransformUtilitiesWindow)EditorWindow.GetWindow(typeof(TransformUtilitiesWindow)); window.Show(); } /// <summary> /// Window drawing operations /// </summary> void OnGUI () { toolbarOption = GUILayout.Toolbar(toolbarOption, toolbarTexts); switch (toolbarOption) { case 0: CreateAxisCheckboxes("Align"); CreateAlignTransformWindow(); break; case 1: CreateAxisCheckboxes("Copy"); CreateCopyTransformWindow(); break; case 2: CreateAxisCheckboxes("Randomize"); CreateRandomizeTransformWindow(); break; case 3: CreateAxisCheckboxes("Add noise"); CreateAddNoiseToTransformWindow(); break; } } /// <summary> /// Draws the 3 axis checkboxes (x y z) /// </summary> /// <param name="operationName"></param> private void CreateAxisCheckboxes(string operationName) { GUILayout.Label(operationName + " on axis", EditorStyles.boldLabel); GUILayout.BeginHorizontal(); xCheckbox = GUILayout.Toggle(xCheckbox, "X"); yCheckbox = GUILayout.Toggle(yCheckbox, "Y"); zCheckbox = GUILayout.Toggle(zCheckbox, "Z"); GUILayout.EndHorizontal(); EditorGUILayout.Space(); } /// <summary> /// Draws the range min and max fields /// </summary> private void CreateRangeFields() { GUILayout.Label("Range", EditorStyles.boldLabel); GUILayout.BeginHorizontal(); randomRangeMin = EditorGUILayout.FloatField("Min:", randomRangeMin); randomRangeMax = EditorGUILayout.FloatField("Max:", randomRangeMax); GUILayout.EndHorizontal(); EditorGUILayout.Space(); } /// <summary> /// Creates the Align transform window /// </summary> private void CreateAlignTransformWindow() { //Source transform GUILayout.BeginHorizontal(); GUILayout.Label("Align to: \t"); source = EditorGUILayout.ObjectField(source, typeof(Transform)) as Transform; GUILayout.EndHorizontal(); string[] texts = new string[4] { "Min", "Max", "Center", "Pivot" }; //Display align options EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginVertical(); GUILayout.Label("Selection:", EditorStyles.boldLabel); alignSelectionOption = GUILayout.SelectionGrid(alignSelectionOption, texts, 1); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(); GUILayout.Label("Source:", EditorStyles.boldLabel); alignSourceOption = GUILayout.SelectionGrid(alignSourceOption, texts, 1); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); //Position if (GUILayout.Button("Align")) { if (source != null) { //Add a temporary box collider to the source if it doesn't have one Collider sourceCollider = source.collider; bool destroySourceCollider = false; if (sourceCollider == null) { sourceCollider = source.gameObject.AddComponent<BoxCollider>(); destroySourceCollider = true; } foreach (Transform t in Selection.transforms) { //Add a temporary box collider to the transform if it doesn't have one Collider transformCollider = t.collider; bool destroyTransformCollider = false; if (transformCollider == null) { transformCollider = t.gameObject.AddComponent<BoxCollider>(); destroyTransformCollider = true; } Vector3 sourceAlignData = new Vector3(); Vector3 transformAlignData = new Vector3(); //Transform switch (alignSelectionOption) { case 0: //Min transformAlignData = transformCollider.bounds.min; break; case 1: //Max transformAlignData = transformCollider.bounds.max; break; case 2: //Center transformAlignData = transformCollider.bounds.center; break; case 3: //Pivot transformAlignData = transformCollider.transform.position; break; } //Source switch (alignSourceOption) { case 0: //Min sourceAlignData = sourceCollider.bounds.min; break; case 1: //Max sourceAlignData = sourceCollider.bounds.max; break; case 2: //Center sourceAlignData = sourceCollider.bounds.center; break; case 3: //Pivot sourceAlignData = sourceCollider.transform.position; break; } Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? sourceAlignData.x - (transformAlignData.x - t.position.x) : t.position.x; tmp.y = yCheckbox ? sourceAlignData.y - (transformAlignData.y - t.position.y) : t.position.y; tmp.z = zCheckbox ? sourceAlignData.z - (transformAlignData.z - t.position.z) : t.position.z; //Register the Undo Undo.RegisterUndo(t, "Align " + t.gameObject.name + " to " + source.gameObject.name); t.position = tmp; //Ugly hack! //Unity needs to update the collider of the selection to it's new position //(it stores in cache the collider data) //We can force the update by a change in a public variable (shown in the inspector), //then a call SetDirty to update the collider (it won't work if all inspector variables are the same). //But we want to restore the changed property to what it was so we do it twice. transformCollider.isTrigger = !transformCollider.isTrigger; EditorUtility.SetDirty(transformCollider); transformCollider.isTrigger = !transformCollider.isTrigger; EditorUtility.SetDirty(transformCollider); //Destroy the collider we added if (destroyTransformCollider) { DestroyImmediate(transformCollider); } } //Destroy the collider we added if (destroySourceCollider) { DestroyImmediate(sourceCollider); } } else { EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok"); EditorApplication.Beep(); } } } /// <summary> /// Creates the copy transform window /// </summary> private void CreateCopyTransformWindow() { //Source transform GUILayout.BeginHorizontal(); GUILayout.Label("Copy from: \t"); source = EditorGUILayout.ObjectField(source, typeof(Transform)) as Transform; GUILayout.EndHorizontal(); EditorGUILayout.Space(); //Position if (GUILayout.Button("Copy Position")) { if (source != null) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? source.position.x : t.position.x; tmp.y = yCheckbox ? source.position.y : t.position.y; tmp.z = zCheckbox ? source.position.z : t.position.z; Undo.RegisterUndo(t, "Copy position"); t.position = tmp; } } else { EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok"); EditorApplication.Beep(); } } //Rotation if (GUILayout.Button("Copy Rotation")) { if (source != null) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? source.rotation.eulerAngles.x : t.rotation.eulerAngles.x; tmp.y = yCheckbox ? source.rotation.eulerAngles.y : t.rotation.eulerAngles.y; tmp.z = zCheckbox ? source.rotation.eulerAngles.z : t.rotation.eulerAngles.z; Quaternion tmp2 = t.rotation; tmp2.eulerAngles = tmp; Undo.RegisterUndo(t, "Copy rotation"); t.rotation = tmp2; } } else { EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok"); EditorApplication.Beep(); } } //Local Scale if (GUILayout.Button("Copy Local Scale")) { if (source != null) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? source.localScale.x : t.localScale.x; tmp.y = yCheckbox ? source.localScale.y : t.localScale.y; tmp.z = zCheckbox ? source.localScale.z : t.localScale.z; Undo.RegisterUndo(t, "Copy local scale"); t.localScale = tmp; } } else { EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok"); EditorApplication.Beep(); } } } /// <summary> /// Creates the Randomize transform window /// </summary> private void CreateRandomizeTransformWindow() { CreateRangeFields(); //Position if (GUILayout.Button("Randomize Position")) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.position.x; tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.position.y; tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.position.z; Undo.RegisterUndo(t, "Randomize position"); t.position = tmp; } } //Rotation if (GUILayout.Button("Randomize Rotation")) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.rotation.eulerAngles.x; tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.rotation.eulerAngles.y; tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.rotation.eulerAngles.z; Quaternion tmp2 = t.rotation; tmp2.eulerAngles = tmp; Undo.RegisterUndo(t, "Randomize rotation"); t.rotation = tmp2; } } //Local Scale if (GUILayout.Button("Randomize Local Scale")) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.localScale.x; tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.localScale.y; tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.localScale.z; Undo.RegisterUndo(t, "Randomize local scale"); t.localScale = tmp; } } } /// <summary> /// Creates the Add Noise To Transform window /// </summary> private void CreateAddNoiseToTransformWindow() { CreateRangeFields(); //Position if (GUILayout.Button("Add noise to Position")) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0; tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0; tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0; Undo.RegisterUndo(t, "Add noise to position"); t.position += tmp; } } //Rotation if (GUILayout.Button("Add noise to Rotation")) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? t.rotation.eulerAngles.x + Random.Range(randomRangeMin, randomRangeMax) : 0; tmp.y = yCheckbox ? t.rotation.eulerAngles.y + Random.Range(randomRangeMin, randomRangeMax) : 0; tmp.z = zCheckbox ? t.rotation.eulerAngles.z + Random.Range(randomRangeMin, randomRangeMax) : 0; Undo.RegisterUndo(t, "Add noise to rotation"); t.rotation = Quaternion.Euler(tmp); } } //Local Scale if (GUILayout.Button("Add noise to Local Scale")) { foreach (Transform t in Selection.transforms) { Vector3 tmp = new Vector3(); tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0; tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0; tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0; Undo.RegisterUndo(t, "Add noise to local scale"); t.localScale += tmp; } } } }</span>
其窗口如下圖所示:
4. Editor
對某自定義組件進行觀察的Inspector窗口,可以從它派生。如下代碼所示:
代碼片段1定義了一個名為Star的組件:
<span style="font-size: 18px;">using System; using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class Star : MonoBehaviour { [Serializable] public class Point { public Color color; public Vector3 offset; } public Point[] points; public int frequency = 1; public Color centerColor; private Mesh mesh; private Vector3[] vertices; private Color[] colors; private int[] triangles; void Start () { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; if(frequency < 1){ frequency = 1; } if(points == null || points.Length == 0){ points = new Point[]{ new Point()}; } int numberOfPoints = frequency * points.Length; vertices = new Vector3[numberOfPoints + 1]; colors = new Color[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; colors[0] = centerColor; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } }</span>
代碼片段2定義了對Star組件進行觀測的Inspector窗口:
<span style="font-size: 18px;">using UnityEditor; using UnityEngine; [CustomEditor(typeof(Star))] public class StarInspector : Editor { private static GUIContent insertContent = new GUIContent("+", "duplicate this point"), deleteContent = new GUIContent("-", "delete this point"), pointContent = GUIContent.none; private static GUILayoutOption buttonWidth = GUILayout.MaxWidth(20f), colorWidth = GUILayout.MaxWidth(50f); private SerializedObject star; private SerializedProperty points, frequency, centerColor; void OnEnable () { … } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); if(GUILayout.Button(insertContent, EditorStyles.miniButtonLeft, buttonWidth)){ points.InsertArrayElementAtIndex(i); } if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){ points.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }</span>
其Inspector窗口如下圖所示:
說到這里,大家對ScriptableObject, ScriptableWizard, EditorWindow和Editor應該都有應有了一定了解。其中EditorWindow和Editor都繼承了ScriptableObject,而ScritableWizard則繼承了EditorWindow派。在實際開發應用中,應該根據需求的特點,靈活使用這四個類進行編輯器擴展。
參考資料:
1. http://catlikecoding.com/unity/tutorials/star/
2. http://www.unifycommunity.com/wiki
3. http://www.blog.silentkraken.com/2010/02/06/transformutilities/
4.http://unity3d.com/support/documentation/ScriptReference
轉:http://blog.csdn.net/jjiss318/article/details/7435708