作者最新更新

游戏开发工具

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] 不被序列化(不被序列化且不在面板显示)