Unity編輯器案列

  • 2019 年 12 月 2 日
  • 筆記

【Unity】編輯器小教程

  1. 寫在前面
  2. 場景一
  3. 場景二
  4. 場景三
  5. 場景四
  6. 場景五
  7. 場景六
  8. 場景七
  9. 場景八
  10. 場景九

寫在前面

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 );      }

場景二

關注點

  • 組織面板上的參數,添加滑動條、Header、空白等
    [Space( 10 )]      public float MaximumHeight;      public float MinimumHeight;        [Header( "Safe Frame" )]      [Range( 0f, 1f )]      public float SafeFrameTop;      [Range( 0f, 1f )]      public float SafeFrameBottom;

注意到上面面板的最小面有個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;          }      }  }

場景三

關注點

  • 自定義繪製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>();        ....

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;  }.

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();        }

DrawDefaultInspector:先繪製默認的,DrawStatesInspector:自定義繪製面板函數。 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();    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();   

場景四

關注點

  • 可排序的數組面板,通過使用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();      }  }

場景五

關注點

  • 實現了一個可以在編輯器狀態下預覽效果的編輯器窗口
using UnityEngine;  // 要實現自定義窗口需要包含進來UnityEditor  using UnityEditor;  using System.Collections;    // EditorWindow是另一個非常有用的Editor類。我們可以靠它來定義自己的窗口  public class PreviewPlaybackWindow : EditorWindow  {      // MenuItem可以讓我們在菜單欄中打開這個窗口      [MenuItem( "Window/Preview Playback Window" )]      static void OpenPreviewPlaybackWindow()      {          EditorWindow.GetWindow<PreviewPlaybackWindow>( false, "Playback" );          // 另一個有用的寫法是下面這樣          // 可以讓我們訪問到窗口的屬性,例如定義最小尺寸等          //EditorWindow window = EditorWindow.GetWindow<PreviewPlaybackWindow>( false, "Playback" );          //window.minSize = new Vector2(100.0f, 100.0f);      }        float m_PlaybackModifier;      float m_LastTime;        void OnEnable()      {          // Update函數會每秒調用30次來刷新編輯器界面          // 我們可以據此來註冊自己的編輯器Update函數          EditorApplication.update -= OnUpdate;          EditorApplication.update += OnUpdate;      }        void OnDisable()      {          EditorApplication.update -= OnUpdate;      }        void OnUpdate()      {          if( m_PlaybackModifier != 0f )          {              // PreviewTime是自定義的一個類:              //public class PreviewTime              //{              //    public static float Time              //    {              //        get              //        {              //            if( Application.isPlaying == true )              //            {              //                return UnityEngine.Time.timeSinceLevelLoad;              //            }                //            // EditorPrefsle類似於PlayerPrefs但只在編輯器狀態下工作              //            // 我們可以據此來存儲變量,基本我們關閉了編輯器該變量也可以長久保存              //            return EditorPrefs.GetFloat( "PreviewTime", 0f );              //        }              //        set              //        {              //            EditorPrefs.SetFloat( "PreviewTime", value );              //        }              //    }              //}                // m_PlaybackModifier是用於控制預覽播放速率的變量              // 當它不為0的時候,說明需要刷新界面,更新時間              PreviewTime.Time += ( Time.realtimeSinceStartup - m_LastTime ) * m_PlaybackModifier;                // 當預覽時間改變時,我們需要確保重繪這個窗口以便我們可以立即看到它的更新              // 而Unity只會在它認為該窗口需要重繪時(例如我們移動了窗口)才會重繪              // 因此我們可以調用Repaint函數來強制馬上重繪              Repaint();                // 由於預覽時間發生了變化,我們也希望可以立刻重繪Scene視圖的界面              SceneView.RepaintAll();          }            m_LastTime = Time.realtimeSinceStartup;      }        void OnGUI()      {          // 繪製各個按鈕來控制預覽時間          float seconds = Mathf.Floor( PreviewTime.Time % 60 );          float minutes = Mathf.Floor( PreviewTime.Time / 60 );            GUILayout.Label( "Preview Time: " + minutes + ":" + seconds.ToString( "00" ) );          GUILayout.Label( "Playback Speed: " + m_PlaybackModifier );            GUILayout.BeginHorizontal();          {              if( GUILayout.Button( "|<", GUILayout.Height( 30 ) ) )              {                  PreviewTime.Time = 0f;                  SceneView.RepaintAll();              }                if( GUILayout.Button( "<<", GUILayout.Height( 30 ) ) )              {                  m_PlaybackModifier = -5f;              }                if( GUILayout.Button( "<", GUILayout.Height( 30 ) ) )              {                  m_PlaybackModifier = -1f;              }                if( GUILayout.Button( "||", GUILayout.Height( 30 ) ) )              {                  m_PlaybackModifier = 0f;              }                if( GUILayout.Button( ">", GUILayout.Height( 30 ) ) )              {                  m_PlaybackModifier = 1f;              }                if( GUILayout.Button( ">>", GUILayout.Height( 30 ) ) )              {                  m_PlaybackModifier = 5f;              }          }          GUILayout.EndHorizontal();      }  }
  • 為了在編輯器狀態下可以查看到cube的運動,我們還需要實現OnDrawGizmos來繪製一些線框表示運動。原理就是使用PreviewTime.Time來控制運動。

