CustomPropertyDrawer

Unity3D CustomPropertyDrawer 自定义属性绘制器

api文档

  • 该文档中的EditorGUI.BeginProperty()EditorGUI.EndProperty(),不好用
    • 参考案例:
      • 直接看Unity中你感兴趣的渲染方式的实现方式: Packages/com.unity.ugui/Editor/UI/PropertyDrawers/...
        • FontDataDrawer
        • SpriteStateDrawer
        • ColorBlockDrawer
        • NavigationDrawer
        • 也可以直接在Project视图搜索Drawer,搜索范围选择In Packagers

          Serach Drawer In Project

需要做的事

绘制

  • 可以做的事:
    • 美化 / 简化 显示效果
    • 根绝枚举值, 差异化显示
    • 加一些 按钮 / Toggle 进行快捷操作

重写GetPropertyHeight 返回真正的高度

  • 不要忘记标题占了1行
  • 考虑展开 / 收缩 的情况, 返回不同的高度,
    • 收缩时,高度=EditorGUIUtility.singleLineHeight
    • 展开时,高度=(1 + 标题以外所有内容的行数) x EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing x (所有行数-1)

基本常识:

  • OnGUI(Rect position, SerializedProperty property, GUIContent label)
    • position 是这个变量的起始位置
    • property 是这个变量的SerializedProperty对象
      • 通过这个对象,可以获取到变量的值 property.FindPropertyRelative(paramName).(intValue / stringValue / vector3Value / enumValueIndex / ...)
      • lable.text 是变量名称
  • 默认每行高度是: EditorGUIUtility.singleLineHeight
  • 默认行间距是: EditorGUIUtility.standardVerticalSpacing
  • 想要缩进/反缩进 1个单位, 可以使用: EditorGUI.indentLevel++EditorGUI.indentLevel--
  • 对于不需要自定义渲染方式的字段 使用 EditorGUI.PropertyField执行默认的渲染方案,
  • 所有要绘制的内容,推荐使用EditorGUI类, 而不是GUI
  • EditorGUI类的方法, 会自动处理缩进的问题
  • GUI类的方法, 不会自动处理缩进的问题
    • 比如 GUI.Button(rect, "↑")),不受缩进的影响,所有要额外把rect.x加上缩进的距离35比较合适
  • 有些效果只有PropertyFiled才可以实现,比如,鼠标滑过名称,修改变量的值;

