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. 自定义编辑器窗口
  • 9. 具体应用
    1. 0.1. 通过反射来拷贝自定义组件的字段
    2. 0.2. 编辑器拓展ScriptableObject的显示
    3. 0.3. 拓展Spine插件中的Timeline轨道创建SkeletonAnimat Clip时自动引用SkeletonDataAsset
    4. 0.4. 拓展Spine插件中SkeletonAnimation组件的AnimationName属性的popup下拉框显示方式

  •  

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

    9. 具体应用

    通过反射来拷贝自定义组件的字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [ContextMenu("Copy By Reflection")]
    public void CopyByReflection()
    {
    copyFrom = this;

    Type getType = typeof(AType);
    FieldInfo[] getFields = getType.GetFields(BindingFlags.Public | BindingFlags.Instance);
    }

    [ContextMenu("Paste By Reflection")]
    public void PasteByReflection()
    {
    foreach (FieldInfo field in fieldsCoped)
    {
    object value = field.GetValue(copyFrom);
    field.SetValue(this, value);
    }
    }

    private static AType copyFrom;
    private static FieldInfo[] fieldsCoped;

    编辑器拓展ScriptableObject的显示

    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
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    using System.Collections;
    using System.Collections.Generic;
    using System;
    using UnityEditor;
    using UnityEngine;

    class ExpandableAttribute : PropertyAttribute
    {
    public ExpandableAttribute()
    {

    }
    }
    [CustomPropertyDrawer(typeof(ExpandableAttribute), true)]
    public class ExpandableAttributeDrawer : PropertyDrawer
    {
    #region Style Setup
    private enum BackgroundStyles
    {
    None,
    HelpBox,
    Darken,
    Lighten
    }

    private static bool SHOW_SCRIPT_FIELD = false;
    private static float INNER_SPACING = 6.0f;
    private static float OUTER_SPACING = 4.0f;
    private static BackgroundStyles BACKGROUND_STYLE = BackgroundStyles.HelpBox;
    private static Color DARKEN_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.2f);
    private static Color LIGHTEN_COLOR = new Color(1.0f, 1.0f, 1.0f, 0.2f);
    #endregion

    private Editor editor = null;
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
    float totalHeight = 0.0f;

    totalHeight += EditorGUIUtility.singleLineHeight;

    if (property.objectReferenceValue == null)
    return totalHeight;
    if (!property.isExpanded)
    return totalHeight;
    if (editor == null)
    Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);
    if (editor == null)
    return totalHeight;

    SerializedProperty field = editor.serializedObject.GetIterator();

    field.NextVisible(true);

    if (SHOW_SCRIPT_FIELD)
    {
    totalHeight += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    }

    while (field.NextVisible(false))
    {
    totalHeight += EditorGUI.GetPropertyHeight(field, true) + EditorGUIUtility.standardVerticalSpacing;
    }

    totalHeight += INNER_SPACING * 2;
    totalHeight += OUTER_SPACING * 2;

    return totalHeight;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
    Rect fieldRect = new Rect(position);
    fieldRect.height = EditorGUIUtility.singleLineHeight;

    EditorGUI.PropertyField(fieldRect, property, label, true);

    if (property.objectReferenceValue == null)
    return;

    property.isExpanded = EditorGUI.Foldout(fieldRect, property.isExpanded, GUIContent.none, true);

    if (!property.isExpanded)
    return;

    if (editor == null)
    Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);

    if (editor == null)
    return;

    #region Format Field Reccts
    List<Rect> propertyRects = new List<Rect>();
    Rect marchingRect = new Rect(fieldRect);

    Rect bodyRect = new Rect(fieldRect);
    bodyRect.xMin += EditorGUI.indentLevel * 14;
    bodyRect.yMin += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing + OUTER_SPACING;

    SerializedProperty field = editor.serializedObject.GetIterator();
    field.NextVisible(true);

    marchingRect.y += INNER_SPACING + OUTER_SPACING;

    if (SHOW_SCRIPT_FIELD)
    {
    propertyRects.Add(marchingRect);
    marchingRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    }

    while (field.NextVisible(false))
    {
    marchingRect.y += marchingRect.height + EditorGUIUtility.standardVerticalSpacing;
    marchingRect.height = EditorGUI.GetPropertyHeight(field, true);
    propertyRects.Add(marchingRect);
    }

    marchingRect.y += INNER_SPACING;

    bodyRect.yMax = marchingRect.yMax;
    #endregion

    DrawBackground(bodyRect);

    #region Draw Fields
    EditorGUI.indentLevel++;

    int index = 0;
    field = editor.serializedObject.GetIterator();
    field.NextVisible(true);

    if (SHOW_SCRIPT_FIELD)
    {
    // Show the disabled script field
    EditorGUI.BeginDisabledGroup(true);
    EditorGUI.PropertyField(propertyRects[index], field, true);
    EditorGUI.EndDisabledGroup();
    index++;
    }

    while (field.NextVisible(false))
    {
    try
    {
    EditorGUI.PropertyField(propertyRects[index], field, true);
    }
    catch (StackOverflowException)
    {
    field.objectReferenceValue = null;
    Debug.LogError("Detected self-nesting causing a StackOverflowException, avoid using the same object inside a nested structure.");
    }
    index++;
    }

    EditorGUI.indentLevel--;
    #endregion
    }

    private void DrawBackground(Rect rect)
    {
    switch (BACKGROUND_STYLE)
    {
    case BackgroundStyles.HelpBox:
    EditorGUI.HelpBox(rect, "", MessageType.None);
    break;
    case BackgroundStyles.Darken:
    EditorGUI.DrawRect(rect, DARKEN_COLOR);
    break;
    case BackgroundStyles.Lighten:
    EditorGUI.DrawRect(rect, LIGHTEN_COLOR);
    break;
    }
    }
    }

    拓展Spine插件中的Timeline轨道创建SkeletonAnimat Clip时自动引用SkeletonDataAsset

    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
    /// In SpineAnimationStateTrack.cs
    foreach (var clip in GetClips())
    {
    foreach (var bind in outputs)
    {
    if ((clip.asset as SpineAnimationStateClip).template.skeletonDataAsset == null)
    {
    /// 1.Auto Bind Refrence
    (clip.asset as SpineAnimationStateClip).template.skeletonDataAsset = GameObject.Find(go.GetComponent<PlayableDirector>().GetGenericBinding(bind.sourceObject).name).GetComponent<SkeletonAnimation>().skeletonDataAsset;

    /// 2.Set Default animationName
    var _Animations = (clip.asset as SpineAnimationStateClip).template.skeletonDataAsset.GetSkeletonData(true).Animations;
    // Find "xxx" animation
    foreach (Animation anim in _Animations)
    {
    if (anim.Name.Contains("xxx") && !anim.Name.Contains("xxxx"))
    {
    (clip.asset as SpineAnimationStateClip).template.animationName = anim.Name;
    break;
    }
    }
    // If no "xxx" and no "xxxxx", Default...foreach..break;

    /// 3.Set loop
    (clip.asset as SpineAnimationStateClip).template.loop = true;
    }
    }
    }

    拓展Spine插件中SkeletonAnimation组件的AnimationName属性的popup下拉框显示方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /// in SpineAttributeDrawers.cs  void PopulateMenu(GenericMenu menu, SerializedProperty property, SpineAnimation targetAttribute, SkeletonData data)
    if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
    #region Add Func
    {
    if (i > 49 && i % 50 == 0)
    {
    menu.AddSeparator("Page" + (i / 50 + 1).ToString() + "/");
    menu.AddItem(new GUIContent("Page" + (i / 50 + 1).ToString() + "/" + name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
    }
    else if (i > 50)
    {
    menu.AddItem(new GUIContent("Page" + (i / 50 + 1).ToString() + "/" + name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
    }
    else
    {
    menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
    }
    }
    #endregion