場景六

關注點

  • 在Scene視圖中,鼠標的位置繪製特定的Handle
using UnityEngine;  using UnityEditor;  using System.Collections;    // [InitializeOnLoad]可以確保這個類的構造器在編輯器加載時就被調用  [InitializeOnLoad]  public class LevelEditorE06CubeHandle : Editor  {      public static Vector3 CurrentHandlePosition = Vector3.zero;      public static bool IsMouseInValidArea = false;        static Vector3 m_OldHandlePosition = Vector3.zero;        static LevelEditorE06CubeHandle()      {          //The OnSceneGUI delegate is called every time the SceneView is redrawn and allows you          //to draw GUI elements into the SceneView to create in editor functionality          // OnSceneGUI委託在Scene視圖每次被重繪時被調用          // 這允許我們可以在Scene視圖繪製自定義的GUI元素          SceneView.onSceneGUIDelegate -= OnSceneGUI;          SceneView.onSceneGUIDelegate += OnSceneGUI;      }        void OnDestroy()      {          SceneView.onSceneGUIDelegate -= OnSceneGUI;      }        static void OnSceneGUI( SceneView sceneView )      {          if( IsInCorrectLevel() == false )          {              return;          }            bool isLevelEditorEnabled = EditorPrefs.GetBool( "IsLevelEditorEnabled", true );            //Ignore this. I am using this because when the scene GameE06 is opened we haven't yet defined any On/Off buttons          //for the cube handles. That comes later in E07. This way we are forcing the cube handles state to On in this scene          {              if( UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE06" )              {                  isLevelEditorEnabled = true;              }          }            if( isLevelEditorEnabled == false )          {              return;          }            // 更新Handle的位置          UpdateHandlePosition();          // 檢查鼠標所在的位置是否有效          UpdateIsMouseInValidArea( sceneView.position );          // 檢測是否需要重新繪製Handle          UpdateRepaint();            DrawCubeDrawPreview();      }        //I will use this type of function in many different classes. Basically this is useful to      //be able to draw different types of the editor only when you are in the correct scene so we      //can have an easy to follow progression of the editor while hoping between the different scenes      static bool IsInCorrectLevel()      {          return UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE06"              || UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE07"              || UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE08"              || UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE09";      }        static void UpdateIsMouseInValidArea( Rect sceneViewRect )      {          // 確保cube handle只在需要的區域內繪製          // 在本例我們就是當鼠標移動到自定義的GUI上或更低的位置上時,就簡單地隱藏掉handle          bool isInValidArea = Event.current.mousePosition.y < sceneViewRect.height - 35;            if( isInValidArea != IsMouseInValidArea )          {              IsMouseInValidArea = isInValidArea;              SceneView.RepaintAll();          }      }        static void UpdateHandlePosition()      {          if( Event.current == null )          {              return;          }            Vector2 mousePosition = new Vector2( Event.current.mousePosition.x, Event.current.mousePosition.y );            Ray ray = HandleUtility.GUIPointToWorldRay( mousePosition );          RaycastHit hit;            if( Physics.Raycast( ray, out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer( "Level" ) ) == true )          {              Vector3 offset = Vector3.zero;                if( EditorPrefs.GetBool( "SelectBlockNextToMousePosition", true ) == true )              {                  offset = hit.normal;              }                CurrentHandlePosition.x = Mathf.Floor( hit.point.x - hit.normal.x * 0.001f + offset.x );              CurrentHandlePosition.y = Mathf.Floor( hit.point.y - hit.normal.y * 0.001f + offset.y );              CurrentHandlePosition.z = Mathf.Floor( hit.point.z - hit.normal.z * 0.001f + offset.z );                CurrentHandlePosition += new Vector3( 0.5f, 0.5f, 0.5f );          }      }        static void UpdateRepaint()      {          //If the cube handle position has changed, repaint the scene          if( CurrentHandlePosition != m_OldHandlePosition )          {              SceneView.RepaintAll();              m_OldHandlePosition = CurrentHandlePosition;          }      }        static void DrawCubeDrawPreview()      {          if( IsMouseInValidArea == false )          {              return;          }            Handles.color = new Color( EditorPrefs.GetFloat( "CubeHandleColorR", 1f ), EditorPrefs.GetFloat( "CubeHandleColorG", 1f ), EditorPrefs.GetFloat( "CubeHandleColorB", 0f ) );            DrawHandlesCube( CurrentHandlePosition );      }        static void DrawHandlesCube( Vector3 center )      {          Vector3 p1 = center + Vector3.up * 0.5f + Vector3.right * 0.5f + Vector3.forward * 0.5f;          Vector3 p2 = center + Vector3.up * 0.5f + Vector3.right * 0.5f - Vector3.forward * 0.5f;          Vector3 p3 = center + Vector3.up * 0.5f - Vector3.right * 0.5f - Vector3.forward * 0.5f;          Vector3 p4 = center + Vector3.up * 0.5f - Vector3.right * 0.5f + Vector3.forward * 0.5f;            Vector3 p5 = center - Vector3.up * 0.5f + Vector3.right * 0.5f + Vector3.forward * 0.5f;          Vector3 p6 = center - Vector3.up * 0.5f + Vector3.right * 0.5f - Vector3.forward * 0.5f;          Vector3 p7 = center - Vector3.up * 0.5f - Vector3.right * 0.5f - Vector3.forward * 0.5f;          Vector3 p8 = center - Vector3.up * 0.5f - Vector3.right * 0.5f + Vector3.forward * 0.5f;            // 我們可以使用Handles類來在Scene視圖繪製3D物體          // 如果實現恰當的話,我們甚至可以和handles進行交互,例如Unity的移動工具          Handles.DrawLine( p1, p2 );          Handles.DrawLine( p2, p3 );          Handles.DrawLine( p3, p4 );          Handles.DrawLine( p4, p1 );            Handles.DrawLine( p5, p6 );          Handles.DrawLine( p6, p7 );          Handles.DrawLine( p7, p8 );          Handles.DrawLine( p8, p5 );            Handles.DrawLine( p1, p5 );          Handles.DrawLine( p2, p6 );          Handles.DrawLine( p3, p7 );          Handles.DrawLine( p4, p8 );      }  }

