Unity編輯器擴(kuò)展Extending the Editor

前言:
編輯器擴(kuò)展部分在開發(fā)調(diào)試過程中會(huì)非常有幫助,基本上每個(gè)項(xiàng)目都會(huì)涉及到自定義Editor Window和Inspector,以及
制作一些通用的或是針對(duì)當(dāng)前項(xiàng)目的便利性工具,方便開發(fā)人員使用。這里做上總結(jié)下。之前沒怎么關(guān)注文檔,細(xì)節(jié)
太多了,后面會(huì)不時(shí)的回來看看。

ExtendingTheEditor.html

Unity lets you extend the editor with your own custom inspectors and Editor Windows and you can define how properties are displayed in the inspector with custom Property Drawers. This section explains how to use these features.

通過custom inspectors and Editor Windows來擴(kuò)展我們的編輯器。

1.通過自定義的Property Drawers來自定義Inspector面板,定義字段如何顯示在面板上。
2.EditWindow即創(chuàng)建一個(gè)腳本派生自EditWindow,通過代碼來觸發(fā)該EditWindow,并在上面添加功能所需要的UI。

注意:(EditWindow不同于IMGUI中的Window,IMGUI的Window是顯示在當(dāng)前的SceneView中,而EditWindow則是彈出一個(gè)新的獨(dú)立窗體,就像Inspector,GameView,SceneView這些獨(dú)立的內(nèi)建的窗體組件是一樣的)

注意:(所有的這些自定義類的腳本定義都必須放在特殊文件夾Editor下)

定義并顯示一個(gè)EditWindow:

//C# Example

using UnityEngine;
using UnityEditor;
using System.Collections;

class MyWindow : EditorWindow {
    [MenuItem ("Window/My Window")]

    public static void  ShowWindow () {
        EditorWindow.GetWindow(typeof(MyWindow));
    }

    void OnGUI () {
        // The actual window code goes here
    }
}

默認(rèn)顯示在屏幕上的左上角,你可以隨意移動(dòng)位置,該位置會(huì)被保存起來,下一次再打開Window會(huì)讀取之前的位置,省得你繼續(xù)調(diào)整。

EditorWindow.GetWindow(typeof(MyWindow));

這行代碼是啟動(dòng)代碼。

如何想要自定久EditWindow的位置,可以使用GetWindowWithRect

//C# Example

using UnityEngine;
using UnityEditor;
using System.Collections;

class MyWindow : EditorWindow {
    [MenuItem ("Window/My Window")]
    public static void  ShowWindow () {
        //EditorWindow.GetWindow(typeof(MyWindow));
        MyWindow window = (MyWindow)EditorWindow.GetWindowWithRect(typeof(MyWindow), new Rect(0, 0, 500, 550));
    }

    void OnGUI () {
        // The actual window code goes here
    }
}

實(shí)現(xiàn)EditWindow中的GUI:

EditWindow中UI的部分也是通過OnGUI來實(shí)現(xiàn)。所以你可以直接使用GUI或GUILayout來實(shí)現(xiàn),Unity也為Editor單獨(dú)提供了EditGUI和EditGUILayout,他們可以混合實(shí)現(xiàn)。

//C# Example
using UnityEditor;
using UnityEngine;

public class MyWindow : EditorWindow
{
    string myString = "Hello World";
    bool groupEnabled;
    bool myBool = true;
    float myFloat = 1.23f;
    
    // Add menu item named "My Window" to the Window menu
    [MenuItem("Window/My Window")]
    public static void ShowWindow()
    {
        //Show existing window instance. If one doesn't exist, make one.
        EditorWindow.GetWindow(typeof(MyWindow));

//  EditorWindow window = GetWindow(typeof(MyWindow));
  //      window.position = new Rect(100, 100, 300, 150);
    //    window.Show();
    }
    
    void OnGUI()
    {
        GUILayout.Label ("Base Settings", EditorStyles.boldLabel);
        myString = EditorGUILayout.TextField ("Text Field", myString);
        
        groupEnabled = EditorGUILayout.BeginToggleGroup ("Optional Settings", groupEnabled);
            myBool = EditorGUILayout.Toggle ("Toggle", myBool);
            myFloat = EditorGUILayout.Slider ("Slider", myFloat, -3, 3);
        EditorGUILayout.EndToggleGroup ();
    }
}
image.png

但是可以看出來,EditorGUILayout和EditorGUI顯然是封裝處理過了,比如Slider,使用GUI或是GUILayout實(shí)現(xiàn),需要
Label+Slider+TextField(不可編輯)的組合,需要手動(dòng)封裝實(shí)現(xiàn),而EditGUILayout就已經(jīng)封裝好了。所以自定義編輯器盡量
是使用EditXXX吧。

