Unity – 编辑器扩展

最近有自定义Inspector面板的需求,在找资料的时候看到一个Unite Europe 2016的分享,新手向的编辑器开发教程,翻译学习一波。

【链接】
  1. 视频地址:Unite Europe 2016 – Editor Scripting for n00bs
  2. 他在14年也做过一次关于Editor的分享,更加的新手向:Unite 2014 – Editor Scripting from the real world
  3. 学习过程中发现有一位小姐姐也做过16年这个视频的翻译:【Unity】编辑器小教程
【Unite Europe 2016 – Editor Scripting for n00bs】
1. Gizmos

image

Gizmos是Unity中的可视化辅助类。
在Game面板点击Gizmos按钮,就能在Game面板也看到这些可视化信息了。

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

1.2 OnDrawGizmosSelected()是在脚本所附加的物体被选中的时候调用的。

//选中物体时,画黄色的Cube。
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.3 Gizmos.DrawXXX

DrawLine,DrawRay,DrawSphere…可以画线段、射线、球等一大堆东西。

2. Custom Inspector – Basic

image

2.1 空格,标题和滑动条

    [Space( 10 )]
    public float MaximumHeight;
    public float MinimumHeight;

    [Header( "Safe Frame" )]
    [Range( 0f, 1f )]
    public float SafeFrameTop;
    [Range( 0f, 1f )]
    public float SafeFrameBottom;
  • [Space( 10 )]:空10个像素的距离
  • [Header( “Safe Frame” )]:添加属性的标题“Safe Frame”
  • [Range( 0f, 1f )]:把属性值控制在0-1的范围内,并在Inspector面板中添加滑动条

与这些相似的属性还有:
– [AddComponentMenu(“XXX”)]:将一个XXX脚本添加到Component菜单中
– [RequireComponent(typeof(XXX))]:自动添加一个需要的XXX组件,不能移除(如果已经存在则不会再重复添加
– [ContextMenu(“XXX Func”)]:添加一个属性到该组件上,可以通过右键组件调用到它,并且在非运行状态下执行该函数
– [HelpURL(“XXX url”)]:提供一个自定义的链接,点击组件上的文档图标可以打开该链接
– [Multiline(5)]:给string类型添加多行输入
– [Tooltip(“XXX”)]:当鼠标停在该属性时,显示XXX提示

2.2 自定义Inspector面板属性

上图中Camera Height属性滑动条的实现。

  • 记住using UnityEditor
  • 要把这个XXXEditor.cs脚本放在Editor文件夹下
  • [CustomEditor(typeof(XXX))]:重定义XXX组件在Inspector面板的绘制
  • 通过重写OnInspectorGUI()函数来绘制自定义的Inspector面板
//GameCameraEditor.cs
using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor( typeof( GameCamera ) )]
public class GameCameraEditor : Editor 
{
    GameCamera m_Target;

    public override void OnInspectorGUI()
    {
        m_Target = (GameCamera)target;

        //DrawDefaultInspector()函数让Unity按默认方式绘制Inspector面板
        //这个方法在我们只有个别属性需要自定义的时候特别实用
        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.RecordObject( m_Target, "Change Camera Height" );
            m_Target.transform.position = cameraPosition;
        }
    }
}
3. Custom Inspector – 自定义List

image

功能说明:分别在OnTriggerEnter和OnTriggerExit时,将物体设置为不同的状态。总的状态列表是个List,现在需要自定义这个List的显示面板。

3.1 序列化PistonState类
[System.Serializable]:如果组件中有公开的数组或变量时,这个属性告诉Unity来序列化这个类。

//PistonState.cs
[System.Serializable]
public class PistonState 
{
    public string Name;
    public Vector3 Position;
}

序列化
定义:根据Unity的官方定义,序列化就是将数据结构或对象状态转换成可供Unity保存和随后重建的自动化处理过程。
用途:序列化可以用于跨平台。实际上就是把一段数据翻译成(序列化)比较底层的语言(如汇编、机器语言),而基于这个底层语言再可以翻译(反序列化)成多种上一层的语言。

