Unity编辑器扩展

继承自 MonoBehaviour 的类的一些扩展

一方面继承自MonoBehaviour的类都会默认被添加到 Component->Scripts 节点下。
另一方面MonoBehaviour类都可以添加到GameObject作为Component,在Inspector界面可以显示该类的一些信息。
所有继承自UnityEngine.Object的类,如GameObject,Component,MonoBehaviour,Texture2D,AnimationClip;所有基本类型,如int,string,float,bool;一些内建类型,如Vector2,Vector3,Quaternion,Color,Rect,Layermask;序列化类型的Array,序列化类型的List;枚举Enum。默认情况下这些类型的public变量是可以在Inspector界面显示的,同时编辑器对其进行序列化。

一些涉及到变量显示和序列化的属性

  • [HideInInspector] 变量依旧可以被序列化,但是在Inspector界面上不可见
  • [NonSerialized] 变量不可被序列化,且在Inspector面板上不可见
  • [SerializeField] 可以把private类型变量变成可序列化的,在Inspector面板可见。(虽然Inspector面板可见,但仍然是private变量)

ContextMenu属性

继承自MonoBehaviour的类的内部,可以为其函数添加ContextMenu属性,在编辑器界面下执行一些操作。

public class TestContextMenu : MonoBehaviour
{
    [ContextMenu("DoLogTest")]
    void DoLogTest()
    {
        Debug.Log("i am function DoLogTest");
    }
}

如上述代码,当把TestContextMenu脚本附加到GameObject上后,在GameObject的Inspector界面中会有TestContextMenu组件的信息,右键该组件(或者左键点击3个.按钮)会看到DoLogTest菜单项,点击后会按照预期打印日志。

ContextMenuItem属性

可以为变量(public)添加右键弹出命令,从而执行相关的操作。
该标识接受两个变量,1个是display name,一个是右键弹出菜单点击后的方法。

public class Test : MonoBehaviour
{
    [ContextMenuItem("Random Age", "RandomAge")]
    public int Age;
    void RandomAge()
    {
        Age = new System.Random(DateTime.Now.Millisecond).Next(1, 100);
    }

    [ContextMenuItem("Random Name", "RandomName")]
    public string Name;
    private void RandomName()
    {
        string[] names = new string[] { "Jack", "Jim", "Tomas", "Han", "Ann" };
        Name = names[new System.Random(DateTime.Now.Millisecond).Next(0, 4)];
    }
}

上述代码分别为Age和Name这两个字段添加了右键弹出菜单的功能。

Range属性

[Range(min,max)]或者[RangeAttribute(min,max)]可以对变量的输入范围进行限定,使得Inspector检视面板内的数值输入框变成Slider,且范围为(min,max)。

Header属性

[Header(“”)] Inspector面板中在目标字段顶部展示额外的说明文字

窗口扩展基础:MenuItem,ScriptableWizard,EditorWindow

MenuItem

使用MenuItem标识可以为编辑器添加新的菜单,也可以为Inspector上下文添加菜单。点击后执行一些特定的逻辑,没有额外的操作界面。只有静态方法可以使用该标识,该标识可以把静态方法转换为菜单命令。
另外可以为菜单创建快捷方式(hotkey),你可以使用如下特殊修饰字符:%(Windows系统上的ctrl,OS X系统上的cmd),#(shift),&(alt)。比如快捷方式“shift-alt-g”的写法为[MenuItem(“XX/XX/XXX #&g”)]。如果不需要为hotkey提供修饰符需要用字符_修饰,如快捷方式“g”的写法为[MenuItem(“XX/XX/XXX _g”)]
另外还可以支持一些特殊的字符:LEFT、RIGHT、UP、DOWN、F1…F12、HOME、END、PGUP、PGDN。
示例代码:

using UnityEngine;  
using System.Collections;  
using UnityEditor;  