Property Drawers:
有兩個(gè)用處:
1.為每一個(gè)可序列化的實(shí)例,自定義UI
2.為每一個(gè)應(yīng)用了自定義特性的腳本成員,自定義UI

(為可序列化的類,為某自定義的特性Attribute)

如果你有一個(gè)自定義的類,并且他是可序列化的,你想要通過Property Drawer來自定義他在Inspector中的顯示。
注意: Property attribute應(yīng)該放在一個(gè)常規(guī)的文件夾下,而不是Editor

Property Drawers源碼:

using System;
using System.Reflection;
using UnityEngine;

namespace UnityEditor
{
    public abstract class PropertyDrawer : GUIDrawer
    {
        //
        // Properties
        //
        public PropertyAttribute attribute {
            get;
        }

        public FieldInfo fieldInfo {
            get;
        }

        //
        // Constructors
        //
        protected PropertyDrawer ();

        //
        // Methods
        //
        public virtual float GetPropertyHeight (SerializedProperty property, GUIContent label);

        internal float GetPropertyHeightSafe (SerializedProperty property, GUIContent label);

        public virtual void OnGUI (Rect position, SerializedProperty property, GUIContent label);

        internal void OnGUISafe (Rect position, SerializedProperty property, GUIContent label);
    }
}

下面是官方的一個(gè)自定義PropertyDrawer例子:

using System;
using UnityEngine;

public enum IngredientUnit { Spoon, Cup, Bowl, Piece }

// Custom serializable class
[Serializable]
public class Ingredient
{
    public string name;
    public int amount = 1;
    public IngredientUnit unit;
}

public class Recipe : MonoBehaviour
{
    public Ingredient potionResult;
    public Ingredient[] potionIngredients;
}

說明:
定義了Ingredient自定義類,可序列化,稍后會(huì)通過自定義PropertyDrawer來重新繪制Ingredient在Inspector面板中的布局。
(自定義的類需要手動(dòng)加上特性Serializable才可以被序列化)

using UnityEditor;
using UnityEngine;

// IngredientDrawer
[CustomPropertyDrawer(typeof(Ingredient))]
public class IngredientDrawer : PropertyDrawer
{
    // Draw the property inside the given rect
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // Using BeginProperty / EndProperty on the parent property means that
        // prefab override logic works on the entire property.
        EditorGUI.BeginProperty(position, label, property);

        // Draw label
        position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);

        // Don't make child fields be indented
        var indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;

        // Calculate rects
        var amountRect = new Rect(position.x, position.y, 30, position.height);
        var unitRect = new Rect(position.x + 35, position.y, 50, position.height);
        var nameRect = new Rect(position.x + 90, position.y, position.width - 90, position.height);

        // Draw fields - passs GUIContent.none to each so they are drawn without labels
        EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("amount"), GUIContent.none);
        EditorGUI.PropertyField(unitRect, property.FindPropertyRelative("unit"), GUIContent.none);
        EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none);

        // Set indent back to what it was
        EditorGUI.indentLevel = indent;

        EditorGUI.EndProperty();
    }
}

說明:
定義IngredientDrawer,派生自PropertyDrawer,需要加上特性[CustomPropertyDrawer(typeof(Ingredient))]
typeof(XXX)為需要進(jìn)行處理的可序列化的自定義類。
通過上面展示出來的PropertyDrawer源碼,重寫OnGUI(Rect position, SerializedProperty property, GUIContent label)
方法即可。

以 EditorGUI.BeginProperty(position, label, property)開始,并以EditorGUI.EndProperty();結(jié)束
中間插入重繪的代碼。

EditorGUI.PrefixLabel是在一些控件之前創(chuàng)建一個(gè)Label,GUIUtility.GetControlID(FocusType.Passive)獲取唯一的控件
ID,F(xiàn)ocusType.Passive為焦點(diǎn)類型,passive即不接受焦點(diǎn),即我們?cè)诎聪聇ab鍵進(jìn)行控件之間切換時(shí),是否需接受焦點(diǎn)。
(如果需要接受,要通過GetControlID來通過IMGUI系統(tǒng))
EditorGUI.PrefixLabel通常是獲取起始位置,標(biāo)簽則是實(shí)例的名稱(當(dāng)前可序列化自定義類的實(shí)例)

EditorGUI.indentLevel:縮進(jìn)級(jí)別


image.png

這里設(shè)置indentLevel設(shè)置為0,所有元素同樣的縮進(jìn)級(jí)別,并在最后,恢復(fù)至原始的值,這樣不會(huì)影響到后面控件的使用。

之后分別聲明了amountRect,unitRect,nameRect三個(gè)指定字段的矩形區(qū)域,相對(duì)于position的位置。

EditorGUI.PropertyField在編輯器上創(chuàng)建一個(gè)序列化的字段。
FindPropertyRelative用于獲取指定的字段。

image.png

紅框標(biāo)識(shí)的是PrefixLabel

