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菜单来触发的,也可以通过别的条件去触发锁定和解锁操作。