【Unity3D编辑器开发】Unity3D中初次尝试使用PropertyDrawer属性

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

推荐阅读

大家好我是佛系工程师☆恬静的小魔龙☆不定时更新Unity开发技巧觉得有用记得一键三连哦。

一、前言

前段时间一直比较忙没有时间更新博客最近闲下来了就给自己充充电学习一下新知识。

最近订上了Unity3D的编辑器开发感觉打开了新世界的大门特意将学习的知识进行梳理然后分享出来。

这次主要分享的内容是编辑器开发的PropertyDrawer属性。

下面就来了解一下PropertyDrawer属性吧。

二、正文

2-1、简介

PropertyDrawer用于自定义属性绘制器的基类。

使用此基类可以为自己的[System.Serializable]类的每个实例进行GUI也就是重新绘制。

比如说自定义类有[System.Serializable]属性那么就可以使用PropertyDrawer来控制它在Inspector中的样式。

2-2、举个例子

Demo代码

using System;
using UnityEngine;


public enum IngredientUnit { Spoon,Cup,Bowl,Piece}

[Serializable]
public class Ingredient
{
    public string name;
    public int amount = 1;
    public IngredientUnit unit;
}

public class Recipe : MonoBehaviour
{
    public Ingredient potionResult;
    public Ingredient[] pointIngredients;
}

接着可以 使用自定义PropertyDrawer来更改Inspector中Ingredient类的每个实例的样式。

可以使用CustomPropertyDrawer 特性将 PropertyDrawer附加到 Serializable类然后传入绘制器进行渲染。

可以使用 UIElements构建自定义 PropertyDrawer也可以使用 IMGUI

若要使用 UIElements创建自定义 PropertyDrawer必须对 PropertyDrawer类重写 PropertyDrawer.CreatePropertyGUI

若要使用 IMGUI创建自定义 PropertyDrawer必须对 PropertyDrawer类重写 PropertyDrawer.OnGUI

如果在基于 UIElements的检查器或 EditorWindow内使用 PropertyDrawer则在任何 IMGUI实现上使用回退覆盖 PropertyDrawer.CreatePropertyGUI 时将使用 UIElements实现。

如果在基于 IMGUI的检查器或 EditorWindow内使用 PropertyDrawer则将仅显示 IMGUI实现。UIElements无法在 IMGUI内运行。

以下是使用 IMGUI 写入的自定义 PropertyDrawer的示例

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

[CustomPropertyDrawer(typeof(Ingredient))]
public class IngredientDrawerUIE : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);

        // label
        position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);

        // 控制字段缩进 设置为不缩进
        var indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;

        // 计算矩形范围
        var nameRect = new Rect(position.x, position.y, 30, position.height); 
        var amountRect = new Rect(position.x + 35, position.y, 50, position.height);
        var unitRect = new Rect(position.x + 90, position.y, position.width - 90, position.height);


        // 绘制字段
        EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none);
        EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("amount"), GUIContent.none);
        EditorGUI.PropertyField(unitRect, property.FindPropertyRelative("unit"), GUIContent.none);
        

        // 控制字段缩进 设置为原来的数值
        EditorGUI.indentLevel = indent;

        EditorGUI.EndProperty();
    }
}

将Recipe脚本添加到对象上查看效果
在这里插入图片描述
那可能有同学就会问“嗯很好很强大那有啥用呢”
在这里插入图片描述
简单来说就是可以渲染 Serializable 类的实例中的样式让我们可以实现一些快捷的作用下面就是Unity3D中内置的PropertyDrawers一起来看一下效果吧。

2-3、内置的PropertyDrawers

using UnityEngine;

public class ExampleClass : MonoBehaviour
{
    [Range(0, 20)]
    public int intValue = 10;

    [Header("名称")]
    public string nameStr;

    [SerializeField]
    private float floatValue = 10f;
}

在这里插入图片描述

如图所示Range可以限制intValue的取值范围020Header可以给字段做一些描述或备注SerializeField允许我们讲一个Private私有字段同Public字段一样显示在Inspector检视面板上。

2-4、重写Time特性实现秒转分钟功能

