当前位置: 首页 > news >正文

Unity编辑器界面扩展——4、Inspector栏UI扩展

  大家好,我是阿赵。继续介绍Unity编辑器界面扩展。
  在开发过程中,经常会遇到想自定义Inspector栏的内容。Inspector栏的内容都是通过Component来自定义的,比如可以自定义Unity已有的类型,比如Transform、RectTransform、Camera、Light这种,或者自定义自己写的继承MonoBehaviour的脚本的内容。
  下面通过自定义Transform组件的Inspector作为例子,来说明一下应该怎样做。
在这里插入图片描述

一、指定组件的自定义Inspector界面

  在Editor文件夹里面新建一个C#文件,比如我这里新建了一个CustomTransformEditor 文件
在这里插入图片描述

  然后输入以下内容:

using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Transform)), CanEditMultipleObjects]
public class CustomTransformEditor : Editor
{
    public override void OnInspectorGUI()
    {
        //这里写需要显示的组件
    }
}

  代码很简单,简单说明一下:

1. 指定修改某一种组件的Inspector界面

  比如我这里是修改Transform,所以是

CustomEditor(typeof(Transform))

2. 可以同时编辑多个物体

CanEditMultipleObjects

  加上了这一句之后,就可以在同时选择多个有相同Component的对象时,Inspector菜单可以同时修改这些对象的相同Component的内容。

3. 继承Editor

public class CustomTransformEditor : Editor

  虽然这不是一个EditorWindow,但却是一个Editor。继承了Editor之后,才能使用它的很多方法。

4. 写需要显示的UI内容

  需要显示的UI内容,要写在OnInspectorGUI方法里面。
由于现在OnInspectorGUI里面什么内容都没有,所以现在回到Unity编辑器,选择一个GameObject,会发现Transform组件里面什么都没有了。
在这里插入图片描述

  如果现在加上一句代码:

    public override void OnInspectorGUI()
    {
        //这里写需要显示的组件
        GUILayout.Label("this is a transform");
	}

  这时候回去编辑器,会看到这句话显示在Transform组件里面了。
在这里插入图片描述

二、 需要显示原来的界面内容:

  刚才的例子有一个问题,有时候我们可能并不想完全重写原来的组件的UI,而是在原有基础上加一点内容。那么怎么才能把原来的组件UI显示出来呢?
输入以下代码:

using System.Reflection;
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Transform)), CanEditMultipleObjects]
public class CustomTransformEditor : Editor
{
    private Editor instance;
    private void OnEnable()
    {
        if(targets.Length<=0)
        {
            return;
        }
        Transform tr = target as Transform;

        var editorType = Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector");

        instance = CreateEditor(targets, editorType);
        //调用原编辑器的OnEnable方法
        editorType.GetMethod("OnEnable", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(instance, null);
    }
    public override void OnInspectorGUI()
    {
        if(instance!=null)
        {
            instance.OnInspectorGUI();
        }
        //这里写需要显示的组件
        GUIStyle style = new GUIStyle();
        style.normal.textColor = Color.yellow;//这里只是改个字的颜色好看一点,不是必要的
        GUILayout.Label("this is a transform",style);
	}

}

  保存之后回到Unity编辑器,会看到现在Transform组件的属性栏,显示了原有的UI内容,然后在最后面显示了一句新的内容。

在这里插入图片描述

  内容稍微复杂,说明一下:

1. 判断对象

  target是当前一个选中对象
  targets是在可以选择多个物体的情况下,选择的多个对象
  所谓的对象,比如这个是重写Transform组件的界面,所以target应该就是一个Transform,而targets就是一个Transform的数组。
  所以判断一下targets的长度,如果长度小于等于0,其实是没有需要显示的对象,所以直接return

2. 通过反射找到原来类型对应的编辑器类型

 var editorType = Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector");

  由于需要显示原有的组件的Editor内容,所以需要找到原有组件的Editor类型。
  这个地方有点特别,比如Transform,它的原有Editor并不是UnityEditor.Transform,而是UnityEditor.TransformInspector。不同的组件的Editor可能类型不太一样。

3. 生成原来编辑器类型对应的实例

instance = CreateEditor(targets, editorType);

  通过Editor.CreateEditor方法可以把对象和类型传入,返回一个对应类型的Editor实例。
  这个Editor的实例就是我们能显示原组件UI的关键了。

4. 重写原编辑器对应的方法

  由于已经拿到了原组件类型对应的Editor实例,所以以下的操作都是围绕这个实例来进行的。
  比如要显示原编辑器的界面,就在重写的OnInspectorGUI方法里面调用实例的原方法

if(instance!=null)
{
    instance.OnInspectorGUI();
}

5. 调用原编辑器的生命周期