3.2 [HideInInspector]
[HideInInspector]:在Inspector面板隐藏这个属性的默认绘制,因为我们打算用自己的方法来绘制。

//PistonE03.cs
[HideInInspector]
public List<PistonState> States = new List<PistonState>();

3.3 自定义绘制List

3.3.1 首先也是继承Editor,重写OnInspectorGUI()

//PistonE03Editor.cs
[CustomEditor( typeof( PistonE03 ) )]
public class PistonE03Editor : Editor 
{
    PistonE03 m_Target;

    public override void OnInspectorGUI()
    {
        m_Target = (PistonE03)target;

        //按照默认方式绘制Speed和AddForce属性
        DrawDefaultInspector();
        //然后开始自定义绘制State List
        DrawStatesInspector();        
    }
}

3.3.2 自定义绘制State List对象

    void DrawStatesInspector()
    {
        //空格和States标题
        GUILayout.Space( 5 );
        GUILayout.Label( "States", EditorStyles.boldLabel );

        //绘制每一个State
        for( int i = 0; i < m_Target.States.Count; ++i )
        {
            DrawState( i );
        }

        //绘制底下的Add new State Button
        DrawAddStateButton();
    }

3.3.3 每一个State的绘制

  • SerializedProperty & serializedObject:用于对象编辑器属性,是完全通用的方法,自动处理撤销和为预设的UI样式。
  • GUILayout.BeginHorizontal():开始水平组,在这个组里的控件会被水平排列。用GUILayout.EndHorizontal()关闭。
  • BeginChangeCheck() & EndChangeCheck():可以检测之间的变量是否改变。如果改变了,EndChangeCheck()会返回true
  • EditorUtility.SetDirty(XXX):如果没有使用serializedObject,当一个组件的变量被修改的时候,你需要通过这个函数主动告诉Unity他被修改了,这样在你下次保存工程的时候,这个被修改过的变量也会被保存。
  • EditorUtility.DisplayDialog(title,message,ok,cancel):Unity内置的一个对话框面板,十分方面。
    void DrawState( int index )
    {
        if( index < 0 || index >= m_Target.States.Count )
        {
            return;
        }

        SerializedProperty listIterator = serializedObject.FindProperty( "States" );

        GUILayout.BeginHorizontal();
        {
            //如果是实例化的prefab对象,我们模仿Unity默认的Inspector操作,让修改过的变量用粗体表示
            if( listIterator.isInstantiatedPrefab == true )
            {
                EditorGUIHelper.SetBoldDefaultFont( listIterator.GetArrayElementAtIndex( index ).prefabOverride );
            }

            GUILayout.Label( "Name", EditorStyles.label, GUILayout.Width( 50 ) );

            EditorGUI.BeginChangeCheck();
            string newName = GUILayout.TextField( m_Target.States[ index ].Name, GUILayout.Width( 120 ) );
            Vector3 newPosition = EditorGUILayout.Vector3Field( "", m_Target.States[ index ].Position );

            if( EditorGUI.EndChangeCheck() )
            {
                Undo.RecordObject( m_Target, "Modify State" );

                m_Target.States[ index ].Name = newName;
                m_Target.States[ index ].Position = newPosition;

                EditorUtility.SetDirty( m_Target );
            }

            EditorGUIHelper.SetBoldDefaultFont( false );

            //点击Remove按钮后,显示一个面板确认是否删除
            if( GUILayout.Button( "Remove" ) )
            {
                EditorApplication.Beep();

                //点yes返回true,先记录操作到Undo中,然后删除
                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();
    }

3.3.4 绘制Add new State Button

    void DrawAddStateButton()
    {
        if( GUILayout.Button( "Add new State", GUILayout.Height( 30 ) ) )
        {
            Undo.RecordObject( m_Target, "Add new State" );
            m_Target.States.Add( new PistonState { Name = "New State" } );
            EditorUtility.SetDirty( m_Target );
        }
    }
4. Advanced Custom Inspector – 可重排序的List

image

介绍ReorderableList,以及如何使用。
有一篇博客详细地介绍了介绍ReorderableList:Unity make your lists functional with ReorderableList

  • using UnityEditorInternal:是一些还在内测或者供自己使用的库,官方其实并不太希望用户使用。
  • [CanEditMultipleObjects]:当选择相同类型的多个对象时,可以同时对它们进行编辑。

4.1 OnEnable()做好准备工作

//PistonE04PatternEditor.cs
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System.Collections;

[CanEditMultipleObjects]
[CustomEditor( typeof( PistonE04Pattern ) )]
public class PistonE04PatternEditor : Editor 
{
    ReorderableList m_List;
    PistonE03 m_Piston;

    //当自定义Inspector面板被打开时调用
    void OnEnable()
    {
        if( target == null )
        {
            return;
        }

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

4.1.1 找到PistonE03组件,这里是为了得到State列表

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

4.1.2 创建ReorderableList对象
ReorderableList( serializedObject, elements, draggable, displayHeader, displayAddButtion, displayRemoveButton)

    void CreateReorderableList()
    {
        m_List = new ReorderableList(
                        serializedObject,
                        serializedObject.FindProperty( "Pattern" ),
                        true, true, true, true );
    }

4.1.3 设置ReorderableList对象头部的绘制方法
通过drawHeaderCallback回调函数,实现头部State和Delay两个label。

    void SetupReoirderableListHeaderDrawer()
    {
        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" );
        };
    }

4.1.4 设置ReorderableList对象每个元素的绘制方法
通过drawElementCallback回调函数。确保你绘制的所有东西都与你在这个回调中收到的矩形变量是相对的。

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

4.1.5 设置ReorderableList对象[+]号按钮的回调函数
本项目的需求是:点击[+]号按钮后,显示State列表中的所有State选项,点击选项添加一个PistonStatePattern。

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

            //根据States生成一个Menu
            var menu = new GenericMenu();

            foreach( PistonState state in m_Piston.States )
            {
                menu.AddItem( new GUIContent( state.Name ),
                              false,
                              OnReorderableListAddDropdownClick,
                              state );
            }
            //显示菜单
            menu.ShowAsContext();
        };
    }

    //当点击Menu中的选项时的回调函数
    void OnReorderableListAddDropdownClick( object target ) 
    {
        PistonState state = (PistonState)target;

        //列表size增加1
        int index = m_List.serializedProperty.arraySize;
        m_List.serializedProperty.arraySize++;
        m_List.index = index;

        //初始化新添加的State属性值
        SerializedProperty element = m_List.serializedProperty.GetArrayElementAtIndex( index );
        element.FindPropertyRelative( "Name" ).stringValue = state.Name;
        element.FindPropertyRelative( "DelayAfterwards" ).floatValue = 0f;

        //应用修改后的属性值
        serializedObject.ApplyModifiedProperties();
    }  

4.2 绘制!

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

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

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

        m_List.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
5. Editor Window

image

实现一个自定义的编辑器窗口。
功能说明:本工程的自定义编辑器窗口主要用来预览方块的行为。

  • EditorWindow:自定义的编辑器窗口需要继承自EditorWindow,创建出来的窗口可以拖拽等和Unity默认窗口一样的操作。

5.1 在Unity菜单栏中的入口
– [MenuItem( “XXX/YYY” )]:可以在菜单栏中通过 XXX -> YYY 打开窗口。

//PreviewPlaybackWindow.cs
public class PreviewPlaybackWindow : EditorWindow 
{
    [MenuItem( "Window/Preview Playback Window" )]
    static void OpenPreviewPlaybackWindow()
    {
        EditorWindow.GetWindow<PreviewPlaybackWindow>( false, "Playback" );
    }
}

5.2 注册update回调
– EditorApplication.update:在编辑器中每秒会调用30次update。
– Repaint():重新绘制窗口,Unity在自己认为需要重新绘制的时候会调(比如说移动窗口的时候),我们也可以主动调用来强制马上绘制。

    void OnEnable()
    {
        //注册自己的OnUpdate函数
        EditorApplication.update -= OnUpdate;
        EditorApplication.update += OnUpdate;
    }

    void OnDisable()
    {
        EditorApplication.update -= OnUpdate;
    }

    void OnUpdate()
    {
        if( m_PlaybackModifier != 0f )
        {
            PreviewTime.Time += ( Time.realtimeSinceStartup - m_LastTime ) * m_PlaybackModifier;

            Repaint();

            //主动重新绘制Scene面板
            SceneView.RepaintAll();
        }

        m_LastTime = Time.realtimeSinceStartup;
    }

5.3 绘制Playback窗口中的内容

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

image

6.1 Init & onSceneGUIDelegate

  • [InitializeOnLoad]:确保当编辑器被加载的时候就调用这个类的构造函数
  • SceneView.onSceneGUIDelegate:当Scene面板被重绘的时候会回调,可以在Scene面板中自定义绘制GUI信息
//LevelEditorE06CubeHandle.cs
[InitializeOnLoad]
public class LevelEditorE06CubeHandle : Editor 
{
    static LevelEditorE06CubeHandle()
    {
        SceneView.onSceneGUIDelegate -= OnSceneGUI;
        SceneView.onSceneGUIDelegate += OnSceneGUI;
    }
}

6.2 自定义绘制Scene面板GUI信息
更新Handle位置,确认是否有效,绘制。

    static void OnSceneGUI( SceneView sceneView )
    {
        //...

        UpdateHandlePosition();
        UpdateIsMouseInValidArea( sceneView.position );
        UpdateRepaint();

        DrawCubeDrawPreview();
    }

6.2.1 更新Handle位置
从鼠标位置发出一条射线,只与Level层相交,若产生碰撞,取碰撞点法线方向Cube的中心点作为Handle位置。

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

6.2.2 确认鼠标位置是否有效
在Scene面板下方一栏留出一栏空间用于制作控件,当鼠标移到这一区域时,重绘Scene面板。

    static void UpdateIsMouseInValidArea( Rect sceneViewRect )
    {
        bool isInValidArea = Event.current.mousePosition.y < sceneViewRect.height - 35;

        if( isInValidArea != IsMouseInValidArea )
        {
            IsMouseInValidArea = isInValidArea;
            SceneView.RepaintAll();
        }
    }

6.2.3 重绘Scene面板
当鼠标位置改变的时候,重绘Scene面板,并记录鼠标位置。

    static void UpdateRepaint()
    {
        //If the cube handle position has changed, repaint the scene
        if( CurrentHandlePosition != m_OldHandlePosition )
        {
            SceneView.RepaintAll();
            m_OldHandlePosition = CurrentHandlePosition;
        }
    }

6.2.4 在鼠标位置处画一个Cube
如果在控件栏就不画了。

    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.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 );
    }