一新建TimeAttribute.cs编辑代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public sealed class TimeAttribute : PropertyAttribute
{
    /// <summary>
    /// 显示小时
    /// </summary>
    public readonly bool displayHours;
    /// <summary>
    /// 显示毫秒
    /// </summary>
    public readonly bool displayMillseconds;
    /// <summary>
    /// 构造函数 
    /// </summary>
    /// <param name="displayHours">显示小时</param>
    /// <param name="displayMillseconds">显示毫秒</param>
    public TimeAttribute(bool displayHours = false, bool displayMillseconds = false)
    {
        this.displayHours = displayHours;
        this.displayMillseconds = displayMillseconds;
    }
}

2有了TimeAttribute后我们来自定义它如何在Inspector上进行绘制这个需要新建一个Editor文件夹在里面新建TimeAttributeDrawer.cs脚本重写OnGUI方法来实现绘制GetPropertyHeight用来定义绘制属性的高度

using UnityEngine;
using UnityEditor;
using System;
using UnityEngine.UIElements;
using static UnityEditor.PlayerSettings;

[CustomPropertyDrawer(typeof(TimeAttribute))]
public class TimeAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (property.propertyType == SerializedPropertyType.Float)
        {
            property.floatValue = EditorGUI.FloatField(new Rect(position.x, position.y, position.width * 0.6f, position.height), label, property.floatValue);
            EditorGUI.LabelField(new Rect(position.x + position.width * 0.6f, position.y, position.width * 0.4f, position.height), GetTimeFormat(property.floatValue));
        }
        else
        {
            EditorGUI.HelpBox(new Rect(position.x, position.y, position.width, position.height), "只支持float类型属性", MessageType.Error);
        }
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return base.GetPropertyHeight(property, label);
    }

    private string GetTimeFormat(float secondsTime)
    {
        TimeAttribute ta = attribute as TimeAttribute;
        //显示小时不显示毫秒
        if (ta.displayHours && !ta.displayMillseconds)
        {
            int l = Convert.ToInt32(secondsTime);
            int hours = l / 3600;
            int minutes = l % 3600 / 60;
            int seconds = l % 3600 % 60;
            return string.Format("{0:D2}:{1:D2}:{2:D2}", hours, minutes, seconds);
        }
        //显示毫秒不显示小时
        else if (!ta.displayHours && ta.displayMillseconds)
        {
            int l = Convert.ToInt32(secondsTime * 1000);
            int minutes = l / 60000;
            int seconds = l % 60000 / 1000;
            int millSeconds = l % 60000 % 1000;
            return string.Format("{0:D2}:{1:D2}.{2:D3}", minutes, seconds, millSeconds);
        }
        //既显示小时也显示毫秒
        else if (ta.displayHours && ta.displayMillseconds)
        {
            int l = Convert.ToInt32(secondsTime * 1000);
            int hours = l / 3600000;
            int minutes = l % 3600000 / 60000;
            int seconds = l % 3600000 % 60000 / 1000;
            int millSeconds = l % 3600000 % 60000 % 1000;
            return string.Format("{0:D2}:{1:D2}:{2:D2}.{3:D3}", hours, minutes, seconds, millSeconds);
        }
        //既不显示小时也不显示毫秒
        else
        {
            int l = Convert.ToInt32(secondsTime);
            int minutes = l / 60;
            int seconds = l % 60;
            return string.Format("{0:D2}:{1:D2})", minutes, seconds);
        }
    }
}

3随便新建一个类ExampleClass.cs来调用Time特性

using System;
using TreeEditor;
using UnityEngine;

public class ExampleClass : MonoBehaviour
{
    [Time(true)]
    public float time = 123; 
}

4将ExampleClass附加到任意对象上运行代码

在这里插入图片描述

2-5、自定义绘制可序列化的类或结构体

假设场景我们需要制作一个人员添加功能人员有名字、性别、年龄等属性。

那么比较一般的实现方式如下

using System;
using UnityEngine;


public enum Gender
{
    Man,
    Woman
}
[Serializable]
public class Person
{
    public string Name;
    public int Age;
    public Gender Sex;
}
public class ExampleClass : MonoBehaviour
{
    public Person[] people;
}

在这里插入图片描述
接下来就自定义绘制Person类需要在Editor文件夹内新建脚本PersonDrawer.cs继承PropertyDrawer类代码参考如下

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(Person))]
public class PersonDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        //FocusType.Passive 使用Tab键切换时不会被选中FocusType.Keyboard 使用Tab键切换时会被选中很显然这里我们不需要label能被选中进行编辑 
        position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
        //不让indentLevel层级影响到同一行的绘制因为PropertyDrawer在很多地方都有可能被用到可能出现嵌套使用
        var indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        var nameRect = new Rect(position.x, position.y, 80, position.height);
        var typeRect = new Rect(position.x + 85, position.y, 30, position.height);
        var overviewRect = new Rect(position.x + 120, position.y, position.width - 120, position.height);
        EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("Name"), GUIContent.none);
        EditorGUI.PropertyField(typeRect, property.FindPropertyRelative("Age"), GUIContent.none);
        EditorGUI.PropertyField(overviewRect, property.FindPropertyRelative("Sex"), GUIContent.none);
        EditorGUI.indentLevel = indent;
        EditorGUI.EndProperty();
    }
}

重新编译完成后查看Inspector面板
在这里插入图片描述
非常nice

2-6、根据bool属性来开启或者关闭某个对象实例

假设这么一种情况比如勾选了isCollider那么就应该显示BoxCollider的卡槽如果没有勾选isCollider就不需要显示BoxCollider的卡槽。

这种情况少但是应该是有的那么就来实现一下吧。

using System;
using UnityEngine;

[System.Serializable]
public class BoxColliderAttribute
{
    [SerializeField] private bool m_IsCollider;
    [SerializeField] private BoxCollider m_BoxCollider;
}

public class ExampleClass : MonoBehaviour
{
    public BoxColliderAttribute box;
}

在这里插入图片描述
接下来就自定义绘制BoxColliderAttribute类需要在Editor文件夹内新建脚本BoxColliderAttributeDrawer.cs继承PropertyDrawer类代码参考如下

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(BoxColliderAttribute), true)]
public class BoxColliderAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        Rect drawRect = position;
        drawRect.height = EditorGUIUtility.singleLineHeight;
        EditorGUI.LabelField(position, "是否显示");
        SerializedProperty m_Selected = property.FindPropertyRelative("m_IsCollider");
        SerializedProperty m_Panel = property.FindPropertyRelative("m_BoxCollider");
        drawRect.x += 80;
        EditorGUI.PropertyField(drawRect, m_Selected, GUIContent.none);
        if (m_Selected.boolValue)
        {
            drawRect.x += 45;
            drawRect.width = position.width - 125;
            EditorGUI.PropertyField(drawRect, m_Panel, GUIContent.none);
        }

    }

    public override float GetPropertyHeight(SerializedProperty prop, GUIContent label)
    {
        return 1 * EditorGUIUtility.singleLineHeight + 1 * EditorGUIUtility.standardVerticalSpacing;
    }
}

重新编译完成后查看Inspector面板
在这里插入图片描述

三、后记

PropertyDrawer还是很强大的我们可以定义很多非常方便的Attribute去使用。

合理使用的话可以提高工作效率省去重复工作。

如果觉得本篇文章有用别忘了点个关注关注不迷路持续分享更多Unity干货文章。


你的点赞就是对博主的支持有问题记得留言

博主主页有联系方式。

博主还有跟多宝藏文章等待你的发掘哦

专栏方向简介
Unity3D开发小游戏小游戏开发教程分享一些使用Unity3D引擎开发的小游戏分享一些制作小游戏的教程。
Unity3D从入门到进阶入门从自学Unity中获取灵感总结从零开始学习Unity的路线有C#和Unity的知识。
Unity3D之UGUIUGUIUnity的UI系统UGUI全解析从UGUI的基础控件开始讲起然后将UGUI的原理UGUI的使用全面教学。
Unity3D之读取数据文件读取使用Unity3D读取txt文档、json文档、xml文档、csv文档、Excel文档。
Unity3D之数据集合数据集合数组集合数组、List、字典、堆栈、链表等数据集合知识分享。
Unity3D之VR/AR虚拟仿真开发虚拟仿真总结博主工作常见的虚拟仿真需求进行案例讲解。
Unity3D之插件插件主要分享在Unity开发中用到的一些插件使用方法插件介绍等
Unity3D之日常开发日常记录主要是博主日常开发中用到的用到的方法技巧开发思路代码分享等
Unity3D之日常BUG日常记录记录在使用Unity3D编辑器开发项目过程中遇到的BUG和坑让后来人可以有些参考。
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6