public class AddChild
{
    [MenuItem("Custom/Create Child For Selected GameObjects")]
    static void MenuAddChild()
    {
        Transform[] transforms = Selection.GetTransforms(SelectionMode.TopLevel | SelectionMode.OnlyUserModifiable);
        foreach (Transform transform in transforms)
        {
            GameObject aChild = new GameObject("_Child");
            aChild.transform.parent = transform;
        }
    }
    [MenuItem("Custom/Create Child For Selected GameObjects", true)]
    static bool ValidateMenuAddChild()
    {
        return Selection.activeGameObject != null;
    }
}

上述示例可以在Unity菜单项创建新的目录菜单:Custom/Create Child For Selected GameObjects,同时提供了Validate函数确保当前若没有物体被选中的话,该菜单项是灰化不可点击的。

需要注意的是,入口函数和Validate函数都必须是静态的,Validate函数名由Validate和入口函数名组合而成,Validate函数的MenuItem标识中的第1个参数和入口函数的一致,第2个参数为true。

排序、分组

MenuItem函数原型为:public MenuItem(string itemName, bool isValidateFunction, int priority);易知通过控制第3个参数便可以设置分组。
第3个int参数,值越小越排在上面,Unity会自动分组,50为单位。如下Option1和Option2会自动被分成不同的组,具体表现就是有个分割线。

[MenuItem("NewMenu/Option1", false, 3)]
private static void NewMenuOption1()
{
}

[MenuItem("NewMenu/Option2", false, 51)]
private static void NewMenuOption2()
{
}
勾选

下述代码实现Test1和Test2两项,互斥选择,且默认情况下选择Test1

[InitializeOnLoad]
public class TestMenuItem  
{
    static string menuPath1 = "Custom/Test1";
    static string menuPath2 = "Custom/Test2";
    static TestMenuItem()
    {
        //默认选择Test1
        if (Menu.GetChecked(menuPath1) == false && Menu.GetChecked(menuPath2) == false)
        {
            Menu.SetChecked(menuPath1, true);
        }
    }
    [MenuItem("Custom/Test1", false, 1)]
    static void Test1()
    {
        if (Menu.GetChecked(menuPath1)==false)
        {
            Menu.SetChecked(menuPath1, true);
            Menu.SetChecked(menuPath2, false);
        }
    }
    [MenuItem("Custom/Test2", false, 2)]
    static void Test2()
    {
        if (Menu.GetChecked(menuPath2)==false)
        {
            Menu.SetChecked(menuPath1, false);
            Menu.SetChecked(menuPath2, true);
        }
    }
}
一些特定目录结构

上述提到通过MenuItem标识可以为Unity创建新的目录,不仅如此还可以为已有的菜单增加内容。

  • MenuItem(“GameObject/”) 新建菜单项出现在GameObject节点下。
  • MenuItem(“Assets/”) 新建菜单项出现在Assets节点下(Project面板下右键弹出的面板内容和Assets节点展开的是一致的)
  • MenuItem(“CONTEXT/组件名/XXX”) 可以为某个具体组件的Inspector上下文菜单增加内容。打开方式为右键组件或者左键组件最右边的“齿轮”按钮,需要注意的是Inspector上下文添加菜单不支持多重层次。
    如下代码为Transform组件扩展其Inspector上下文菜单:
public class AddChild
{
    [MenuItem("CONTEXT/Transform/Create Child For Selected GameObjects")]
    static void MenuAddChild()
    {
        Transform[] transforms = Selection.GetTransforms(SelectionMode.TopLevel | SelectionMode.OnlyUserModifiable);
        foreach (Transform transform in transforms)
        {
            GameObject aChild = new GameObject("_Child");
            aChild.transform.parent = transform;
        }
    }
    [MenuItem("CONTEXT/Transform/Create Child For Selected GameObjects", true)]
    static bool ValidateMenuAddChild()
    {
        return Selection.activeGameObject != null;
    }
}

如下代码为获得Inspector上下文菜单对应的组件:

[MenuItem("CONTEXT/Rigidbody/DoSomething")]
static void DoSomething(OnInspectorGUI command)
{
    Rigidbody body=command.context;
    body.mass=5;
}
代码执行菜单项

EditorApplication.ExecuteMenuItem(“XX/XX/XX”)

ScriptableWizard

通过继承ScriptableWizard可以创建编辑器向导,Unity已经为我们封装好了一些变量、方法、消息。如:

  • 变量:errorString(设置向导的错误提示信息)、helpString(设置向导的帮助提示)、isValid(可以控制向导的Create Button和Other Button能否点击)。
  • 静态方法:DisplayWizard
  • 消息:OnWizardCreate(当点击Create按钮的时候触发)、OnWizardOtherButton(当点击Other按钮的时候)、OnWizardUpdate(当向导打开的瞬间或者向导中有输入参数的变化时触发)。

示例:

public class CreateACube : ScriptableWizard
{
    public float size = 1f;//声明为public可以被向导序列化显示在界面上,且可以改变其值
    [MenuItem("Custom/CreateACube")]
    static void CreateACubeWizard()
    {        
        ScriptableWizard.DisplayWizard("创建一个Cube", typeof(CreateACube), "确定", "取消");
        //OR
        //ScriptableWizard.DisplayWizard<CreateACube>("创建一个Cube", "确定", "取消"); 
    } 
    void OnWizardCreate()
    {
        GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
        obj.transform.localScale = new Vector3(size, size, size);
    }

    void OnWizardOtherButton()
    {
        Close();//点击otherButton,需要调用Close()方法,才关闭向导。但是点击createButton,并无需调用Close()方法而自动关闭。
    }

    void OnWizardUpdate()
    {
        helpString = "输入Cube的Size,Size要大于等于3,创建一个Cube";

        if (size < 3)
        {
            errorString = "size要大于等于3";
            isValid = false;
        }
        else
        {
            errorString = "";
            isValid = true;
        }
    }
}

EditorWindow

继承自EditorWindow的类,可以实现更复杂的编辑器窗口功能。且这种窗口是可以自由内嵌到Unity编辑器内,共同组成编辑器的Layout。
通过在OnGUI()函数内调用GUILayout、EditorGUILayout、GUI等类的一些方法来实现复杂的界面。
下面通过EditorWindow来实现上述的编辑器向导,相关代码如下:

public class CreateACube2 : EditorWindow
{
    float size = 1f;
    string helpString = "输入Cube的Size,Size要大于等于3,创建一个Cube";
    string errorString = "size需要大于等于3";
    bool enableConfromButton = false;
    static Color originColor;
    [MenuItem("Custom/CreateACube2")]
    static void Init()
    {
        //获取已经打开的window,如果不存在则new一个
        CreateACube2 window = EditorWindow.GetWindow(typeof(CreateACube2)) as CreateACube2;
        originColor = GUI.color;
    }

    void OnGUI()
    {
        GUILayout.Label(helpString, EditorStyles.boldLabel);
        size = EditorGUILayout.FloatField("size:", size);

        enableConfromButton = size >= 3 ? true : false;
        if (enableConfromButton)
        {
            GUI.enabled = true;
        }
        else
        {
            GUI.enabled = false;//设置error的时候Button按钮不可点击
            GUI.color=Color.red;//设置errorString的颜色为红色
            GUILayout.Label(errorString);
        }
        GUI.color = originColor;
        if (GUILayout.Button("确定"))
        {
            DoCreate();
        }
    }

    void DoCreate()
    {
        GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
        obj.transform.localScale = new Vector3(size, size, size);
    }
} 

除了可以使用OnGUI()回调,还有如下一些函数可以丰富EditorWindow:

  • OnDestroy()当EditorWindow关闭的时候触发。
  • OnFocus()当Window获得焦点触发。
  • OnHierarchyChange()当Hierarchy面板内有改变的时候触发。
  • OnInspectorUpdate()每秒十帧的触发,来判断Inspector界面是否有改变。
  • OnLostFocus()当Window失去焦点触发。
  • Update()每秒100次的触发,在所有可见的Windows。

更多参考:Editor Windows

Property Drawers

//TODO:
–TODO:https://docs.unity3d.com/2021.3/Documentation/Manual/editor-PropertyDrawers.html

Custom Editors

Custom Editors是相对来说更强大的一种编辑器扩展。主要实现对Inspector界面和SceneView界面的扩展。

下面以一个最简单的实例来说明如何对Inpector界面扩展。
我们定义一个Player:

