作者最新更新

Unity3D热门教程

    游戏开发工具

    UnityEditor编辑之自定义Inspector面板

    562

    50 次收藏2024-08-24 09:02:21发布

    Inspector 窗口可对 Unity 编辑器中几乎所有内容(包括游戏对象、Unity 组件、资源、材质)查看和编辑属性和设置,以及查看和编辑编辑器内的设置和首选项。

    1.jpg

    通常情况是不需要扩展属性面板的,也就是Inspector,但如果需要开发定制的工具和插件,那么就需要扩展属性面板来完成需求。本文会对自定义扩展Inspector进行全面的介绍


    一、工具类和概念的介绍

    自定义Inspector有两个层面,第一个是扩展已有属性的功能,第二个是按照意图增加新的面板功能。扩展已有的功能,包括替换显示文字,动态控制可见性,增加动画,错误提示,数据的校验和限制等等。增加新的功能就是按照需求定制更为复杂的功能。

    Unity的内置工具类提供了必要的实现基础,主要有EditorGUILayout和EditorGUI两大类。两者有重叠的部分,也有特有的部分。总体上来说,EditorGUILayout是带有自动布局的,EditorGUI需要传入Rect来对控件进行定位。另外,这两者和GUILayout与GUI概念非常类似,只不过GUI的主要绘制是在场景中(运行时,游戏中可见),EditorGUI上在Inspector中(仅在编辑器中)。

    自定义流程一共分为三步:

    第一步,继承Editor父类。

    第二步,添加[CustomEditor(typeof(MonoBehaviour))]注解,告诉编辑器这个类是扩展哪个组件的Inspector。

    第三步,覆写OnInspectorGUI方法,实现自定义的扩展。

    public class CustomEditorTest : MonoBehaviour 
    {
        [Space(10)]
        public int     intValue;
        public bool    boolValue;
        public Vector2 v2;
        public float[] floatArray = new float[] {1.0f, 2.0f, 3.0f};
    }
    
    [CanEditMultipleObjects, CustomEditor(typeof(CustomEditorTest))]
    public class CustomEditorTestEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            // 自定义绘制Inspector
        }
    }
    二、一个简单的自定义Inspector面板

    对MonoTest类编辑自定义面板

    [Serializable]
    public class MonoTest: MonoBehaviour
    {
        public enum EnumValue
        {
            EnumValue1,
            EnumValue2,
            EnumValue3,
        }
    
        public int intValue;
        public bool boolValue;
        public EnumValue enumValue;
    }
    
    //自定义面板代码
    [CustomEditor(typeof(MonoTest), true)]
    //[CustomEditor(typeof(CustomClass ), true)]
    public class MonoTestEditor : Editor
    {
        private SerializedProperty m_IntValue;
        private SerializedProperty m_BoolValue;
        private SerializedProperty m_EnumValue;
        private void OnEnable()
        {
            m_IntValue = serializedObject.FindProperty("intValue");
            m_BoolValue = serializedObject.FindProperty("boolValue");
            m_EnumValue = serializedObject.FindProperty("enumValue");
        }
    
        public override void OnInspectorGUI()
        {
            //base.OnInspectorGUI();
            //serializedObject.Update();
            EditorGUILayout.BeginHorizontal();
            EditorGUIUtility.labelWidth = 100;
            EditorGUILayout.PropertyField(m_IntValue);
            EditorGUILayout.PropertyField(m_BoolValue);
            EditorGUILayout.PropertyField(m_EnumValue);
            EditorGUILayout.EndHorizontal();
            //serializedObject.ApplyModifiedProperties();
        }
    }

    2.jpg

    对CustomClass类编辑自定义面板

    public class CustomClass : MonoBehaviour
    {
        public List<MonoTest> Datas = new List<MonoTest>();
    }

    1.jpg

    三、Inspector面板的Editor类

    Editor类和EditorWindow类都继承自同一个基类:ScriptableObject,他们都可以针对某种脚本类来进行操作

    Editor类只能对指定脚本进行扩展,Inspector面板的显示,或变量在Scene视图中的可视化,使用到的类:

    GUILayout:

    绘制各种2D控件,比如按钮、文本、可折叠列表,以及对它们的分组和排版。这些控件既可以绘制在Editor类里,也可以绘制在EditorWindow里

    EditorGUILayout:

    与GUILayout类似,但是提供更多的预定义控件

    GUIUtility:

    绘制自定义的2D控件,比如进度条、对话框、打开文件夹

    EditorGUIUtility:

    绘制自定义的2D控件,比如进度条、对话框、打开文件夹


    四、编辑器开发之CustomEditor

    CustomEditor特性,允许我们自定义组件的Inspect检视面板。

    public CustomEditor (Type inspectedType);
    public CustomEditor (Type inspectedType, bool editorForChildClasses);

    param1: inspectedType 检视的类型,即自定义哪个类型的Inspector。

    param2: editorForChildClasses 默认为false,为true时表明其子类使用同样的Inspector。

    例如,我们创建一个Person组件:

    using UnityEngine;
    public class Person : MonoBehaviour
    {
        public string Name;
        public int Age;
        public float Weight;
    }

    检视面板显示了Person组件中的三个公开字段:

    接下来自定义该组件的检视面板,首先需要在Editor文件夹中创建一个PersonInspector.cs脚本

    引入命名空间UnityEditor后,为该类添加CustomEditor特性,并继承Editor类:

    using UnityEditor;
    
    [CustomEditor(typeof(Person))]
    public class PersonInspector : Editor {}

    接下来重写OnInspectorGUI方法来自定义我们所需要的内容

    比如在面板上显示一个字符串:

    using UnityEngine;
    using UnityEditor;
    
    [CustomEditor(typeof(Person))]
    public class PersonInspector : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            GUILayout.Label("Editor Extension...");
        }
    }

    再比如在面板上添加一个按钮:

    using UnityEngine; 
    using UnityEditor;
    
    [CustomEditor(typeof(Person))]
    public class PersonInspector : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            GUILayout.Label("Editor Extension...");
            GUILayout.Button("Button");
        }
    }

    具体如何绘制自定义检视面板,例如添加Button、Toggle、Slider等元素,显示string、int、float、enum等字段,在后续文章中进行介绍。

    五、重写OnInspectorGUI方法进行绘制
    public override void OnInspectorGUI()
    {
        // 绘制全部原有属性
         base.DrawDefaultInspector()
        // 后面可以扩展自己功能
    }


    六、自定义绘制

    有两个重要的内置对象,target和serializedObject。target代表的是CustomEditorTest本身,而serializedObject代表的是当前Inspector的可绘制对象。

    public override void OnInspectorGUI()
    {
      // 更新显示
      this.serializedObject.Update();
       
      // 自定义绘制
      
      // 应用属性修改
      this.serializedObject.ApplyModifiedProperties();
    }


    七、绘制已经有的属性

    有两个接口用来查找已有的属性,FindProperty和FindPropertyRelative。FindProperty用来查找当前属性名对应的属性对象,FindPropertyRelative查找相对于属性对象的属性。

    // 显示intValue属性
    EditorGUILayout.PropertyField(this.serializedObject.FindProperty("intValue"));
    // 显示boolValue并替换属性标签为GUIContent
    EditorGUILayout.PropertyField(this.serializedObject.FindProperty("boolValue"), this.boolValueContent);
    var v2Property = this.serializedObject.FindProperty("v2");
    // 现实v2属性
    EditorGUILayout.PropertyField(v2Property);
    // 设置v2属性的x属性值
    v2Property.FindPropertyRelative("x").floatValue = 999.0f;
    // 分隔符
    EditorGUILayout.Separator();

    EditorGUILayout.PropertyField能够在Inspector上绘制属性控件,如果返回false表示属性控件不可用,或者处在不可见的状态。

    2.jpg

    八、绘制数组属性
    // 查找floatArray属性
    var elements = this.serializedObject.FindProperty("floatArray");
    
    // 属性元素可见,控件展开状态
    if (EditorGUILayout.PropertyField(elements))
    {
        // 缩进一级
        EditorGUI.indentLevel++;
        // 设置元素个数
        elements.arraySize = EditorGUILayout.DelayedIntField("Size", elements.arraySize);
        // 绘制元素
        for (int i = 0, size = elements.arraySize; i < size; i++)
        {
           // 检索属性数组元素
           var element = elements.GetArrayElementAtIndex(i);
           EditorGUILayout.PropertyField(element);
        }
        // 重置缩进
        EditorGUI.indentLevel--;
    }
    // 空格
    EditorGUILayout.Space();

    对elements.arraySize赋值就会自动设置数组容量。DelayedIntField在回车的时候,控件设置的数值才会返回到arraySize。

    1.jpg

    九、绘制默认数组和对象
    public CustomData   data;
    public CustomData[] datas;
    
    [System.Serializable]
    public class CustomData
    {
     public int a;
     public int b;
     public int c;
    }
    
    // 第二个参数为true,则会默认绘制所有子元素
    EditorGUILayout.PropertyField(this.serializedObject.FindProperty("data"), true);
    EditorGUILayout.PropertyField(this.serializedObject.FindProperty("datas"), true);

    1.jpg

    十、绘制折叠动画
    // 每个动画都需要一个AnimBool
    private AnimBool fadeGroup;
    
    private void OnEnable()
    {
          this.fadeGroup = new AnimBool(true);
          // 注册动画监听
          this.fadeGroup.valueChanged.AddListener(this.Repaint);
    }
    
    private void OnDisable()
    {
        // 移除动画监听
        this.fadeGroup.valueChanged.RemoveListener(this.Repaint);
    }
    
    // target控制动画开始播放
    this.fadeGroup.target = EditorGUILayout.Foldout(this.fadeGroup.target, "BeginFadeGroup", true);
    
    // 系统使用tween渐变faded数值
    if (EditorGUILayout.BeginFadeGroup(this.fadeGroup.faded))
    {
        EditorGUILayout.BoundsField("BoundsField", new Bounds());
        EditorGUILayout.BoundsIntField("BoundsIntField", new BoundsInt());
    }
    // begin - end 之间元素会进行动画
    EditorGUILayout.EndFadeGroup();
    // 又一种风格的空格
    GUILayout.Space(10);

    1.gif

    十一、绘制水平布局
    // 水平布局,并使用box皮肤
    EditorGUILayout.BeginHorizontal(GUI.skin.box);
    // 使用了GUILayout去覆盖自动布局的设置
    EditorGUILayout.LabelField("This is BeginHorizontal", GUILayout.MaxWidth(150.0f));
    EditorGUILayout.DelayedDoubleField(11.1f);
    EditorGUILayout.DelayedTextField("DelayedTextField");
    EditorGUILayout.DropdownButton(GUIContent.none, FocusType.Passive);
    
    EditorGUILayout.EndHorizontal();
    
    EditorGUILayout.Separator();

    2-1.jpg

    十二、绘制垂直布局
    EditorGUILayout.BeginVertical(GUI.skin.box);
    // 依然可以使用GUILayout来显示控件
    GUILayout.Box("This is BeginVertical");
    EditorGUILayout.ColorField("ColorField", Color.yellow);
    EditorGUILayout.CurveField("CurveField", new AnimationCurve(), GUILayout.MaxWidth(400.0f));
    EditorGUILayout.HelpBox("HelpBox", MessageType.Info);
    EditorGUILayout.EnumFlagsField("EnumFlagsField", this.types);
    EditorGUILayout.EnumPopup("EnumPopup", this.types);
    
    this.selectOption = EditorGUILayout.IntPopup("IntPopup",   (int) this.selectOption, new string[] {"0", "1", "2"}, new int[] {0, 1, 2});
    this.selectOption = EditorGUILayout.IntSlider("IntSlider", (int) this.selectOption, 0, 2);
    this.selectOption = EditorGUILayout.MaskField("MaskField", (int) this.selectOption, new string[] {"mask1", "mask2", "mask3"});
    this.selectOption = EditorGUILayout.Popup("Popup", (int) this.selectOption, new string[] {"s1", "s2", "s3"});
    
    EditorGUILayout.EndVertical();
    
    EditorGUILayout.Separator();

    2.jpg

    十三、绘制滚动区域
    // 超出内容会出现滚动条
    this.scrollPos   = EditorGUILayout.BeginScrollView(this.scrollPos, GUI.skin.box);
    
    GUILayout.Box("this is BeginScrollView");
    EditorGUILayout.TextArea("this is TextArea");
    EditorGUILayout.RectField("RectField", new Rect());
    this.toggle      = EditorGUILayout.Toggle("Toggle", this.toggle);
    // 在toggleGroup之间的会被整体设置可用性
    this.toggleGroup = EditorGUILayout.BeginToggleGroup("BeginToggleGroup", this.toggleGroup);
    
    GUILayout.Button("Btn1");
    GUILayout.Button("Btn2");
    GUILayout.Button("Btn3");
    
    EditorGUILayout.EndToggleGroup();
    
    EditorGUILayout.EndScrollView();

    1.jpg

    十四、总结

    1、PropertyField方法是用来显示已有属性控件的,第二个参数为true则绘制所有子元素。

    2、Delayed开头的Field方法,是延迟控件。回车确定后,数值才会返回。

    3、其它Field方法是用来绘制自定义控件的。

    4、Begin - End 是区域绘制控件,在之间的绘制会被整体控制。

    5、可以混合使用GUI和GUILayout以及EditorGUI进行绘制。

    6、GUIContent 提供了重写控件名字和添加提示的机会。

    7、GUILayoutOption 是覆盖自动布局的设置,属性来自于GUILayout类。