7. Handles GUI

image

再上一个场景预留的位置绘制一栏控件。

7.1 注册

  • EditorApplication.hierarchyWindowChanged:判断用户是否加载了一个新的场景
//LevelEditorE07ToolsMenu.cs
    static LevelEditorE07ToolsMenu()
    {
        SceneView.onSceneGUIDelegate -= OnSceneGUI;
        SceneView.onSceneGUIDelegate += OnSceneGUI;

        EditorApplication.hierarchyWindowChanged -= OnSceneChanged;
        EditorApplication.hierarchyWindowChanged += OnSceneChanged;
    }

7.2 绘制控件
绘制一个工具条,3个单选按钮。

    static void OnSceneGUI( SceneView sceneView )
    {
        //...
        DrawToolsMenu( sceneView.position );
    }

    static void DrawToolsMenu( Rect position )
    {

        Handles.BeginGUI();

        GUILayout.BeginArea( new Rect( 0, position.height - 35, position.width, 20 ), EditorStyles.toolbar );
        {
            string[] buttonLabels = new string[] { "None", "Erase", "Paint" };

            SelectedTool = GUILayout.SelectionGrid(
                SelectedTool, 
                buttonLabels, 
                3,
                EditorStyles.toolbarButton,
                GUILayout.Width( 300 ) );
        }
        GUILayout.EndArea();

        Handles.EndGUI();
    }

