1. 程式人生 > >Unity 編輯器擴充套件教程

Unity 編輯器擴充套件教程

來自:candycat1992

http://blog.csdn.net/candycat1992/article/details/52067975

寫在前面

Unity最強大的地方之一是它擴充套件性非常強的編輯器。Unite Europe 2016上有一個視訊專門講編輯器程式設計的:

這裡大概記錄一下里面的關鍵點。

場景一

關注點

  • 繪製重要區域,Gizmos.DrawXXX
  • OnDrawGizmos和OnDrawGizmosSelected回撥函式
  • 點選Gizmos按鈕就可以在Game檢視也看到線框了
這裡寫圖片描述
    // OnDrawGizmos()會在編輯器的Scene檢視重新整理的時候被呼叫
// 我們可以在這裡繪製一些用於Debug的資料 void OnDrawGizmos() { Gizmos.color = new Color( 1f, 0f, 0f, 1f ); Gizmos.DrawWireCube( transform.position + BoxCollider.center, BoxCollider.size ); Gizmos.color = new Color( 1f, 0f, 0f, 0.3f ); Gizmos.DrawCube( transform.position + BoxCollider.center, BoxCollider.size ); } // OnDrawGizmosSelect()類似於OnDrawGizmos(),它會在當該元件所屬的物體被選中時被呼叫
void OnDrawGizmosSelected() { Gizmos.color = new Color( 1f, 1f, 0f, 1f ); Gizmos.DrawWireCube( transform.position + BoxCollider.center, BoxCollider.size ); Gizmos.color = new Color( 1f, 1f, 0f, 0.3f ); Gizmos.DrawCube( transform.position + BoxCollider.center, BoxCollider.size ); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

場景二

關注點

  • 組織面板上的引數,新增滑動條、Header、空白等
這裡寫圖片描述
    [Space( 10 )]
    public float MaximumHeight;
    public float MinimumHeight;

    [Header( "Safe Frame" )]
    [Range( 0f, 1f )]
    public float SafeFrameTop;
    [Range( 0f, 1f )]
    public float SafeFrameBottom;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意到上面面板的最小面有個Camera Height,調節它可以改變攝像機的高度。這個改變是可以發生在編輯器模式下的,而且也不需要指令碼新增ExecuteInEditor。這是通過實現自定義的Editor指令碼來實現的:

using UnityEngine;
using UnityEditor;
using System.Collections;

// 我們可以通過為一個類定義它的Editor型別的[CustomEditor]來自定義該類的繪製介面
// 這需要把這個檔案放在Editor目錄下
[CustomEditor( typeof( GameCamera ) )]
public class GameCameraEditor : Editor 
{
    GameCamera m_Target;

    // 過載OnInspectorGUI()來繪製自己的編輯器
    public override void OnInspectorGUI()
    {
        // target可以讓我們得到當前繪製的Component物件
        m_Target = (GameCamera)target;

        // DrawDefaultInspector告訴Unity按照預設的方式繪製面板,這種方法在我們僅僅想要自定義某幾個屬性的時候會很有用
        DrawDefaultInspector();
        DrawCameraHeightPreviewSlider();  
    }

    void DrawCameraHeightPreviewSlider()
    {
        GUILayout.Space( 10 );

        Vector3 cameraPosition = m_Target.transform.position;
        cameraPosition.y = EditorGUILayout.Slider( "Camera Height", cameraPosition.y, m_Target.MinimumHeight, m_Target.MaximumHeight );

        if( cameraPosition.y != m_Target.transform.position.y )
        {
            // 改變狀態前,使用該方法來記錄操作,以便之後Undo
            Undo.RecordObject( m_Target, "Change Camera Height" );
            m_Target.transform.position = cameraPosition;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

場景三

關注點

  • 自定義繪製List物件
  • 使用serializedObject來修改引數的話Unity會自動有各種幫助函式,例如自動新增Undo功能
  • 如果直接修改引數的話,需要使用EditorUtility.SetDirty來告訴Unity需要儲存資料
  • BeginChangeCheck()和EndChangeCheck()會檢測它們之間的GUI有沒有被修改,如果修改了的話可以據此修改引數
  • Undo.RecordObject可以為下一步修改新增Undo/Redo
  • EditorUtility.DisplayDialog可以開啟內建對話方塊
這裡寫圖片描述
  1. 首先在面板上隱藏預設的List繪製方法,使用HideInInspector隱藏屬性:

    public class PistonE03 : MonoBehaviour 
    {
        public float Speed;
        public Vector3 AddForceWhenHittingPlayer;
    
        //We are hiding this in the inspector because we want to draw our own custom
        //inspector for it.
        [HideInInspector]
        public List<PistonState> States = new List<PistonState>();
        ......
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  2. 為了讓PistonState可以顯示在面板上,需要序列化PistonState:

    //[System.Serializable] tells unity to serialize this class if 
    //it's used in a public array or as a public variable in a component
    [System.Serializable]
    public class PistonState 
    {
        public string Name;
        public Vector3 Position;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  3. 實現自定義的繪製方程:

    [CustomEditor( typeof( PistonE03 ) )]
    public class PistonE03Editor : Editor 
    {
        PistonE03 m_Target;
    
        public override void OnInspectorGUI()
        {
            m_Target = (PistonE03)target;
    
            DrawDefaultInspector();
            DrawStatesInspector();        
        }
    
        //Draw a beautiful and useful custom inspector for our states array
        void DrawStatesInspector()
        {
            GUILayout.Space( 5 );
            GUILayout.Label( "States", EditorStyles.boldLabel );
    
            for( int i = 0; i < m_Target.States.Count; ++i )
            {
                DrawState( i );
            }
    
            DrawAddStateButton();
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    DrawDefaultInspector:先繪製預設的,DrawStatesInspector:自定義繪製面板函式。
    
    • 1
    • 2
  4. DrawState函式:

     void DrawState( int index )
    {
        if( index < 0 || index >= m_Target.States.Count )
        {
            return;
        }
    
        // 在我們的serializedObject中找到States變數
        // serializedObject允許我們方便地訪問和修改引數,Unity會提供一系列幫助函式。例如,我們可以通過serializedObject來修改元件值,而不是直接修改,Unity會自動建立Undo和Redo功能
        SerializedProperty listIterator = serializedObject.FindProperty( "States" );
    
        GUILayout.BeginHorizontal();
        {
            // 如果是在例項化的prefab上修改引數,我們可以模仿Unity預設的途徑來讓修改過的而且未被Apply的值顯示成粗體
            if( listIterator.isInstantiatedPrefab == true )
            {
                //The SetBoldDefaultFont functionality is usually hidden from us but we can use some tricks to
                //access the method anyways. See the implementation of our own EditorGUIHelper.SetBoldDefaultFont
                //for more info
                EditorGUIHelper.SetBoldDefaultFont( listIterator.GetArrayElementAtIndex( index ).prefabOverride );
            }
    
            GUILayout.Label( "Name", EditorStyles.label, GUILayout.Width( 50 ) );
    
            // BeginChangeCheck()和EndChangeCheck()會檢測它們之間的GUI有沒有被修改
            EditorGUI.BeginChangeCheck();
            string newName = GUILayout.TextField( m_Target.States[ index ].Name, GUILayout.Width( 120 ) );
            Vector3 newPosition = EditorGUILayout.Vector3Field( "", m_Target.States[ index ].Position );
    
            // 如果修改了的話EndChangeCheck()就會返回true,此時我們就可以進行一些操作例如儲存變化的數值
            if( EditorGUI.EndChangeCheck() )
            {
                //Create an Undo/Redo step for this modification
                Undo.RecordObject( m_Target, "Modify State" );
    
                m_Target.States[ index ].Name = newName;
                m_Target.States[ index ].Position = newPosition;
    
                // 如果我們直接修改屬性,而沒有通過serializedObject,那麼Unity並不會儲存這些資料,Unity只會儲存那些標識為dirty的屬性
                EditorUtility.SetDirty( m_Target );
            }
    
            EditorGUIHelper.SetBoldDefaultFont( false );
    
            if( GUILayout.Button( "Remove" ) )
            {
                EditorApplication.Beep();
    
                // 可以很方便的顯示一個包含特定按鈕的對話方塊,例如是否同意刪除
                if( EditorUtility.DisplayDialog( "Really?", "Do you really want to remove the state '" + m_Target.States[ index ].Name + "'?", "Yes", "No" ) == true )
                {
                    Undo.RecordObject( m_Target, "Delete State" );
                    m_Target.States.RemoveAt( index );
                    EditorUtility.SetDirty( m_Target );
                }
            }
        }
        GUILayout.EndHorizontal();
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

場景四

關注點

  • 可排序的陣列面板,通過使用ReorderableList來實現的,以及它的各個回撥函式
這裡寫圖片描述
using UnityEngine;
using UnityEditor;
// UnityEditorInternal是Unity內部使用、還未開放給用使用者的一些庫,可能有一些很有意思的類,例如ReorderableList,但注意可能會隨著新版本發生變化
using UnityEditorInternal;
using System.Collections;

// CanEditMultipleObjects告訴Unity,當我們選擇同一種類型的多個元件時,我們自定義的面板是可以支援同時修改所有選中的元件的
// 如果我們在修改引數時使用的是serializedObject,那麼這個功能Unity會自動完成的
// 但如果我們是直接使用"target"來訪問和修改引數的話,這個變數只能訪問到選中的第一個元件
// 此時我們可以使用"targets"來得到所有選中的相同元件
[CanEditMultipleObjects]
[CustomEditor( typeof( PistonE04Pattern ) )]
public class PistonE04PatternEditor : Editor 
{
    // UnityEditorInternal中提供了一種可排序的列表面板顯示類
    ReorderableList m_List;
    PistonE03 m_Piston;

    // OnEnable會在自定義面板被開啟的時候呼叫,例如當選中一個包含了PistonE04Pattern的gameobject時
    void OnEnable()
    {
        if( target == null )
        {
            return;
        }

        FindPistonComponent();
        CreateReorderableList();
        SetupReoirderableListHeaderDrawer();
        SetupReorderableListElementDrawer();
        SetupReorderableListOnAddDropdownCallback();
    }

    void FindPistonComponent()
    {
        m_Piston = ( target as PistonE04Pattern ).GetComponent<PistonE03>();
    }

    void CreateReorderableList()
    {
        // ReorderableList是一個非常棒的檢視陣列型別變數的實現類。它位於UnityEditorInternal中,這意味著Unity並沒有覺得該類足夠好到可以開放給公眾
        // 更多關於ReorderableLists的內容可參考:
        // http://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/
        m_List = new ReorderableList(
                        serializedObject,
                        serializedObject.FindProperty( "Pattern" ),
                        true, true, true, true );
    }

    void SetupReoirderableListHeaderDrawer()
    {
        // ReorderableList有一系列回撥函式來讓我們過載繪製這些陣列
        // 這裡我們使用drawHeaderCallback來繪製表格的頭headers
        // 每個回撥會接受一個Rect變數,它包含了該元素繪製的位置
        // 因此我們可以使用這個變數來決定我們把當前的元素繪製在哪裡
        m_List.drawHeaderCallback = 
            ( Rect rect ) =>
        {
            EditorGUI.LabelField( 
                new Rect( rect.x, rect.y, rect.width - 60, rect.height ), 
                "State" );
            EditorGUI.LabelField(
                new Rect( rect.x + rect.width - 60, rect.y, 60, rect.height ),
                "Delay" );
        };
    }

    void SetupReorderableListElementDrawer()
    {
        // drawElementCallback會定義列表中的每個元素是如何被繪製的
        // 同樣,保證我們繪製的元素是相對於Rect引數繪製的
        m_List.drawElementCallback =
            ( Rect rect, int index, bool isActive, bool isFocused ) =>
        {
            var element = m_List.serializedProperty.GetArrayElementAtIndex( index );
            rect.y += 2;

            float delayWidth = 60;
            float nameWidth = rect.width - delayWidth;

            EditorGUI.PropertyField(
                new Rect( rect.x, rect.y, nameWidth - 5, EditorGUIUtility.singleLineHeight ),
                element.FindPropertyRelative( "Name" ), GUIContent.none );

            EditorGUI.PropertyField(
                new Rect( rect.x + nameWidth, rect.y, delayWidth, EditorGUIUtility.singleLineHeight ),
                element.FindPropertyRelative( "DelayAfterwards" ), GUIContent.none );
        };
    }

    void SetupReorderableListOnAddDropdownCallback()
    {
        // onAddDropdownCallback定義當我們點選列表下面的[+]按鈕時發生的事件
        // 在本例裡,我們想要顯示一個下拉選單來給出預定義的一些States
        m_List.onAddDropdownCallback = 
            ( Rect buttonRect, ReorderableList l ) =>
        {
            if( m_Piston.States == null || m_Piston.States.Count == 0 )
            {
                EditorApplication.Beep();
                EditorUtility.DisplayDialog( "Error", "You don't have any states defined in the PistonE03 component", "Ok" );
                return;
            }

            var menu = new GenericMenu();

            foreach( PistonState state in m_Piston.States )
            {
                menu.AddItem( new GUIContent( state.Name ),
                              false,
                              OnReorderableListAddDropdownClick,
                              state );
            }

            menu.ShowAsContext();
        };
    }

    // 這個回撥函式會在使用者選擇了[+]下拉選單中的某一項後呼叫
    void OnReorderableListAddDropdownClick( object target ) 
    {
        PistonState state = (PistonState)target;

        int index = m_List.serializedProperty.arraySize;
        m_List.serializedProperty.arraySize++;
        m_List.index = index;

        SerializedProperty element = m_List.serializedProperty.GetArrayElementAtIndex( index );
        element.FindPropertyRelative( "Name" ).stringValue = state.Name;
        element.FindPropertyRelative( "DelayAfterwards" ).floatValue = 0f;

        serializedObject.ApplyModifiedProperties();
    }  

    public override void OnInspectorGUI()
    {
        GUILayout.Space( 5 );

        EditorGUILayout.PropertyField( serializedObject.FindProperty( "DelayPatternAtBeginning" ) );

        serializedObject.ApplyModifiedProperties();
        serializedObject.Update();

        m_List.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • <