Unity編輯器案列
- 2019 年 12 月 2 日
- 筆記
【Unity】編輯器小教程
寫在前面
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可以打開內置對話框

- 首先在面板上隱藏默認的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的視頻,強烈建議看一下: