作者最新更新

Unity3D热门教程

    游戏开发工具

    UnityEditor之改变面板的常用方法

    738

    73 次收藏2024-08-21 22:52:34发布

    Attributes属性

    属于U3D的RunTimeClass,所以要加上命名空间: 

    using UnityEngine; 
    using System.Collections;

    在Unity引擎中,Attributes(属性)用于修饰类、字段、方法等,以提供额外的元数据或行为。例如,[SerializeField]属性用于在Unity编辑器中显示私有字段。我们可以模仿这种机制,在C#中实现自定义属性,并在运行时或编译时应用这些属性。

    下面是一个商业化级别的具体代码实现,展示如何创建和使用自定义属性。


    一、面板内建特性

    首先是喜闻乐见的特性(Attribute),Unity内建了许多方便的特性来改变和扩展编辑器。

    此外关于更多Unity内建的属性,可以在官方API文档中查看。 但文档并没有单独整理用于 Inspector面板 的特性,所以我在这做了一些简单的展开。

    总所周知,Unity 提供了 “PropertyAttribute” 类,使我们可以方便的定义一个控制变量在Inspector中样式的特性。 于是,我们可以通过一个简单的反射,来获取Unity内建的这些属性:

    using System.Reflection;
    
    [UnityEditor.MenuItem("Test/打印PropertyAttribute派生类")]
    static public void GetPropertyAttributeDerivedClass()
    {
        foreach (var item in Assembly.Load("UnityEngine.CoreModule").GetTypes())
            if (item.IsSubclassOf(typeof(PropertyAttribute)))
                Debug.Log(item);
    }

    使用上面方法,我们得到了12个特性,没错只有12个。是不是总觉得少了很多。 

    所以我又打印了一下从 “Attribute” 派生的所有类,这回一共有90个。

    于是简单的整理了下面表格(仅与Inspector面板有关): 

    特性说明
    “PropertyAttribute”派生的所有特性 | 改变一个字段的样式
    [HideInInspector]隐藏一个字段
    [HelpURL]、[ContextMenu]作用在组件头部的两个特性

    [RequireComponent]

    [AddComponentMenu]

    功能性的两个特性

    [SerializeField]

    [SerializeReference]

    [FormerlySerializedAsAttribute]

    序列化相关
    [Serializable]、[NonSerialized]序列化相关,但属于 "System" 命名空间下


    从上可以看出,除了 “PropertyAttribute” 派生的所有特性 和 [HideInInspector] 外,其他特性主要是功能性的。

    至于 [HideInInspector] 为什么不用继承 “PropertyAttribute” 类来实现。 

    目前我所能理解的,由于 “PropertyDrawer” 的实现方式,面板在绘制数组或List时,其根部并不在 “PropertyDrawer” 的处理范围内,所以无法全部隐藏,于是单独定义了一个 [HideInInspector] 特性。

    二、自定义特性

    扯完内建特性,接下来当然是自定义特性。 本着“要致富,先撸树”的原则。我们自然需要先定义一个继承自 “PropertyAttribute” 的类。

    using UnityEngine;
    
    [System.AttributeUsage(AttributeTargets.Field)]    //限制属性只能使用在Field上
    public class CustomAttribute : PropertyAttribute {}


    //定义特性时建议在命名后加上“Attribute”以方便区分,使用时则可以省略

    属性定义完,接着就是画UI了。 绘制UI的方式有则有两种, 一是继承 “DecoratorDrawer” 类,这可以在原有的基础上添加一些内容,但不提供修改变量的方法,如 [Space] [Header]。 二是继承 “PropertyDrawer” 类,这就需要自己全部重头绘制了,但可以通过方法中的 “property” 参数来修改变量。

    using UnityEngine;
    using UnityEditor;
    
    /*************** DecoratorDrawer ***************/
    [CustomPropertyDrawer(typeof(CustomAttribute))]    //关联特性
    public class CustomAttributeDrawer : DecoratorDrawer
    {
        public override void OnGUI(Rect position)
        {
            EditorGUI.DrawRect(position, Color.green);
            GUI.Label(position, "DecoratorDrawer");
        }
    }
    
    /*************** PropertyDrawer ***************/
    [CustomPropertyDrawer(typeof(CustomAttribute))]    //关联特性
    public class CustomAttributeDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            EditorGUI.DrawRect(position, Color.green);
            GUI.Label(position, "PropertyDrawer");
        }
        //PropertyDrawer 除了用重载 OnGUI 的方式来绘制基于 IMGUI 的 GUI 外,
        //还可以选择用重载 CreatePropertyGUI 的方式来绘制基于 UIElements 的 GUI。
    }


    之后定义个变量测试一下。

    1.jpg

    三、可序列化类型的自定义绘制

    上面使用了 “DecoratorDrawer” 和 “PropertyDrawer” 两个类,来关联特性。 实际上我们也可以直接用来关联可序列化的类型。 只需要把 [CustomPropertyDrawer(typeof(...))] 中的特性类换掉就可以了。 [CustomPropertyDrawer(typeof(可序列化类型))]

    像上面示例中,变量 test 是 int 类型的。 所以,将 “typeof(CustomAttribute)” 换成 “typeof(int)” 也能做到同样的效果。 不同的是,之后所有 int 类型的变量,默认样式都会改变。

    当然,用在自定义的可序列化类型上也是可以的。

    //==================== Test.cs
    public class Test : MonoBehaviour
    {
        //定义个类,记得加上可序列化特性
        [System.Serializable]
        public class MyClass { ... }
    
        //之后定义变量就不用加特性啦
        public MyClass c;
    }
    
    //==================== MyClassDrawer.cs
    [CustomPropertyDrawer(typeof(Test.MyClass))]    //关联可序列化类型
    public class MyClassDrawer : DecoratorDrawer    //或者继承PropertyDrawer
    {
        ...
    }


    四、组件的自定义绘制

    编写组件的时候,免不了整了一些有关联的变量,或是数组套数组,看着特闹心, 又或是想加个功能按钮之类的,上面的方法就不好用了, 所以这时候,一不做二不休,直接继承 Editor,重画整个组件吧。

    using UnityEngine;
    using UnityEditor;
    
    [CustomEditor(typeof(Test))]    //关联类型
    public class TestEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            Rect position = GUILayoutUtility.GetRect(0, 20, GUILayout.ExpandWidth(true));
            EditorGUI.DrawRect(position, Color.green);
            GUI.Label(position, "TestEditor");
        }
        //也可以选择用重载 CreateInspectorGUI 的方式来绘制基于 UIElements 的 GUI。
    }

    2.jpg

    当然,改变Unity自带组件的样式也是可以的。 甚至,GameObject 本身也算个组件。所以,需要的话改变 GameObject 的显示也不是问题。

    2-1.jpg

    五、控制对象销毁的hideFlags

    hideFlags 主要用于控制对象销毁,保存以及在检查器中的可见性,一般不会动他。 但我们的游戏物体和组件一般默认是 None 的状态, 而 HideInHierarchy、HideInInspector、NotEditable 也就改变了对象在 Hierarchy 和 Inspector 中的显示, 所以有需要的大可放心用。需要注意的是剩下带 “Dont” 的状态,都需要手动销毁。

    using UnityEngine;
    
    public class Test : MonoBehaviour
    {
        private void Start()
        {
            gameObject.hideFlags = HideFlags.NotEditable;    //使gameObject不可编辑
        }
    }

    4.jpg

    六、AddComponentMenu添加组件菜单

    AddComponentMenu 属性允许将一个脚本添加到 Component 菜单中,然后你便可以通过 Component ->(你设置的名字)为一个选中的游戏对象创建该脚本,如下所示:

    [AddComponentMenu("Learning/People")]
    public class People : MonoBehaviour 
    {
     
    }

    1.gif

    七、RequireComponent() 添加组件

    RequireComponent()属性会自动帮你添加你需要的组件,如果已经存在则不再重复添加,且不能移除,如下所示:

    [AddComponentMenu("Learning/People")]
    [RequireComponent(typeof(Rigidbody))]
    public class People : MonoBehaviour 
    {
    
    }

    2.gif

    提示:经过测试,我发现一个问题,如果脚本已经挂在物体身上,然后再修改脚本,为添加 RequireComponent 属性的话,完全不起作用,因此建议大家在用此属性的时候要注意。

    八、ContextMenu()和ContextMenuItem() 菜单定义

    ContextMenu()属性允许添加一个命令到该组件上,你可以通过右键或者点击设置图标来调用到它(一般用于函数),且是在非运行状态下执行该函数; 

    ContextMenuItem()属性允许添加一个命令到该变量上,可通过右击变量来调用对应方法 

    如下所示:

    [AddComponentMenu("Learning/People")]
    [RequireComponent(typeof(Rigidbody))]
    public class People : MonoBehaviour 
    {
        public string name;
        public int age;
        [ContextMenu("OutputInfo")]
        void OutputInfo()
        {
            print(name+"|"+age);
        }
    }


    3.gif

    public class People : MonoBehaviour 
    {
        [ContextMenuItem("右击时显示名", "OutputInfo")]
        public int age;
        public string name="Hia";
        void OutputInfo()
        {
            print(name+"|"+age);
        }
    }

    1.jpg

    九、HelpURL() 指定文档链接

    HelpURL()提供一个自定义的文档链接,点击组件上的文档图标既能打开到你指定的链接,如下所示:

    [HelpURL("http://www.baidu.com")]
    public class People : MonoBehaviour 
    {
        public string name;
        public int age;
    }


    4.gif

    提示:填写链接时,一定要写上 http:// 或者 https://,否则将无任何反应。

    十、InitializeOnLoad 启动事件监听

    在Unity开发过程中,我们经常需要在编辑器启动时或脚本重新编译后执行一些操作,例如初始化数据、注册事件等。这时,我们可以使用InitializeOnLoad特性来实现这一需求。本文将详细介绍InitializeOnLoad特性的用法,并通过三个实际案例来展示其应用场景。

    InitializeOnLoad是Unity引擎提供的一种特性,用于在编辑器启动时或脚本重新编译后自动执行指定的操作。这种特性非常适合用于在编辑器启动时执行一些初始化操作,以确保项目在启动后能够正常运行。

    要使用InitializeOnLoad特性,只需在编辑器脚本中为一个静态类添加该特性即可。例如:

    using UnityEditor;
    using UnityEngine;
    [InitializeOnLoad]
    public static class MyInitializer
    {
    	static MyInitializer()
    	{
    		Debug.Log("InitializeOnLoad called.");
    	}
    }

    在这个例子中,我们创建了一个名为MyInitializer的静态类,并为其添加了InitializeOnLoad特性。当编辑器启动时,MyInitializer类的静态构造函数将被自动调用,从而实现自动初始化的功能。

    十一、Range()、Multiline()、header()

    Range()属性用于将一个值指定在一定的范围内,并在Inspector面板中为其添加滑块; 

    Multiline() 属性用于给 string 类型添加多行输入; 

    header() 属性用于添加属性的标题,具体操作如下所示:

    public class People : MonoBehaviour 
    {
        [Header("BaseInfo")]
        [Multiline(5)]
        public string name;
     
        [Range(-2,2)]
        public int age;
    }


    2-1.jpg

    简单的分解一下:

    1、第9行,我们使用了 [Header(“BaseInfo”)] 为其设置了标题(为“BaseInfo”),如上图所示。 

    2、第10行,我们使用了 [Multiline(5)] 为其 name 属性添加了5行输入,如上图所示,明显输入框变大了。 

    3、第12行,我们使用了 [Range(-2,2)] 为其 age 属性指定了一个(-2,2)的范围,并且为其添加了一个滑块,如上图所示。

    十二、Tooltip()提示、Space() 距离设定

    Tooptip()属性用于在 Inspector 面板中,当鼠标停留在设置了Tooptip()的属性添加指定的提示; 

    Space()用于为在 Inspector 面板两属性之间添加指定的距离,如下所示:

    public class People : MonoBehaviour 
    {
     
        [Header("BaseInfo")]
        [Multiline(5)]
        public string name;
     
        [Range(-2, 2)]
        public int age;
     
        [Space(100)]
        [Tooltip("用于设置性别!")]
        public string sex;
        
    }


    1.jpg

    十三、Serializable、SerializeField 序列化

    1、Serializable这个属性可以让子类(继承类)的变量属性显示在检视面板中,也能序列化它。(JS的话完全不需要这个属性。)

    [System.Serializable]
    public class Boy
    {
        public int stength;
    }
    public class People : MonoBehaviour 
    {
     
        public Boy bb;
    }


    2.jpg

    2、SerializeField 序列化域(强制序列化):可以将私有变量序列化,将U3D的内建变量序列化等。

    [SerializeField, Range(-2, 2)]
    private int age;

    当然公有变量也可以在面板隐藏: 

    [HideInInspector] 面板隐藏 

    [NonSerialized] 不被序列化(不被序列化且不在面板显示)