public class Player : MonoBehaviour
{    
    public int armor = 75;
    public int damage = 20;
    public GameObject gun;
} 

默认情况下,其对应的Inspector界面如下:

要自定义Player组件的Inspector界面,需要新建脚本PlayerEdiotr.cs并继承自Editor。把PlayerEditor.cs放到Editor目录下,代码如下:

[CanEditMultipleObjects]
[CustomEditor(typeof(Player))]
public class PlayerEditor : Editor
{
    SerializedProperty armorProp;
    SerializedProperty damageProp;
    SerializedProperty gunProp;

    void OnEnable()
    {
        armorProp = serializedObject.FindProperty("armor");
        damageProp = serializedObject.FindProperty("damage");
        gunProp = serializedObject.FindProperty("gun");
    }

    public override void OnInspectorGUI()
    {
        //更新serializedProperty,一般在OnInspector函数的一开始使用。
        serializedObject.Update();

        //依次重写Player.cs脚本中的armor,damage,gun
        EditorGUILayout.IntSlider(armorProp, 0, 100, new GUIContent("Armor"));
        if (!armorProp.hasMultipleDifferentValues)//当该值相同的时候才显示ProgressBar
        {
            ProgressBar(armorProp.intValue / 100.0f, "ArmorCount");
        }

        EditorGUILayout.IntSlider(damageProp, 0, 100, new GUIContent("Damage"));
        if (!damageProp.hasMultipleDifferentValues)
        {
            ProgressBar(damageProp.intValue / 100.0f, "DamagePower");
        }

        EditorGUILayout.PropertyField(gunProp, new GUIContent("Gun Object"));

        //使更改生效
        serializedObject.ApplyModifiedProperties();
    }

    private void ProgressBar(float value, string str)
    {
        Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
        EditorGUI.ProgressBar(rect, value, str);
        EditorGUILayout.Space();
    }
}

重写后界面如下:

上述代码使用了较常用的SerializedProperty和SerializedObject来实现对Inspector界面的重写。

  • [CustomEditor(typeof(Player))] 这个编辑器属性可以把Editor脚本和原始cs脚本建立关系,从而实现重写。

  • [CanEditMultipleObjects] 使用了这个编辑器属性,可以确保当选择的多个物体具有相同组件的时候不会报错“Multi-object editing not supported”,而且可以支持多个物体编辑。

  • OnInspectorGUI() 重写该方法可以实现对默认Inspector界面的重写。该方法也是最常用的一个方法。

还有一种重写Inspector界面的方式

代码如下:

[CanEditMultipleObjects]
[CustomEditor(typeof(Player))]
public class PlayerEditor : Editor
{
    Player player;
    //or: Player player { get { return target as Player; } }
    void OnEnable()
    {
        player = target as Player;
    }

    public override void OnInspectorGUI()
    {
        //base.OnInspectorGUI(); //调用父类方法绘制一次GUI,Player中原本的可序列化数据等会在这里绘制一次。 如果不调用父类方法,则这个Mono的Inspector全权由下面代码绘制。

        player.armor = EditorGUILayout.IntSlider("Armor", player.armor, 0, 100);
        ProgressBar(player.armor / 100f, "ArmorCount");

        player.damage = EditorGUILayout.IntSlider("Damage", player.damage, 0, 100);
        ProgressBar(player.damage / 100f, "DamagePower");

        bool allowSceneObjects = !EditorUtility.IsPersistent(player);
        player.gun = EditorGUILayout.ObjectField("Gun Object", player.gun, typeof(GameObject), allowSceneObjects) as GameObject;

        //数据有改变的话SetDirty
        if (GUI.changed)
        {
            EditorUtility.SetDirty(target);
        }
    }

    private void ProgressBar(float value, string str)
    {
        Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
        EditorGUI.ProgressBar(rect, value, str);
        EditorGUILayout.Space();
    }
}

该段代码对应的界面如下:

  • EditorUtility.IsPersistent(player) 该方法获得目标物体是否存在disk上。官方的解释为:Typically assets like prefabs, textures, audio clips, animation clips, materials are stored on disk.Returns false if the object lives in the scene. Typically this is a game object or component but it could also be a material that was created from code and not stored in an asset but instead stored in the scene.因此EdiotrUtility.IsPersistent(player)返回结果为false。