通過屬性特性Property Attribute來自定義腳本成員的GUI:
比如我當(dāng)前類中使用了int或是float類型的值,我只想針對(duì)該成員進(jìn)行自定義GUI,而不影響其它元素的布局和顯示。
可以針對(duì)Property Attribute特性本身應(yīng)用PropertyDrawer.

// Show this float in the Inspector as a slider between 0 and 10
[Range(0f, 10f)]
float myFloat = 0f;
using UnityEngine;

public class MyRangeAttribute : PropertyAttribute 
{
       public readonly float min;
       public readonly float max;
        
       public MyRangeAttribute(float min, float max)
        {
            this.min = min;
            this.max = max;
        }
}
using UnityEditor;
using UnityEngine;

// Tell the MyRangeDrawer that it is a drawer for properties with the MyRangeAttribute.
[CustomPropertyDrawer(typeof(MyRangeAttribute))]
public class RangeDrawer : PropertyDrawer
{
    // Draw the property inside the given rect
    public overrride void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // First get the attribute since it contains the range for the slider
        MyRangeAttribute range = (MyRangeAttribute)attribute;

        // Now draw the property as a Slider or an IntSlider based on whether it's a float or integer.
        if (property.propertyType == SerializedPropertyType.Float)
            EditorGUI.Slider(position, property, range.min, range.max, label);
        else if (property.propertyType == SerializedPropertyType.Integer)
            EditorGUI.IntSlider(position, property, (int) range.min, (int) range.max, label);
        else
            EditorGUI.LabelField(position, label.text, "Use MyRange with float or int.");
    }
}
image.png

另一個(gè)針成類成員的例子,之前文章中有提到過位標(biāo)志,即進(jìn)行位|(OR)操作,開關(guān)方式,讓一個(gè)值可以包含多種條件,
也是可以通過自定義Inspector實(shí)現(xiàn),默認(rèn)定義了一個(gè)枚舉值,在默認(rèn)的GUI只是顯示一個(gè)下拉菜單,里面列所當(dāng)前枚舉類型所有的值,但只能單選,下面小例子實(shí)現(xiàn)可以多選的實(shí)現(xiàn):

// Custom serializable class
public enum IngredientUnit { Spoon, Cup, Bowl, Piece }

同樣以IngredientUnit枚舉為例。

// Custom serializable class
[System.Serializable]
public class Ingredient
{
    public string name;
    public int amount = 1;
    [MyEnum]
    public IngredientUnit unit;
}

在IngredientUnit上應(yīng)用了自定義特性[MyEnum]

public class MyEnumAttribute:PropertyAttribute
{
    public MyEnumAttribute()
    {
        
    }
}

[CustomPropertyDrawer(typeof(MyEnumAttribute))]
public class MyEnumDrawer:PropertyDrawer{
    
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    {
        property.intValue = EditorGUI.MaskField (position, label, property.intValue, property.enumNames);
    }
}

自定義特性類MyEnumAttribute,派生自PropertyAttribute
自定義MyEnumDrawer,派生自PropertyDrawer,并通過CustomPropertyDrawer告訴編譯器MyEnumAttribute作為處理的對(duì)象。

public Ingredient potionResult;

在Monobehaviour中定義Ingredient變量,效果如下:

image.png

注意:位標(biāo)志中,每個(gè)枚舉值都必須是2的冪,保證每一個(gè)值都有唯一的不重復(fù)的位。

再小總結(jié)下,如何是針對(duì)某個(gè)成員進(jìn)行自定義在Inspector中顯示,要實(shí)現(xiàn)PropertyAttribute特性類的派生類,并對(duì)該類
進(jìn)行Property Drawer實(shí)現(xiàn)即可。

最后,如何給Component來進(jìn)行自定義的Inspector顯示,上面是對(duì)成員以及可序列化的實(shí)例進(jìn)行自定義。

官方例子:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LookAtPoint : MonoBehaviour {

    public Vector3 lookAtPoint = Vector3.zero;

    void Update()
    {
        transform.LookAt(lookAtPoint);
    }
}

定義LookAtPoint組件腳本,并綁定在一個(gè)gameObject物體上。
現(xiàn)在要對(duì)LookAtPoint腳本進(jìn)行Inspector的自定義。需要派生自Editor,并通過CustomEditor特性來告訴編譯器,哪個(gè)組件將做為Editor類。

//c# Example (LookAtPointEditor.cs)
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor 
{
    SerializedProperty lookAtPoint;
    
    void OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookAtPoint");
    }
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(lookAtPoint);
        serializedObject.ApplyModifiedProperties();
    }
}
image.png

和上面提到的不同,繼承自Editor,重寫OnInspectorGUI方法,在里面完成GUI的自定義。
并且通過serializedObject.FindProperty來找到具體的lookAtPoint字段。

