Unity编辑器扩展常用方法与特性

  1. 1.  
  • 0. 准备工作
  • 1. MenuItem
    1. 0.1. 1.1 添加菜单栏按钮
    2. 0.2. 1.2 添加菜单栏按钮快捷键
    3. 0.3. 1.3 给特定组件添加右键菜单栏按钮
    4. 0.4. 1.4 获取当前操作的组件
  • 2. Selection.objects
  • 3. ContextMenu
    1. 0.1. 3.1 给某组件添加右边小齿轮菜单选项
    2. 0.2. 3.2 给某属性添加右键菜单选项
  • 4. Gizmos辅助调试工具
    1. 0.1. 4.1 绘制
  • 5. 特性
    1. 0.1. 5.1 System命名空间下的特性
    2. 0.2. 5.2 UnityEngine命名空间下的特性
    3. 0.3. 5.3 UnityEditor命名空间下的特性
  • 6. 自定义Inspector面板
  • 7. Inspector面板上数组或List集合的显示方式
  • 8. 自定义编辑器窗口

  •  

    0. 准备工作

    • Editor文件夹(可以多个):主要用来存放编辑器下使用的一些脚本和资源,一般用来扩展Unity编辑器使用,不会发布到应用程序中,也不会在运行时运行
    • Editor Default Resources文件夹(根目录唯一):用来存储编辑器下的一些默认资源,这些资源只能通过EditorGUIUtility.Load函数按需进行加载
    • Editor文件夹中的子文件夹Resources:用来存储一些原型设计时可以从脚本中按需加载的资源,其中的资源需要通过Editor脚本进行加载,并会从构建发布中剥离
    • Gizmos文件夹:用来存储编辑器中的特殊对象图标,用来标记特殊对象或位置,Gizmos允许将图形添加到Scene视图中,以帮助可视化不可见的设计细节。同样也不会发布到运行时
    • Editor文件夹内针对编辑器组件的方法,需要设置为Static方法

    1. MenuItem

    • 需要引用UnityEditor命名空间

    1.1 添加菜单栏按钮

    [MenuItem(string path, bool? hide, int? priority)]

    • 第一个参数:菜单的路径
    • 第二个参数:是否为有效函数,是否需要显示
    • 第三个参数:优先级,用来表示菜单按钮的先后顺序,默认值为1000。数值相差大于10会分栏

    注意:需要是静态方法


    • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [MenuItem("MyTools/ShowMsg/msg1", false, 1000)]
    public static void ShowMsg1()
    {
    Debug.Log("msg1");
    }

    [MenuItem("MyTools/ShowMsg/msg2", false, 1011)]
    public static void ShowMsg2()
    {
    Debug.Log("msg2");
    }

    1.2 添加菜单栏按钮快捷键

         符号           字符     
          %	       Ctrl/Command  
          #            Shift     
          &             Alt      
    LEFT/RIGHT/DOWN    方向键     
        F1-F12	      F功能键 
          _g            字母g    
    
    • 示例:
    1
    [MenuItem("MyTools/ShowMsg/msg1 %_t", false, 1000)]

    1.3 给特定组件添加右键菜单栏按钮

    [MenuItem(string path)]

    • 参数:按钮的路径,以”CONTEXT/[ComponentName]/“开头

    • 示例:
    1
    2
    3
    4
    5
    [MenuItem("CONTEXT/Rigidbody/Init")]
    private static void RigidbodyInit()
    {
    Debug.Log("rgdInit");
    }

    1.4 获取当前操作的组件

    MenuCommand

    • 示例:给自定义组件Test添加右键Init按钮
    1
    2
    3
    4
    5
    6
    [MenuItem("CONTEXT/Test/Init")]
    private static void Init(MenuCommand cmd)
    {
    Test test = cmd.contex as Test;
    test.ShowMsg();
    }

    2. Selection.objects

    • 需要引用UnityEditor命名空间
    • 返回场景或者Project中选择的多个对象
    • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    [MenuItem("MyTools/DeleteAllObj &d", false)]
    private static void MyToolDelete()
    {
    foreach (Object item in Selection.objects)
    {
    // 记录删除操作,允许撤销
    Undo.DestroyObjectImmediate(item);
    }
    }

    3. ContextMenu

    • 无需引用UnityEditor命名空间

    3.1 给某组件添加右边小齿轮菜单选项

    [ContextMenu(string buttonName)]

    • 示例:
    1
    2
    3
    4
    5
    [ContextMenu("TestFunc")]
    public void TestFunction()
    {
    Debug.Log("Test");
    }

    3.2 给某属性添加右键菜单选项

    [ContextMenuItem(string buttonName, string functionName)]

    • 示例:
    1
    2
    3
    4
    5
    6
    [ContextMenuItem("ChangeNum", "ChangeNumFunc")]
    public int testNum;
    private void ChangeNumFunc()
    {
    testNum = 2;
    }

    4. Gizmos辅助调试工具

    4.1 绘制

    • Gizmos是Scene窗口的可视化调试或辅助工具

    • 可以通过两种方式实现

      • 通过OnDrawGizmos或者OnDrawGizmosSelected方法,无需引用UnityEditor命名空间
      • 通过DrawGizmos特性,需要引用UnityEditor命名空间
    • OnDrawGizmos方法:绘制效果一直显示

    • OnDrawGizmosSelected方法:绘制效果在选中对象时显示

    • DrawGizmo特性:该方法需要将该类放在Editor文件夹内,使用特性的方法可以将业务逻辑和调试脚本分开

    • GizmosType:

         GizmosType                   描述                
           Active               如果激活,则绘制            
       SelectedOrChild    如果被选择或者选择子物体时,则绘制  
         NotSelected          如果全没选择,则绘制          
          Selected              如果选择,则绘制            
          Pickable           在编辑器中Gizmo可以被点选      
      
    • 常用Gizmos的方法:

      • Gizmos.DrawCube() : 绘制实体立方体
      • Gizmos.DrawWireCube() : 绘制立方体边框
      • Gizmos.DrawRay() : 绘制射线
      • Gizmos.DrawLine() : 绘制直线
      • Gizmos.DrawIcon() : 绘制Icon,Icon素材需要放在Gizmos文件夹中,代码中需要加图片后缀名
      • Gizmos.DrawFrustum() : 绘制摄像机视椎体的视野范围
    • 方式一示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private void OnDrawGizmos()
    {
    var color = Gizmos.color;
    Gizmos.color = Color.red;
    // public static void DrawCube(Vector3 center, Vector3 size)
    Gizmos.DrawCube(transform.position, Vector3.one);
    Gizmos.color = color;
    }

    private void OnDrawGizmosSelected()
    {
    var color = Gizmos.color;
    Gizmos.color = Color.red;
    Gizmos.DrawWireCube(transform.position, Vector3.one);
    Gizmos.color = color;
    }
    • 方式二示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 表示物体显示并且被选择状态,绘制Gizmos
    [DrawGizmo(GizmoType.Active | GizmoType.Selected)]
    // 第一个参数需指定目标类,目标类需要挂载在场景对象上,target为挂载对应组件的对象
    private static void MyCustomOnDrawGizmos(TestTarget target, GizmoType gizmoType)
    {
    var color = Gizmos.color;
    Gizmos.color = Color.red;
    Gizmos.DrawCube(target.transform.position, Vector3.one);
    Gizmos.color = color;
    }
    • 一直显示主摄像机视野范围示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 业务逻辑代码
    private Camera mainCamera;

    private void OnDrawGizmos()
    {
    if (mainCamera == null)
    {
    mainCamera = Camera.main;
    }
    Gizmos.color = Color.green;
    // 设置Gizmos的矩阵
    Gizmos.matrix = Matrix4x4.TRS(mainCamera.transform.position, mainCamera.transform.rotation, Vector3.one);
    Gizmos.DrawFrustum(Vector3.zero, mainCamera.fieldOfView, mainCamera.farClipPlane, mainCamera.nearClipPlane, mainCamera.aspect);
    }

    5. 特性

    5.1 System命名空间下的特性

    • Serializable : 序列化一个类,作为一个子属性显示在监视面板
    • NonSerialized : 反序列化一个变量,在监视面板上隐藏

    5.2 UnityEngine命名空间下的特性

    • AddComponentMenu : 可以添加一个组件菜单项到编辑器里

      • 效果:
    • AssemblyIsEditorAssembly : 汇编级别的属性。带了这个属性的类就被认为是编辑器类。只能对于程序集有效

    • ColorUsage : 可以修改Color的配置,是否显示Alpha通道,或者使用HDR模式

      • 参数1:是否显示透明度(Alpha)
      • 参数2:是否用HDR模式,若为true,需要下面四个参数
      • 3、4:最小、最大亮度
      • 5、6:最小、最大曝光
      • 示例:[ColorUsageAttribute(true, true)] public Color targetColor;
    • ContextMenu : 给脚本的右键菜单添加一个自定义方法,不能是静态的

    • ContextMenuItem : 给字段的右键菜单添加一个自定义方法,不能是静态的

    • CreateAssetMenu : 用于Scriptable的子类,使其可以在Asset菜单项中创建

      • 参数1:fileName 新创建的此类实例使用的默认文件名(创建文件必须以 .asset 结尾)
      • 参数2:menuName 此类型显示的名称显示在”Asset/Create”菜单中
      • 参数3:order 菜单项在”Asset/Create”菜单中的位置
      • 示例:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        [CreateAssetMenu(fileName = "New Material Item", menuName = "Inventory/New Material Item")]
        public class ItemMaterial : Item
        {

        }
        public class Item : ScriptableObject
        {

        }
    • Delayed : 用于float、int或string变量,只有按了回车或焦点离开字段才会返回新值

    • DisallowMultipleComponent : 用于MonoBehaviour或其子类,不能重复添加这个类的组件,重复添加会弹出对话框

    • ExecuteInEditMode : 带了这个特性的实例会直接在编辑模式下执行,但不是像进入游戏模式那样时刻执行

      • (1)Update在这个场景中任意物体变化了执行
      • (2)OnGUI在Game View接收到一个Event时执行
      • (3)OnRenderObject和其他渲染回调函数在Scene View或Game View重新渲染时执行
    • GUITarget : 选择哪些显示器调用OnGUI函数

      • 示例:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        using UnityEngine;
        public class ExampleClass1 : MonoBehaviour
        {
        // Label will appear on display 0 and 1 only
        [GUITarget(0, 1)]
        void OnGUI()
        {
        GUI.Label(new Rect(10, 10, 300, 100), "Visible on TV and Wii U GamePad only");
        }
        }
    • Header : 标题特性,给监视面板上的属性加一个小标题

    • HelpURL : 给类提供一个自定义文档URL,可以点击组件右上角小书或代码中按Ctrl+鼠标左键跳转到目标

    • HideInInspector : 在监视面板里隐藏变量,不改变序列化属性

    • ImageEffectAllowedInSceneView : 使用了这个特性的图像特效可以渲染在SceneView的摄像机上

    • ImageEffectOpaque : 可以在不透明通道直接执行图像特效

    • ImageEffectTransformsToLDR : 在HDR渲染模式下,使用图像特效用LDR渲染模式

      • ImageEffet在Unity Pro上才有
    • Multiline : 可以让string变量在监视面板上多行显示

    • PreferBinarySerialization : 只能用于ScriptableObject子类,用二进制序列化,有利于处理大量数据的资源文件,提升读写性能。主要缺点是二进制的文件我们看不懂,并且不能用版本控制软件合并它

    • Property : 监视面板里面修改样式的抽象基类,例如显示小标题、显示多行编辑文本等等都是以它为基类

    • Range : 在监视面板里限制int或float类型的变量值

    • RequireComponent : 自动添加需要的组件。若已存在则不额外添加。这使得脚本可以安全的使用该组件

    • RPC : 用于Networking,但废弃了

    • RuntimeInitializeOnLoadMethod : 不用作为组件添加到对象也可以直接自动调用初始化方法。要求方法为静态,类、方法可以为私有。当游戏开始就会调用,但有多个这种特性的方法调用时,执行顺序是不能确定的

      • 场景加载前调用:[RuntimeInitializeOnLoadMethodAttribute(RuntimeInitializeLoadType.BeforeSceneLoad)]
      • 场景加载后调用:[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    • SelectionBase : 带这个特性的GameObject,如果点击本身就一定选中本身,即便父对象也有这特性;如果子对象没有带这个特性,则当在场景点击子对象时,选中的是带特性的父对象;如果父对象和父父对象都有这特性,选父对象

    • SerializeField : 序列化字段,主要用于序列化私有字段

    • SharedBetweenAnimators : 用于StateMachineBehaviour,类似Prefab,Animator之间共用这个实例,减少内存消耗

    • Space : 在监视面板上加空行

    • TextArea : 让string在监视面板上显示成带滚动条的文本域

      • 第1参数:默认显示几行,默认值为3
      • 第2参数:最多显示几行,默认值为3
      • [TextArea]默认显示3行,最多显示3行,超出自动显示滚动条
      • [TextArea(2,5)]默认显示2行,最多显示5行,当大于5行,会自动显示滚动条
    • Tooltip : 给监视面板的字段添加小贴士,即鼠标指向字段显示的提示

    • UnityAPICompatibilityVersion : 用来声明程序集API版本,避免处理时是否可以用旧版本的Unity API

    5.3 UnityEditor命名空间下的特性

    • CallbackOrder : 所有带order(顺序)回调属性的特性基类
    • CanEditMultipleObjects : 使自定义编辑器支持同时编辑多个对象,一般配合CustomEditor使用类
      • 示例:

    • CustomPreview : 添加自定义类型的preview在监视面板
    • CustomPropertyDrawer : 自定义属性渲染,如果要自定义PropertyDrawer或DecoratorDrawer,要加上该特性
    • DrawGizmo : 自定义Gizmo渲染方法,用法见4.1方法二
    • InitializeOnLoad : 当Unity工程装载时,会自动调用一个类来初始化,这个类必须有静态构造函数
    • InitializeOnLoadMethod : InitializeOnLoad的静态方法
    • MenuItem : 添加菜单项,必须是静态方法。第二参数若为true,则会先判断该方法是否返回true,是则可以使用,否则按钮是不可用(灰色)的
    • PreferenceItem : 给Preference窗口添加菜单项,调用的也是静态方法
      • 官方示例:

    6. 自定义Inspector面板

    • 新建一个脚本(一般命名为需要被扩展的脚本名+Editor,例如Test脚本的Inspector扩展类命名为TestEditor),脚本需要继承自Editor,我们知道自定义的Window窗口需要在OnGUI中绘制,而自定义的Inspector面板需要在OnInspectorGUI中绘制
    • 案例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    using UnityEngine;

    public class DrawInspectorGUITest : MonoBehaviour
    {
    public float testValue1 = 10;
    public float testValue2 = 20;
    public float testValue3 = 30;
    public float testValue4 = 40;
    public int testValue5 = 50;
    public string testValue6 = "60";

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    using UnityEditor;
    using UnityEngine;

    [CustomEditor(typeof(DrawInspectorGUITest))]
    public class DrawInspectorGUITestEditor : Editor
    {
    private bool showFolder = false;
    public override void OnInspectorGUI()
    {
    DrawInspectorGUITest t = (DrawInspectorGUITest)target;

    EditorGUILayout.BeginVertical(); // 垂直布局

    EditorGUILayout.FloatField("value1", t.testValue1);

    t.testValue2 = EditorGUILayout.Slider("value2", t.testValue2, 0, 50);

    EditorGUILayout.Space();
    EditorGUILayout.Space();

    t.testValue3 = EditorGUILayout.Slider("value3", t.testValue3, 0, 100);
    if (t.testValue3 < 50)
    {
    GUI.color = Color.red;
    }
    else
    {
    GUI.color = Color.green;
    }
    Rect rect = GUILayoutUtility.GetRect(50, 60);
    EditorGUI.ProgressBar(rect, t.testValue3 / 100, "testBar");
    GUI.color = Color.white;

    EditorGUILayout.Space();
    EditorGUILayout.Space();

    t.testValue4 = EditorGUILayout.Slider("value4", t.testValue4, 0, 100);
    if (t.testValue4 < 20)
    {
    EditorGUILayout.HelpBox("<20!", MessageType.Error);
    }
    else if (t.testValue4 < 40)
    {
    EditorGUILayout.HelpBox("<40!", MessageType.Warning);
    }
    else if (t.testValue4 < 60)
    {
    EditorGUILayout.HelpBox("<60!", MessageType.Info);
    }
    else if (t.testValue4 < 80)
    {
    EditorGUILayout.HelpBox("<80!", MessageType.None);
    }

    EditorGUILayout.Space();
    EditorGUILayout.Space();

    showFolder = EditorGUILayout.Foldout(showFolder, "其他信息");
    if (showFolder)
    {
    EditorGUILayout.LabelField("Test Info");
    t.testValue5 = EditorGUILayout.IntField("value5", t.testValue5);
    t.testValue6 = EditorGUILayout.TextArea(t.testValue6, GUILayout.MinHeight(90));
    }


    EditorGUILayout.EndVertical();
    }

    }


    枚举示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public enum TaskType
    {
    除法竖式,
    乘法竖式,
    单行算式
    }
    public class NewTask : ScriptableObject
    {
    public TaskType taskType;
    }


    [CustomEditor(typeof(NewTask))]
    public class NewTaskEditor : Editor
    {
    public override void OnInspectorGUI()
    {
    NewTask t = (NewTask)target;
    t.taskType = (TaskType)EditorGUILayout.EnumPopup("题目类型", t.taskType);
    }
    }

    7. Inspector面板上数组或List集合的显示方式

    • 新版本Unity已经实现了显示数组或List,以及可排序列表的功能
    • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    using System.Collections.Generic;
    using UnityEngine;

    public class ListTest : MonoBehaviour
    {
    public string[] strs;
    public List<string> strsList;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    using UnityEditor;

    [CustomEditor(typeof(ListTest))]
    public class ListTestEditor : Editor
    {
    private SerializedProperty strs;
    private SerializedProperty strsList;

    private void OnEnable()
    {
    strs = serializedObject.FindProperty("strs");
    strsList = serializedObject.FindProperty("strsList");
    }

    public override void OnInspectorGUI()
    {
    serializedObject.Update();
    EditorGUILayout.PropertyField(strs, true);
    EditorGUILayout.PropertyField(strsList, true);
    serializedObject.ApplyModifiedProperties();

    //DrawDefaultInspector();
    }
    }
    • ReorderableList可以实现通过鼠标拖动,修改列表元素的排列顺序,其命名空间为UnityEditorInternal
    • 更多功能参考 CSDN

    8. 自定义编辑器窗口

    • 示例:


    窗口标题的另一种设置方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    using UnityEditor;
    using UnityEngine;
    using UnityEditor.SceneManagement;
    using System.IO;
    public enum TestType
    {
    Type1,
    Type2,
    }
    public class WindowsTest : EditorWindow
    {
    [MenuItem("My_Tools/创建模板场景", false, 5)]
    static void CreateTempleteScene_Menu()
    {
    WindowsTest createWindow = EditorWindow.GetWindow<WindowsTest>("Title test");
    createWindow.Show();
    }
    WindowsTest()
    {
    this.titleContent = new GUIContent("title test"); // 会被覆盖
    }

    private string className;
    private GameObject testObj;
    private TestType testType;
    private void OnGUI()
    {
    EditorGUILayout.Space(10);

    GUI.skin.label.fontSize = 40;
    GUI.skin.label.alignment = TextAnchor.MiddleCenter;
    GUILayout.Label("TitleTest");

    EditorGUILayout.Space(10);

    GUI.skin.label.fontSize = 16;
    GUI.skin.label.alignment = TextAnchor.MiddleLeft;
    GUILayout.Label("场景名:" + EditorSceneManager.GetActiveScene().name);
    GUILayout.Label("当前时间:" + System.DateTime.Now);

    EditorGUILayout.Space(10);

    testObj = (GameObject)EditorGUILayout.ObjectField("GameObj:", testObj, typeof(GameObject), true);

    EditorGUILayout.Space(10);

    className = EditorGUILayout.TextField("课程号", className);
    if (GUILayout.Button("创建新场景"))
    {
    switch (testType)
    {
    case TestType.Type1:

    break;
    case TestType.Type2:

    break;
    default:

    break;
    }
    }

    EditorGUILayout.Space();
    EditorGUILayout.Space();
    EditorGUILayout.Space();
    EditorGUILayout.Space();
    GUILayout.Label("testInfo", EditorStyles.boldLabel);
    }


    private void CopyFile(string oriPath, string tarPath)
    {
    if (File.Exists(Application.dataPath + oriPath))
    {
    if (Directory.Exists(Application.dataPath + tarPath))
    {
    try
    {
    // 不允许覆盖
    File.Copy(Application.dataPath + oriPath, Application.dataPath + tarPath);
    AssetDatabase.Refresh();
    }
    catch
    {
    //
    }
    }
    else
    {
    //
    return;
    }
    }
    else
    {
    //
    return;
    }
    }
    }