补充一些在OnInspectorGUI()方法中常用到的方法

  • DrawDefaultInspector() 绘制默认的对应脚本中的Inspector,常用于扩展一个已经存在的组件:比如想在Camera组件上添加个按钮,实现特殊功能,则可以写个类用[CustomEditor(typeof(Camera))]和Camera建立关系,然后在OnInspector()中先调用DrawDefaultInspector(),然后再添加需要的按钮之类的。

Scene界面扩展

扩展编辑器的SceneView界面

右键菜单

public class SceneInterative
{
    [InitializeOnLoadMethod]
    static void Init()
    {
        //Unity2018为SceneView.onSceneGUIDelegate+=
        SceneView.duringSceneGui += SceneView_duringSceneGui;
    }

    private static void SceneView_duringSceneGui(SceneView sceneView)
    {
        //获得当前事件
        Event e = Event.current;
        //鼠标右键放开时,弹出菜单
        if (e != null && e.button == 1 && e.type == EventType.MouseUp)
        {
            //设置右键菜单内容,点击事件,数据
            GenericMenu menu = new GenericMenu();
            menu.AddItem(new GUIContent("Home"), false, OnMenuClick, "menu_1");
            menu.AddItem(new GUIContent("Skill/Skill1"), false, OnMenuClick, "menu_2");
            menu.AddItem(new GUIContent("Skill/Skill2"), false, OnMenuClick, "menu_3");
            menu.ShowAsContext();

            //必须调用Use()方法,完成这次事件的使用,否则会误伤到Unity编辑器Scene视图内自带的右键事件
            e.Use();
        }
    }


    static void OnMenuClick(object userData)
    {
        EditorUtility.DisplayDialog("Tip", "OnMenuClick:" + userData.ToString(), "Ok");
    }
}

如上代码实现了在Scene界面内点击鼠标右键后弹出右键菜单,同时又不影响编辑器自带的鼠标交互。

  • 在duringSceneGui回调中获得当前的交互事件Event e=Event.current;,通过判断e.isKey, e.button, e.character,e.type来获得对应的鼠键操作
  • e.button的值:0为鼠标左键,1为鼠标右键,2为鼠标中键
  • 有时候我们会在Inspector界面内点击某个按钮后触发Scene界面交互,这个时候需要强制获得Scene的焦点,可以使用:(SceneView.sceneViews[0] as SceneView).Focus();
  • SceneView.RepaintAll();可以用来刷新Scene窗口
  • Scene界面内获得当前鼠标所在位置发射的射线:Ray ray=HandleUtility.GUIPointToWorldRay(e.mousePosition);
  • Scene内世界坐标转换为屏幕GUI坐标:HandleUtility.WorldToGUIPoint(worldPos);
一种自定义菜单的方式

上述使用GenericMenu来创建菜单并添加菜单项,下面展示通过EditorUtility.DisplayCustomMenu()展示自定义菜单方式。

            Vector2 mousePosition = e.mousePosition;
            //设置菜单项
            var options = new GUIContent[]{
                    new GUIContent("Test1"),
                    new GUIContent("Test2"),
                    new GUIContent(""),
                    new GUIContent("Test/Test3"),
                    new GUIContent("Test/Test4"),
                };
            //设置菜单显示区域
            var selected = -1;
            var userData = Selection.activeGameObject;
            var width = 100;
            var height = 100;
            var position = new Rect(mousePosition.x, mousePosition.y - height, width, height);
            //显示菜单
            EditorUtility.DisplayCustomMenu(position, options, selected, delegate (object data, string[] opt, int select)
            {
                Debug.Log(opt[select]+",name:"+userData.name);
            }, userData);

Scene视图中禁用选择对象

        Event e = Event.current;
        if (e != null)
        {
            int controlID = GUIUtility.GetControlID(FocusType.Passive);
            if (e.type == EventType.Layout)
            {
                HandleUtility.AddDefaultControl(controlID);
            }
        }