7.3 设置3个点击按钮
0:不选中任何工具
1:橡皮擦工具
2:增加Cube工具

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

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

                Tools.hidden = true;
                break;
            }
        }
    }
8. Add and Remove Objects

给上一个场景的工具条按钮添加各自对应的具体功能

8.1 还是原来的配方,还是熟悉的代码

//LevelEditorE08AddAndRemoveObjects.cs
    static LevelEditorE08AddAndRemoveObjects()
    {
        SceneView.onSceneGUIDelegate -= OnSceneGUI;
        SceneView.onSceneGUIDelegate += OnSceneGUI;
    }

8.2 交互方式
– GUIUtility.GetControlID():创建一个新的ControlID来获取Scene面板的鼠标输入,阻止了Unity默认的处理方式
– FocusType.Passive:意味着这个控制只关心鼠标输入而不关心键盘输入

    static void OnSceneGUI( SceneView sceneView )
    {
        //...
        //如果是None选项,则跳过绘制步骤
        if( LevelEditorE07ToolsMenu.SelectedTool == 0 )
        {
            return;
        }

        int controlId = GUIUtility.GetControlID( FocusType.Passive );

        //如果左键点击了,并且没有按下alt,shift,ctrl这些按钮
        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 )
            {
                //如果是Eraser选项
                if( LevelEditorE07ToolsMenu.SelectedTool == 1 )
                    RemoveBlock( LevelEditorE06CubeHandle.CurrentHandlePosition );

                //如果是Paint选项
                if( LevelEditorE07ToolsMenu.SelectedTool == 2 )
                    AddBlock( LevelEditorE06CubeHandle.CurrentHandlePosition );
            }
        }

        //按下ESC键选择None选项
        if( Event.current.type == EventType.KeyDown &&
            Event.current.keyCode == KeyCode.Escape )
            LevelEditorE07ToolsMenu.SelectedTool = 0;

        //Add our controlId as default control so it is being picked instead of Unitys default SceneView behaviour
        //把我们的 controlId 加到 default control 中,这样Unity就会默认选择我们的 controlId ,而不是Unity默认的Scene面板行为了
        HandleUtility.AddDefaultControl( controlId );
    }