小技巧

  • 绘制展开 / 折叠按钮
    • 使用EditorGUI.Foldout -> if (foldout = EditorGUI.Foldout(rect, foldout,$"{label.text} 更多内容:{}")
      • 标题行除了显示变量名称以外, 其实可以显示更多信息, 以便在没有展开的情况下就可以把 关键信息 显示到标题行的标题后面
  • 💡 瞬间计算出高度: 直接在绘制结束时, 使用rect.y - position.y即可!!!
  • 获取当前字段从属的脚本所依附的对象 (property.serializedObject.targetObject as Component).gameObject
  • 获取当前property对应的字段的原始数据对象 fieldInfo.GetValue(property.serializedObject.targetObject)
    • 如果当前对象是以Array / List出现, 那么这个方法获取到的是Array / List对象,而不是单个对象,从property.PropertyPath就可以看出其路径
    • fieldInfo : The reflection FieldInfo for the member this property represents.

⚠️ 注意:

  • 必须十分清除, 自己的绘制方案, 占用了多少高度, 否则会出现绘制不全的情况
  • 绘制时
    • 关于layout周边的方法
      • EditorGUI.BeginProperty()EditorGUI.EndProperty包裹的范围里,不可以使用 layout 相关的方法, 否则会报错:ArgumentException: Getting control 1's position in a group with only 1 controls when doing repaint
      • EditorGUI.BeginProperty()外面, 执行layout相关的方法, 不生效不显示
    • OnGUI中使用

简单举例

[Serializable]
public class RotaAtAxisData
{
    public Transform self;

    public Vector3 axis = Vector3.up;
    public float angleTotal;
    public float duration = 1;
    public Space space = Space.World;
}

默认显示如下:

img

重写后↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓如下:

img

  • 变量名字后面直接把关键信息,显示在变量名称后面,即便不展开,也可以看到关键信息

  • 允许点击按钮,直接选择6个标准轴向作为旋转轴

  • 使用2个Toggle来渲染space,要选择Space.World / Space.Self时,直接点就行,下拉框需要点2次,不方便

  • Bug:

    • 当视图过于小时,AxisXYZ会被挤到下一行,导致覆盖下一行内容,这个问题暂时没有解决方案
  • 完整代码如下:

using System;
using System.Collections;
using UnityEngine;
using UnityEditor;

namespace BaseToolsForUnity
{
    /// <summary>
    /// RotaAtAxisData 检视视图个性化渲染器<br/>
    /// </summary>
    [CustomPropertyDrawer(typeof(TransformTween.RotaAtAxisData))]
    public class RotaAtAxisDataDrawer : PropertyDrawer
    {
        // 自身的所有变量
        private SerializedProperty self;
        private SerializedProperty axis;
        private SerializedProperty angleTotal;
        private SerializedProperty duration;
        private SerializedProperty space;

        private float axisButtonWidth = 20;
        private float axisButtonSpaceHor = 5;
        private float spaceToggleWidth = 70;

        /// <summary>
        /// 总高度 <br/>
        /// 直接在 OnGUI 中绘制结束时,计算整绘制所占用的高度!!!<br/>
        /// 默认值,不宜太小,否则一开始点展开的按钮都点不住<br/>
        /// </summary>
        private float heightTotal = 30;

        /// <summary>
        /// <see langword="true"/>:展开<br/>
        /// </summary>
        private bool foldout = false;

        private void Init(SerializedProperty property)
        {
            self = property.FindPropertyRelative("self");
            axis = property.FindPropertyRelative("axis");
            angleTotal = property.FindPropertyRelative("angleTotal");
            duration = property.FindPropertyRelative("duration");
            space = property.FindPropertyRelative("space");
        }



        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            Init(property);

            var rect = position;
            rect.height = EditorGUIUtility.singleLineHeight;

            // 折叠 / 展开 (附加关键信息到标题行后面)
            if (foldout = EditorGUI.Foldout(rect, foldout, EditorGUIUtility.TrTextContent($"{label.text} Axis:[{axis.vector3Value}] Angle:[{angleTotal.floatValue}] Time:[{duration.floatValue}] Space:[{((Space)space.enumValueIndex)}]")))
            {
                //// 绘制标题
                //EditorGUI.LabelField(rect, $"{label.text}({typeof(TransformTween.RotaAtAxisData).Name})");

                // 开始缩进 // 开始绘制内部的字段
                ++EditorGUI.indentLevel;

                // 绘制 self 字段 
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, self);

                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;

                #region 绘制 轴向选择 按钮 到1行
                var rectBackUp = rect;
                rect.x = 35;
                rect.width = axisButtonWidth;
                if (GUI.Button(rect, "↑"))
                {
                    axis.vector3Value = Vector3.up;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "↓"))
                {
                    axis.vector3Value = Vector3.down;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "F"))
                {
                    axis.vector3Value = Vector3.forward;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "B"))
                {
                    axis.vector3Value = Vector3.back;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "←"))
                {
                    axis.vector3Value = Vector3.left;
                }

                rect.x += rect.width;
                rect.x += axisButtonSpaceHor;
                if (GUI.Button(rect, "→"))
                {
                    axis.vector3Value = Vector3.right;
                }
                // 恢复 rect
                rect = rectBackUp;
                #endregion


                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, axis);

                // 绘制要旋转的总角度
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, angleTotal);

                // 绘制动画时间
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(rect, duration);

                // 绘制空间类型
                rect.y += rect.height;
                rect.y += EditorGUIUtility.standardVerticalSpacing;
                // 绘制空间类型的 名称
                EditorGUI.LabelField(rect, $"{space.displayName}");

                // 绘制空间类型 世界空间
                rect.width = spaceToggleWidth;
                rect.x += EditorGUIUtility.labelWidth;
                var isWorld = EditorGUI.ToggleLeft(rect, Space.World.ToString(), (Space)space.enumValueIndex == Space.World);
                space.enumValueIndex = isWorld ? (int)Space.World : (int)Space.Self;

                // 绘制空间类型 自身空间
                rect.x += spaceToggleWidth;
                var isLocal = EditorGUI.ToggleLeft(rect, Space.Self.ToString(), (Space)space.enumValueIndex == Space.Self);
                space.enumValueIndex = !isLocal ? (int)Space.World : (int)Space.Self;

                // 结束缩进 // 结束绘制内部的字段
                --EditorGUI.indentLevel;

                // 多加1行作为备用
                rect.y += EditorGUIUtility.singleLineHeight;

                // 计算总高度
                heightTotal = Math.Abs(rect.y - position.y);

            }
        }

        /// <summary>
        /// 重写 GetPropertyHeight 方法
        /// </summary>
        /// <param name="property"></param>
        /// <param name="label"></param>
        /// <returns></returns>
        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            // 折叠状态
            if (!foldout)
            {
                return EditorGUIUtility.singleLineHeight;
            }

            return heightTotal;
        }

    }

}
posted @ 2023-08-18 12:31  清风0307  阅读(118)  评论(1编辑  收藏  举报