上述代码放到SceneView.duringSceneGui回调中。可以实现Scene视图无法选择对象,只能通过Hierarchy视图选择对象。通常用于复杂界面编辑,避免Scene视图误操作别的对象。

  • FocusType.Passive 禁止控件接收鼠标、键盘焦点
  • GUIUtility.GetControlID 获取控件ID
  • EventType.Layout 布局事件
  • HandleUtility.AddDefaultControl 添加默认控件ID

OnSceneGUI

OnSceneGUI()可以丰富Scene界面交互,一般用于继承自Editor类重写Inspector界面的类中,选中目标物体,才可触发OnSceneGUI()方法。(该方法实际上是和上面提到的Custom Editor配套的)。
Handles类中定义了很多可以直接在OnSceneGUI()方法中使用的方法。

这里以上述Player: MonoBehaviour类为例,进行扩展。

    Rect screenOrigo;
    void OnSceneGUI()
    { 
        screenOrigo = Camera.current.pixelRect;

        Handles.BeginGUI();//2D GUI物体(GUI or EditorGUI)的绘制必须放在BeginGUI()和EndGUI()之间。        
        var guiPoint = HandleUtility.WorldToGUIPoint(player.transform.position);
        GUI.color = Color.green;
        GUI.Label(new Rect(guiPoint.x-20,guiPoint.y-50,100,30), player.transform.name);
        if (GUI.Button(new Rect(guiPoint.x-30,guiPoint.y,30,20),"x+"))
        {
            player.transform.position = new Vector3(player.transform.position.x + 1, player.transform.position.y, player.transform.position.z);
        }
        if (GUI.Button(new Rect(guiPoint.x + 10, guiPoint.y, 30, 20), "x-"))
        {
            player.transform.position = new Vector3(player.transform.position.x - 1, player.transform.position.y, player.transform.position.z);
        }
        GUI.color = Color.white;
        if(GUI.Button(new Rect(screenOrigo.width-100,screenOrigo.height-50, 80, 30), "Reset"))
        {
            player.transform.position = Vector3.zero;
        }
        Handles.EndGUI();
    }

当把Player类附加到某Cube上后并选中该Cube,SceneView界面如下:

  • 实质上OnSceneGUI()中绘制的内容也属于Gizmos,也会受编辑器中开关Gizmos的显示控制。
  • 上文中提到的SceneView.duringSceneGui回调中其实也是可以进行类似OnSceneGUI()中的绘制的。同样2D GUI需要在Handles.BeginGUI()和Handles.EndGUI()之间进行绘制。区别就是duringSceneGui回调中的绘制不要求选中目标物体,绘制的内容可以实现常驻SceneView

获得SceneView界面的尺寸

Screen.width获得尺寸是对的,但是Screen.height获得的尺寸是包含了界面顶部标签区域和快捷键区域(即ribbon区)。
而OnSceneGUI()界面内添加GUI时(0,0)点不是从界面最左上点开始的,而是从空白区域算起的。因此获得空白区域的尺寸至关重要。
可以通过Camera.current.pixelRect获取,参考how-to-find-out-the-real-viewportscreen-size-in-sc

TreeView

TreeView

Gizmos&Handles

二者都可以扩展SceneView界面的显示。

Gizmos

可以在SceneView绘制line、sphere、icon、texture、mesh等用来丰富界面显示,方便调试等。
绘制需要在OnDrawGizmos或者OnDrawGizmosSelected方法中进行,这两个方法都要求类继承自MonoBehaviour。

  • OnDrawGizmos() 每帧都会调用
  • OnDrawGizmosSelected() 只有当附着了组件的物体被选中后才会调用

如下代码,在GameObject周围绘制一个10单位的cube(附着该组件的物体选中后才会显示Gizmos)

public class GizmosExample : MonoBehaviour
{
    void OnDrawGizmosSelected()
    {
        // Draw a yellow cube at the transform position
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireCube(transform.position, new Vector3(10, 10, 10));
    }
}
另一种Gizmos显示方式

通过DrawGizmo标签来实现,如下建立一个TestDrawGizmo.cs的脚本,放到Editor目录下。