8.3 具体的增删行为

    //在给定的位置创建一个Cube
    public static void AddBlock( Vector3 position )
    {
        GameObject newCube = GameObject.CreatePrimitive( PrimitiveType.Cube );
        newCube.transform.parent = LevelParent;
        newCube.transform.position = position;
        newCube.AddComponent<BoxCollider>();
        newCube.tag = "LevelCube";
        newCube.layer = LayerMask.NameToLayer( "Level" );
        newCube.GetComponent<Renderer>().material = (Material)AssetDatabase.LoadAssetAtPath( "Assets/Shared Resources/Materials/Grid@GreenBlue.mat", typeof( Material ) );

        //注册Undo行为
        Undo.RegisterCreatedObjectUndo( newCube, "Add Cube" );

        //标注Dirty,这样会在下次保存场景或工程的时候保存这个Cube
        UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty();
    }

    //删除给定位置的Cube
    public static void RemoveBlock( Vector3 position )
    {
        for( int i = 0; i < LevelParent.childCount; ++i )
        {
            float distanceToBlock = Vector3.Distance( LevelParent.GetChild( i ).transform.position, position );
            if( distanceToBlock < 0.1f )
            {
                Undo.DestroyObjectImmediate( LevelParent.GetChild( i ).gameObject );

                UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty();
                return;
            }
        }
    }