  关于原组件编辑器实例的其他生命周期方法,都可以通过实例来调用,使用反射的方法是这样调用:

editorType.GetMethod("OnEnable", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(instance, null);

  通过反射获取方法,然后通过Invoke来调用。

三、实用的扩展Transform例子

  上面简单的介绍了用法,接下来写一个有点实用价值的例子具体说明一下用法。
输入以下代码:

using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Transform)), CanEditMultipleObjects]
public class CustomTransformEditor : Editor
{
    private Vector3 posVal;
    private Vector3 rotaVal;
    private Vector3 scaleVal;
    private Transform tr;
    private void OnEnable()
    {
        tr = target as Transform;
        posVal = tr.localPosition;
        rotaVal = tr.localRotation.eulerAngles;
        scaleVal = tr.localScale;
    }
    public override void OnInspectorGUI()
    {
        //这一行很重要,如果不先规定label的宽度,下面的layout里面的label就会显示得很宽,导致没法布局排版
        EditorGUIUtility.labelWidth = 15f;
        //获取对象的Transform属性
        posVal = tr.localPosition;
        rotaVal = tr.localRotation.eulerAngles;
        scaleVal = tr.localScale;
        //显示一个HelpBox
        EditorGUILayout.HelpBox("这是一个阿赵修改过的面板",MessageType.Warning,true);
        //显示一个按钮,点击之后,对象的Transform全部都会重置
        if(GUILayout.Button("一键重置Transform"))
        {
            Undo.RegisterCompleteObjectUndo(tr, "Reset Transform");
            posVal = Vector3.zero;
            rotaVal = Vector3.zero;
            scaleVal = Vector3.one;
            tr.localPosition = posVal;
            tr.localRotation = Quaternion.Euler(rotaVal.x, rotaVal.y, rotaVal.z);
            tr.localScale = scaleVal;
        }

        //显示位置属性
        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("C", GUILayout.Width(20)))//显示一个按钮,点击之后会把对应的属性复制到剪切板
        {
            CopyValue(posVal);
        }
        EditorGUI.BeginChangeCheck();//检查属性是否被修改
        if (GUILayout.Button("P", GUILayout.Width(20)))//显示一个按钮,点击之后会把位置归零
        {
            posVal = Vector3.zero;
        }
        EditorGUILayout.LabelField("位置:", GUILayout.Width(40));
        posVal = DrawVector3(posVal);//为了不写重复代码,把显示Vector3的输入组装一个方法。用Vector3Field也行,但显示会没那么好看
        EditorGUILayout.EndHorizontal();

        if (EditorGUI.EndChangeCheck())//当输入了之后,对Transform的值进行修改
        {
            Undo.RegisterCompleteObjectUndo(tr, "Change Position");//在修改前把当前状态记录到可撤回的操作
            tr.localPosition = posVal;
        }

        //显示旋转属性,和显示位置雷同,所以不做额外备注
        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("C", GUILayout.Width(20)))
        {
            CopyValue(rotaVal);
        }
        EditorGUI.BeginChangeCheck();
        if (GUILayout.Button("P", GUILayout.Width(20)))
        {
            rotaVal = Vector3.zero;
        }
        EditorGUILayout.LabelField("旋转:", GUILayout.Width(40));
        rotaVal = DrawVector3(rotaVal);
        EditorGUILayout.EndHorizontal();

        if (EditorGUI.EndChangeCheck())
        {
            Undo.RegisterCompleteObjectUndo(tr, "Change Rotation");
            tr.localRotation = Quaternion.Euler(rotaVal.x,rotaVal.y,rotaVal.z);
        }

        //显示缩放属性,和显示位置雷同,所以不做额外备注
        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("C", GUILayout.Width(20)))
        {
            CopyValue(scaleVal);
        }
        EditorGUI.BeginChangeCheck();
        if (GUILayout.Button("P", GUILayout.Width(20)))
        {
            scaleVal = Vector3.one;
        }
        EditorGUILayout.LabelField("缩放:", GUILayout.Width(40));
        scaleVal = DrawVector3(scaleVal);
        EditorGUILayout.EndHorizontal();

        if (EditorGUI.EndChangeCheck())
        {
            Undo.RegisterCompleteObjectUndo(tr, "Change Scale");
            tr.localScale = scaleVal;
        }
        //再显示一个HelpBox
        EditorGUILayout.HelpBox("下面还可以继续添加内容", MessageType.Warning, true);
    }

    /// <summary>
    /// 显示Vector3的输入
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    private Vector3 DrawVector3(Vector3 value)
    {
        GUILayoutOption opt = GUILayout.MinWidth(30f);
        value.x = EditorGUILayout.FloatField("X", value.x, opt);
        value.y = EditorGUILayout.FloatField("Y", value.y, opt);
        value.z = EditorGUILayout.FloatField("Z", value.z, opt);
        return value;
    }

    /// <summary>
    /// 复制值到剪切板
    /// </summary>
    /// <param name="obj"></param>
    private void CopyValue(object obj)
    {
        GUIUtility.systemCopyBuffer = obj.ToString();
    }
}

  保存之后,回到Unity编辑器,会看到现在Transform组件的Inspector界面变成了这样了:
在这里插入图片描述

  代码上面已经有详细的备注,扩展了Transform的面板,可以对Transform一键重置,或者点C键复制内容到剪切板,或者点P键给对应属性重置,算是一个比较实用的小工具扩展。