public class TestDrawGizmo
{
    [DrawGizmo(GizmoType.NotInSelectionHierarchy | GizmoType.Pickable | GizmoType.InSelectionHierarchy)]
    static void RenderLightGizmo(Light light, GizmoType gizmoType)
    {
        Handles.Label(light.transform.position, light.transform.gameObject.name);
        Gizmos.DrawIcon(light.transform.position + Vector3.up, "Light.png");
        if ((gizmoType & GizmoType.InSelectionHierarchy) != 0)
        {
            if ((gizmoType & GizmoType.Active) != 0)
            {
                Gizmos.color = Color.red * 0.4f;
            }
            else
                Gizmos.color = Color.red * 0.5f;

            Gizmos.DrawSphere(light.transform.position, light.range);
        }
    }
}

未选中物体时,也可以在SceneView界面内显示Light组件物体的名字。
当我们选中Light组件对应的物体时,SceneView界面内结果如下:

下面脚本放到Editor目录后,Scene场景中物体处于未选中状态时可以显示物体名。

public class ShowObjName
{

    [DrawGizmo(GizmoType.NonSelected)]
    static void DrawGameObjectName(Transform transform, GizmoType gizmoType)
    {
        Handles.Label(transform.position, transform.gameObject.name);
    }
}

本示例中,通过GizmoType.NonSelected来控制只对未选中的物体生效。

使用Gizmos进行Debug

Gizmos 提供了Drawline,DrayRay,DrawSphere等方法,可以方便开发过程中的调试和验证。

Handles

在OnSceneGUI()方法中调用,用来丰富SceneView界面的显示。官方示例
也可以在SceneView.duringSceneGui回调中进行绘制。

扩展Game视图

运行模式下Game视图可以通过OnGUI()函数内绘制GUI,是一种很古老的方式了。在非运行模式下其实也可以绘制GUI,一般处理方式是在类(需要继承自MonoBehaviour)名前加上[ExecuteInEditMode],表示该脚本可以在非运行状态下执行各个生命周期。必要时辅助UNITY_EDITOR宏,用以发布时剥离相关代码。

#if UNITY_EDITOR
[ExecuteInEditMode]
public class Test : MonoBehaviour
{
    // 其中值得介绍的是OnEnable和OnDisable
    // 因为改了代码之后会将数据清零,而且不会执行Awake和Start
    // 但是会在编译前执行OnDisable,编译后会执行OnEnable
    // 可以用这一个时机,对一些委托的绑定与解绑,或者数据的初始化和销毁
    // 当然正常的SetActive也会触发这两个时机
    void OnEnable()
    {
    }

    void OnDisable()
    {
    }

     void OnGUI()
    {
        if(GUILayout.Button("Click"))
        {
            Debug.Log("Hello World!");
        }
    }
}
#endif

一些实用Attribute

  • [InitializeOnLoadMethod]
    在Class内部修饰静态方法,起到自动调用目标方法的功能。
  • [InitializeOnLoad]
    修饰Class,和类的静态构造函数结合使用,可以达到和[InitializeOnLoadMethod]一样的目的。
  • [DidReloadScripts]
    修饰静态方法;代码被编译好后,会被执行
	[DidReloadScripts]
	private static void Reload()
	{
	}
  • [Header(“”)]
    修饰继承自MonoBehavior的类中的字段,用来在Inspector面板中在该字段顶部展示说明文字

其它重要委托

  • 编辑器Update委托 EditorApplication.update+=

编辑器扩展涉及到的类

GUI,GUILayout;EditorGUI,EditorGUILayout

GUI和GUILayout

这二者既可以用于运行时在OnGUI()函数中显示UI,也可以用于一些扩展的编辑器面板(Inspector重写、扩展的Window等等)。

  • GUI
    一种需要通过自行计算Rect(用于设置坐标和组件的尺寸)来显示UI组件的类
  • GUILayout
    是基于GUI的实现,自动进行排版,计算坐标和宽高
EditorGUI和EditorGUILayout

这二者只能用于扩展的编辑器面板显示UI。

  • EditorGUI
    需要通过自行计算Rect来显示UI
  • EditorGUILayout
    基于EditorGUI的实现,会自动进行排版,计算坐标和宽高

Handles,Gizmos

上文中已涉及,不赘述

EditorUtility,EditorGUIUtility,AssetDatabase,Selection

Unity内置的一些工具类,涉及到序列化对象修改、报错;资源读取、保存等

  • EditorUtility: Unity内置的关于Editor使用的工具类,常用API有:
    1,资源置脏
EditorUtility.SetDirty(Object obj);//标记目标资源为“脏”
AssetDatabase.SaveAssets();//保存项目内的“脏数据”

2,显示对话框
EditorUtility.DisplayDialog
3,进度条
EditorUtility.DisplayProgressBar,EditorUtility.ClearProgressBar

  • EditorGUIUtility
    1,搜索框
    EditorGUIUtility.ShowObjectPicker
    2,选中提示
    EditorGUIUtility.PingObject

  • AssetDatabase: 在编辑器模式下,对项目资源的管理(创建资产、获得资产路径、资产加载、资产刷新、资产保存)

  • Selection: 含有当前选择的资源文件、文件夹或场景中选中的目标对象信息
    下面代码展示了获得选中文件夹下的所有Texture(需要先选中一个目标文件夹)

var texs = Selection.GetFiltered<Texture>(SelectionMode.DeepAssets); //进行深度遍历所有文件夹

下面代码展示:创建一个Cube并选中它

if (GUILayout.Button("创建一个 Cube,同时选中"))
{
    var obj = GameObject.CreatePrimitive(PrimitiveType.Cube);  
    Selection.activeGameObject = obj; 
}

常用编辑器UI绘制API

空行

  • EditorGUILayout.Separator() 一个比较大的空行
  • EditorGUILayout.Space() 一个比较小的空行
  • GUILayout.Space(10f) 可控具体空多少行
  • GUILayout.FlexibleSpace() 底层自动计算空白区

布局

水平,垂直布局
  • GUILayout.BeginHorizontal()和GUILayout.EndHorizontal() 二者一起可以让内部GUI水平排列。同理还有Vertical。
折叠区域
  • 折叠组 EditorGUILayout.BeginFoldoutHeaderGroup()和EditorGUILayout.EndFoldoutHeaderGroup()
  • 折叠 EditorGUILayout.Foldout()
开关控件
  • 单个开关 EditorGUILayout.Toggle,EditorGUILayout.ToggleLeft
  • 开关组 EditorGUILayout.BeginToggleGroup,EditorGUILayout.EndToggleGroup
滚动区域
  • EditorGUILayout.BeginScrollView()和EditorGUILayout.EndScrollView()

类型字段

EditorGUILayout.LabelField() 标签字段
EditorGUILayout.IntField() 整数字段
EditorGUILayout.FloatField() 浮点数字段
EditorGUILayout.TextField() 文本字段
EditorGUILayout.Vector2Field() 二维向量字段
EditorGUILayout.Vector3Field() 三维向量字段
EditorGUILayout.Vector4Field() 四维向量字段
EditorGUILayout.ColorField() 颜色字段
EditorGUILayout.CurveField() 曲线字段(AnimationCurve)

滑动条、进度条

EditorGUILayout.Slider(),EditorGUILayout.IntSlider()
EditorGUILayout.MinMaxSlider() 双滑块滑动条
EditorGUI.ProgressBar()

提示框

EditorGUILayout.HelpBox()

枚举选择

  • EditorGUILayout.EnumPopup 单选枚举
  • EditorGUILayout.EnumFlagsField 多选枚举
  • EditorGUILayout.IntPopup和EditorGUILayout.MaskField
    EditorGUILayout.IntPopup 单选整型
    EditorGUILayout.MaskField 多选整形

一些注意项

  • EditorGUILayout.DropdownButton 并不是下拉按钮,而是鼠标按下就会触发的按钮。而Button是鼠标放开才会触发。

继承关系

ScriptableWizard:EditorWindow:ScriptableObject:Object
Editor:ScriptableObject:Object

  • 15
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iningwei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值