9. Scriptable Objects

ScriptableObject
官方文档解释:
1 如果你想创建Objects但又不想挂在GameObjects上,那么你可以从ScriptableObject这个类派生出一个类。(ScriptableObject继承自Object)
2 这对于只用于存储数据的资源最有用。
3 为了使创建脚本对象实例变得容易,这些实例与工程中的资源绑定在一起,详情查看 CreateAssetMenuAttribute

image

如图,运用Scriptable Object在Scene面板上预览Prefab

9.1 LevelBlocks类

  • [CreateAssetMenu]:在默认的Create菜单中创建了一个入口, 你可以方便的创建这个ScriptableObject类的实例。(Assets -> Create -> LevelBlocks)
//LevelBlocks.cs
[System.Serializable]
public class LevelBlockData
{
    public string Name;
    public GameObject Prefab;
}

[CreateAssetMenu]
public class LevelBlocks : ScriptableObject 
{
    //这个脚本储存了一列表的block。它就像一个简单的数据库。
    public List<LevelBlockData> Blocks = new List<LevelBlockData>();
}

9.2 填好数据

image

9.3 Prefab预览面板的实现

  • LoadAssetAtPath:是一种从项目中加载资源的好方法
    static LevelEditorE09ScriptableObject()
    {
        SceneView.onSceneGUIDelegate -= OnSceneGUI;
        SceneView.onSceneGUIDelegate += OnSceneGUI;

        //确保我们加载了block的数据库
        m_LevelBlocks = AssetDatabase.LoadAssetAtPath<LevelBlocks>( "Assets/E09 - Scriptable Object/LevelBlocks.asset" );
    }

    static void OnSceneGUI( SceneView sceneView )
    {
        //...如果block列表不是空的

        DrawCustomBlockButtons( sceneView );
        HandleLevelEditorPlacement();
    }

    //在Scene面板左侧绘制我们定义的block列表
    static void DrawCustomBlockButtons( SceneView sceneView )
    {
        Handles.BeginGUI();

        GUI.Box( new Rect( 0, 0, 110, sceneView.position.height - 35 ), GUIContent.none, EditorStyles.textArea );

        for( int i = 0; i < m_LevelBlocks.Blocks.Count; ++i )
        {
            DrawCustomBlockButton( i, sceneView.position );
        }

        Handles.EndGUI();
    }

    static void DrawCustomBlockButton( int index, Rect sceneViewRect )
    {
        bool isActive = false;

        if( LevelEditorE07ToolsMenu.SelectedTool == 2 && index == SelectedBlock )
        {
            isActive = true;
        }

        //通过将一个Prefab或者GameObject传递到AssetPreview中。GetAssetPreview你会得到一个这个对象的预览贴图
        Texture2D previewImage = AssetPreview.GetAssetPreview( m_LevelBlocks.Blocks[ index ].Prefab );
        GUIContent buttonContent = new GUIContent( previewImage );

        GUI.Label( new Rect( 5, index * 128 + 5, 100, 20 ), m_LevelBlocks.Blocks[ index ].Name );
        bool isToggleDown = GUI.Toggle( new Rect( 5, index * 128 + 25, 100, 100 ), isActive, buttonContent, GUI.skin.button );

        //如果这个按钮被点击了,并且它之前没有被点击
        //选择相应Prefab,并且把Scene面板下方的工具栏选择Paint工具
        if( isToggleDown == true && isActive == false )
        {
            SelectedBlock = index;
            LevelEditorE07ToolsMenu.SelectedTool = 2;
        }
    }

–Done–

Add a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注