需要注意一下的是

1. 控制label的宽度

EditorGUIUtility.labelWidth = 15f;

  在Inspector面板里面,label的宽度默认是很大的,如果不强制设置小一些,会发现很多时候layout自动布局会很乱。

2. 在编辑器状态检查值是否有改变

EditorGUI.BeginChangeCheck()
if (EditorGUI.EndChangeCheck())

  在BeginChangeCheck和EndChangeCheck之间的代码,如果中途有值发生了变化,就会在EndChangeCheck的时候触发,这时候就可以判断用户修改了对应的内容,就可以执行对应的修改操作了。

3. 可撤回操作

Undo.RegisterCompleteObjectUndo

  在代码里面的操作,按道理是不能通过Ctrl+Z或者菜单栏的撤销功能来撤回的。不过我们可以通过RegisterCompleteObjectUndo,把修改前的内容记录下来,就可以正常通过Ctrl+Z之类的操作来撤回了。

四、显示右键菜单

可以在组件的Inspector菜单上面鼠标右键弹出菜单出现特殊的选项,可以试试:

    [MenuItem("CONTEXT/Transform/ResetTransform")]
    static void ResetTransform(MenuCommand command)
    {        
        Transform trans = command.context as Transform;
        Undo.RegisterCompleteObjectUndo(trans, "Reset Transform");
        trans.localPosition = Vector3.zero;
        trans.localRotation = Quaternion.identity;
        trans.localScale = Vector3.one;
}

  保存之后回到Unity编辑器,在Transform组件右键,会看到多出来自定义的选项
在这里插入图片描述

说明:

1. 显示右键菜单

[MenuItem("CONTEXT/Transform/ResetTransform")]

  MenuItem在之前介绍EditorWindow窗体时介绍过,这里指定的路径是CONTEXT/Transform,所以出现菜单的地方就会变成在Transform的Inspector右键菜单了

2. 获取需要控制的对象

Transform trans = command.context as Transform;

  由于MenuItem对应的方法都是要求Static的,所以不能通过target来获取当前选择对象的Transform。不过可以通过传入的MenuCommand 变量来获取到对应的content,就是选择对象对应的组件了。

五、 禁用某些功能

有时候如果某些条件出现时,可能我们会想禁用整个组件的Inspector菜单,试试一下代码:

    [MenuItem("CONTEXT/Transform/LockComponent")]
    static void LockComponent(MenuCommand command)
    {
        Transform trans = command.context as Transform;
        Undo.RegisterCompleteObjectUndo(trans, "LockComponent");
        trans.hideFlags = HideFlags.NotEditable;
    }

    [MenuItem("CONTEXT/Transform/UnLockComponent")]
    static void UnLockComponent(MenuCommand command)
    {
        Transform trans = command.context as Transform;
        Undo.RegisterCompleteObjectUndo(trans, "UnLockComponent");
        trans.hideFlags = HideFlags.None;
}

保存之后回到编辑器,可以看到Transform组件Inspector菜单右键多出2个选项
在这里插入图片描述

  如果选择LockComponent,会发现Transform的Inspector栏变成不可编辑状态了:
在这里插入图片描述

  这里是通过MenuItem菜单来触发的,也可以通过别的条件去触发锁定和解锁操作。

相关文章:

  • SpringBoot实现一个Redis限流注解
  • 如何从受 Cloudflare 保护的网站提取数据:技术与挑战
  • 每日一题---数组中两个字符串的最小距离
  • 混淆矩阵概念
  • 使用kubeadm方式以及使用第三方工具sealos搭建K8S集群
  • 使用Node的http模块创建web服务,给客户端返回html页面时,css失效的根本原因(有助于理解http)
  • 走路碎步营养补充贴士
  • 使用libwebsocket写一个server
  • 【AI】利用Azure AI的元数据过滤器提升 RAG 性能并增强向量搜索案例
  • 【备考记录】三种校验码
  • pop是什么的缩写?为什么Python用它表示删除元素?
  • 【统计学相关笔记】2. 多元正态的Cochran定理
  • iptables练习笔记20250315
  • 盖革管死区时间导致脉冲丢失分析
  • 3.9/Q2,Charls最新文章解读!
  • 苹果电脑杀毒软件CleanMyMac
  • Android 手机启动过程
  • [C++Qt] 槽函数收不到信号问题(信号的注册)
  • 当大模型训练遇上“双向飙车”:DeepSeek开源周 DualPipe解析指南
  • 数字化转型 - 数据驱动
  • “大国重器”、新型反隐身雷达……世界雷达展全面展示尖端装备
  • “复旦源”一源六馆焕新启幕,设立文化发展基金首期1亿元
  • 中国驻美大使:远离故土的子弹库帛书正随民族复兴踏上归途
  • 特朗普指控FBI前局长“暗示刺杀总统”,“8647”藏着什么玄机?
  • 六省会共建交通枢纽集群,中部离经济“第五极”有多远?
  • 一周文化讲座|“我的生命不过是温柔的疯狂”