serializedObject.Update();
serializedObject.ApplyModifiedProperties();
是一定要有的,不然無法保存修改后的值。
EditorGUILayout.PropertyField(lookAtPoint);沒有改變,在編輯器上創(chuàng)建一個(gè)字段。

如果暫地保持布局原樣不動(dòng),可以只寫下面這句代碼:
base.OnInspectorGUI ()或DrawDefaultInspector ();
調(diào)用基本類的繪制,默認(rèn)布局。

除上使用serializedObject.FindProperty的形式獲取實(shí)例的成員之外,還有另外一種實(shí)現(xiàn)方式:

LookAtPoint point = (LookAtPoint)target;
point.lookAtPoint = EditorGUILayout.Vector3Field ("LookAtPoint", point.lookAtPoint);

如果有限制值的區(qū)間,也可以這樣定義:

LookAtPoint point = (LookAtPoint)target;
point.lookAtPoint.x = EditorGUILayout.Slider ("X",point.lookAtPoint.x, 0, 1);
point.lookAtPoint.y = EditorGUILayout.Slider ("Y",point.lookAtPoint.y, 0, 1);
point.lookAtPoint.z = EditorGUILayout.Slider ("Z",point.lookAtPoint.z, 0, 1);

在Editor中有另外一個(gè)可以重載的方法:

public override bool HasPreviewGUI()
    {
        return point.HasPreview;
    }

通過布爾開關(guān)控制是否顯示預(yù)覽視圖。


image.png

同樣,也有相對(duì)應(yīng)可以在預(yù)覽視圖上繪制的方法:

public void OnPreviewGUI (Rect r, GUIStyle background);

通常HasPreviewGUI和OnPreviewGUI要獲取來自另外一個(gè)腳本的信息,比如我有一個(gè)腳本叫ImagePreview,
我只有在將該腳本附加到gameobject上以后,我才應(yīng)該顯示預(yù)覽圖。并顯示在previewGUI上,下面是小例子:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ImagePreview : MonoBehaviour {

    public Texture2D texture;

    // Use this for initialization
    void Start () {
        
    }

}

注意,如果沒有Start方法,那么在Inspector面板上就不會(huì)有選框可供關(guān)閉顯示。
texture初始化可以直接在Inspector中指定,也可以通過代碼加載,代碼加載需要將資源放置
在特殊的文件夾Edit default resources中,代碼如下:

texture = EditorGUIUtility.Load ("xxxx.jpg") as Texture2D;

在LookAtPointEditor方法中,修改HasPreviewGUI:

public override bool HasPreviewGUI()
    {
        ImagePreview preview = (target as LookAtPoint).GetComponent<ImagePreview> ();
        if (preview != null&&preview.isActiveAndEnabled) {
            return true;
        }
        return false;

    }

獲取ImagePreview為空,并且處于活動(dòng)狀態(tài)。

下一步在OnInspectorGUI中處理繪制:

public override void OnPreviewGUI (Rect r, GUIStyle background)
    {
        ImagePreview preview = (target as LookAtPoint).GetComponent<ImagePreview> ();
        if (preview.texture == null) {
            EditorGUI.DrawRect (r, Color.red);
        } else {
            EditorGUI.DrawPreviewTexture (r, preview.texture);
        }
    }

如果texture==null,就繪制一個(gè)紅色的矩形框

image.png

image.png

Editor中也可以重載OnSceneGUI,比如編輯mesh,地形的繪制等等,這里就不介紹了,應(yīng)用得不多,后面有具體使用時(shí),再寫出來

最后想到另一個(gè),如何在SceneView中繪制圖標(biāo),讓當(dāng)前的對(duì)象更具辨識(shí)度,如下:

void OnDrawGizmos() {
        Gizmos.DrawIcon(transform.position, "xxx.png", true);
    }

先告一段落,之后接觸到新的知識(shí)點(diǎn),再補(bǔ)充上來。

(AssetDataBase)

自定義編輯器這些可以參照很多源碼的實(shí)來學(xué)習(xí),如地圖編輯類插件是對(duì)自定義部分使用非常繁雜的。


到此為止,如果大家發(fā)現(xiàn)有什么不對(duì)的地方,歡迎指正,共同提高,感謝您的閱讀!

編輯于2018.7.25

最近發(fā)現(xiàn)將自己在未來要去深入學(xué)習(xí)的東西新建成一篇文章,會(huì)很想著馬上就去解決他,然后發(fā)表出來,現(xiàn)在已經(jīng)列出來很多了,但事情都分輕重緩急,我已經(jīng)抵抗此一段時(shí)間了,還是要先做眼下最重要的事兒,之后在業(yè)余時(shí)間上都會(huì)補(bǔ)上來。
閑話少說,有點(diǎn)累了,休息半小時(shí),接著學(xué)習(xí)。

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容