場景七

關注點

  • 在Scene視圖繪製自定義的工具條
using UnityEngine;  using UnityEditor;  using System.Collections;    [InitializeOnLoad]  public class LevelEditorE07ToolsMenu : Editor  {      //This is a public variable that gets or sets which of our custom tools we are currently using      //0 - No tool selected      //1 - The block eraser tool is selected      //2 - The "Add block" tool is selected      public static int SelectedTool      {          get          {              return EditorPrefs.GetInt( "SelectedEditorTool", 0 );          }          set          {              if( value == SelectedTool )              {                  return;              }                EditorPrefs.SetInt( "SelectedEditorTool", value );                switch( value )              {              case 0:                  EditorPrefs.SetBool( "IsLevelEditorEnabled", false );                    Tools.hidden = false;                  break;              case 1:                  EditorPrefs.SetBool( "IsLevelEditorEnabled", true );                  EditorPrefs.SetBool( "SelectBlockNextToMousePosition", false );                  EditorPrefs.SetFloat( "CubeHandleColorR", Color.magenta.r );                  EditorPrefs.SetFloat( "CubeHandleColorG", Color.magenta.g );                  EditorPrefs.SetFloat( "CubeHandleColorB", Color.magenta.b );                    //Hide Unitys Tool handles (like the move tool) while we draw our own stuff                  Tools.hidden = true;                  break;              default:                  EditorPrefs.SetBool( "IsLevelEditorEnabled", true );                  EditorPrefs.SetBool( "SelectBlockNextToMousePosition", true );                  EditorPrefs.SetFloat( "CubeHandleColorR", Color.yellow.r );                  EditorPrefs.SetFloat( "CubeHandleColorG", Color.yellow.g );                  EditorPrefs.SetFloat( "CubeHandleColorB", Color.yellow.b );                    //Hide Unitys Tool handles (like the move tool) while we draw our own stuff                  Tools.hidden = true;                  break;              }          }      }        static LevelEditorE07ToolsMenu()      {          SceneView.onSceneGUIDelegate -= OnSceneGUI;          SceneView.onSceneGUIDelegate += OnSceneGUI;            // EditorApplication.hierarchyWindowChanged可以讓我們知道是否在編輯器加載了一個新的場景          EditorApplication.hierarchyWindowChanged -= OnSceneChanged;          EditorApplication.hierarchyWindowChanged += OnSceneChanged;      }        void OnDestroy()      {          SceneView.onSceneGUIDelegate -= OnSceneGUI;            EditorApplication.hierarchyWindowChanged -= OnSceneChanged;      }        static void OnSceneChanged()      {          if( IsInCorrectLevel() == true )          {              Tools.hidden = LevelEditorE07ToolsMenu.SelectedTool != 0;          }          else          {              Tools.hidden = false;          }      }        static void OnSceneGUI( SceneView sceneView )      {          if( IsInCorrectLevel() == false )          {              return;          }            DrawToolsMenu( sceneView.position );      }        static bool IsInCorrectLevel()      {          return UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE07"              || UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE08"              || UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name == "GameE09";      }        static void DrawToolsMenu( Rect position )      {          // 通過使用Handles.BeginGUI(),我們可以開啟繪製Scene視圖的GUI元素          Handles.BeginGUI();            //Here we draw a toolbar at the bottom edge of the SceneView          // 這裡我們在Scene視圖的底部繪製了一個工具條          GUILayout.BeginArea( new Rect( 0, position.height - 35, position.width, 20 ), EditorStyles.toolbar );          {              string[] buttonLabels = new string[] { "None", "Erase", "Paint" };                // GUILayout.SelectionGrid提供了一個按鈕工具條              // 通過把它的返回值存儲在SelectedTool里可以讓我們根據不同的按鈕來實現不同的行為              SelectedTool = GUILayout.SelectionGrid(                  SelectedTool,                  buttonLabels,                  3,                  EditorStyles.toolbarButton,                  GUILayout.Width( 300 ) );          }          GUILayout.EndArea();            Handles.EndGUI();      }  }

場景八

關注點

  • 可以在場景七的基礎上,點擊相應按鈕後增加或刪除Cube

新的編輯器腳本邏輯和場景七類似,重點在於回調函數OnSceneGUI:

static void OnSceneGUI(SceneView sceneView)  {      if (IsInCorrectLevel() == false)      {          return;      }        if (LevelEditorE07ToolsMenu.SelectedTool == 0)      {          return;      }        // 通過創建一個新的ControlID我們可以把鼠標輸入的Scene視圖反應權從Unity默認的行為中搶過來      // FocusType.Passive意味着這個控制權不會接受鍵盤輸入而只關心鼠標輸入      int controlId = GUIUtility.GetControlID(FocusType.Passive);        // 如果是鼠標左鍵被點擊同時沒有其他特定按鍵按下的話      if (Event.current.type == EventType.mouseDown &&          Event.current.button == 0 &&          Event.current.alt == false &&          Event.current.shift == false &&          Event.current.control == false)      {          if (LevelEditorE06CubeHandle.IsMouseInValidArea == true)          {              if (LevelEditorE07ToolsMenu.SelectedTool == 1)              {                  // 如果選擇的是erase按鍵(從場景七的靜態變量SelectedTool判斷得到),移除Cube                  RemoveBlock(LevelEditorE06CubeHandle.CurrentHandlePosition);              }                if (LevelEditorE07ToolsMenu.SelectedTool == 2)              {                  /// 如果選擇的是add按鍵(從場景七的靜態變量SelectedTool判斷得到),添加Cube                  AddBlock(LevelEditorE06CubeHandle.CurrentHandlePosition);              }          }      }        // 如果按下了Escape,我們就自動取消選擇當前的按鈕      if (Event.current.type == EventType.keyDown &&          Event.current.keyCode == KeyCode.Escape)      {          LevelEditorE07ToolsMenu.SelectedTool = 0;      }        // 把我們自己的controlId添加到默認的control里,這樣Unity就會選擇我們的控制權而非Unity默認的Scene視圖行為      HandleUtility.AddDefaultControl(controlId);  }

場景九

關注點

  • 使用Scriptable Object把一些Prefab預覽在Scene視圖上

Scriptable Object是一個相當於自定義Assets對象的類。下面是LevelBlocks的定義。它包含了一個LevelBlockData的數組來存儲可選的Prefab對象。

using UnityEngine;  using System.Collections;  using System.Collections.Generic;    //[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 LevelBlockData  {      public string Name;      public GameObject Prefab;  }    //[CreateAssetMenu] creates an entry in the default Create menu of the ProjectView so you can easily create an instance of this ScriptableObject  [CreateAssetMenu]  public class LevelBlocks : ScriptableObject  {      //This ScriptableObject simply stores a list of blocks. It kind of acts like a database in that it stores rows of data      public List<LevelBlockData> Blocks = new List<LevelBlockData>();  }

我們之後就可以在Hierency視圖創建一個LevelBlock資源,Editor類則會加載這個資源來得到相應的數據。

static LevelEditorE09ScriptableObject()  {      SceneView.onSceneGUIDelegate -= OnSceneGUI;      SceneView.onSceneGUIDelegate += OnSceneGUI;        //Make sure we load our block database. Notice the path here, which means the block database has to be in this specific location so we can find it      //LoadAssetAtPath is a great way to load an asset from the project      m_LevelBlocks = AssetDatabase.LoadAssetAtPath<LevelBlocks>( "Assets/E09 - Scriptable Object/LevelBlocks.asset" );  }

Unite 2016上還有另一個專門講Scriptable Object的視頻,強烈建議看一下: