内容将会持续更新,有错误的地方欢迎指正,谢谢!
拥有更好的学习体验 —— 不断努力,不断进步,不断探索 |
助力快速掌握 Inspector 编辑器扩展 为初学者节省宝贵的学习时间,避免困惑! |
文章目录
- 一、Inspector面板头部扩展
- 二、原生Component扩展
- 2.1、直接继承原生组件的编辑器脚本
- 2.2、使用反射获取编辑器脚本的扩展方式
- 三、自定义组件编辑器扩展
- 3.1、使用 UIElements 构建自定义 UI
- 3.1.1、创建 MyPlayer 类
- 3.1.2、创建自定义编辑器脚本MyPlayerEditor
- 3.1.3、定义 UXML 文件
- 3.1.4、定义 USS 文件
- 3.2、使用EditorGUILayout直接绘制
- 3.3、使用 SerializedObject 和 SerializedProperty绘制
一、Inspector面板头部扩展
通过订阅 Editor.finishedDefaultHeaderGUI 事件,这个事件在 Inspector 面板的默认 GUI 绘制完成后触发。
我们可以在 Unity Editor 的 Inspector 面板顶部添加自定义按钮,从而实现对选中对象的自定义操作。
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
[InitializeOnLoadAttribute]
static class EditorHeaderGUID
{
static EditorHeaderGUID()
{
Editor.finishedDefaultHeaderGUI += DisplayGUIDIfPersistent;
}
static void DisplayGUIDIfPersistent(Editor editor)
{
// 确保当前对象是在场景中的对象
if (editor.target is GameObject go && !EditorUtility.IsPersistent(go))
{
GUI.color = Color.green;
// 在顶部添加一个按钮
if (GUILayout.Button("Add Cube Components", GUILayout.Height(30)))
{
// 为选中的物体添加组件
AddComponents(go);
}
// 在顶部添加一个按钮
if (GUILayout.Button("Remove Cube Components", GUILayout.Height(30)))
{
// 为选中的物体移除组件
RemoveComponents(go);
}
GUI.color = Color.white;
}
}
static void AddComponents(GameObject go)
{
if (go != null)
{
// 检查并添加 BoxCollider 组件
if (go.GetComponent<BoxCollider>() == null)
{
go.AddComponent<BoxCollider>();
}
// 检查并添加 Rigidbody 组件
if (go.GetComponent<Rigidbody>() == null)
{
go.AddComponent<Rigidbody>();
}
// 标记场景已更改
EditorSceneManager.MarkSceneDirty(go.scene);
}
}
static void RemoveComponents(GameObject go)
{
if (go != null)
{
// 删除 BoxCollider 组件
BoxCollider boxCollider = go.GetComponent<BoxCollider>();
if (boxCollider != null)
{
Object.DestroyImmediate(boxCollider);
}
// 删除 Rigidbody 组件
Rigidbody rigidbody = go.GetComponent<Rigidbody>();
if (rigidbody != null)
{
Object.DestroyImmediate(rigidbody);
}
// 标记场景已更改
EditorSceneManager.MarkSceneDirty(go.scene);
}
}
}
在本示例中,我们实现了添加和删除BoxCollider 和Rigidbody组件的功能。这种方法可以极大地提高我们在 Unity 编辑器中的工作效率。
二、原生Component扩展
在 Unity 开发过程中,原生组件(如 Transform、Camera、Text、BoxCollider等组件)是 Unity 自带的基本组件。
2.1、直接继承原生组件的编辑器脚本
适用于可以直接访问和继承原生组件的编辑器脚本,比如 TextEditor等。
[CustomEditor(typeof(Text), true)]
public class TextExtension : TextEditor
{
private Text Target { get { return (Text)target; } }
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
GUILayout.Space(10);
GUI.color = new Color(0, 1, 0, 0.5f);
GUILayout.Space(10);
if (GUILayout.Button("Set Text", GUILayout.Height(30)))
{
Target.fontSize = 20;
Target.alignment = TextAnchor.MiddleCenter;
Target.color = Color.white;
}
GUI.color = Color.white;
}
}
TextEditor外部能够访问,可以直接继承,通过base.OnInspectorGUI();可以绘制父类的GUI,可以保持原先布局不变。
2.2、使用反射获取编辑器脚本的扩展方式
某些原生组件的编辑器脚本并不直接暴露给开发者,需要通过反射方式获取才能进行扩展,例如某些内部或私有的编辑器脚本。
实现方法:通过 Assembly、Type 和反射来获取和操作目标组件的编辑器脚本
[CustomEditor(typeof(Transform), true)]
public class ComponentExtension : Editor
{
private Editor m_Editor;
private Transform Target { get { return (Transform)target; } }
private void OnEnable()
{
Type type = Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector", false);
m_Editor = Editor.CreateEditor(target, type);
}
public override void OnInspectorGUI()
{
if (m_Editor == null) return;
m_Editor.OnInspectorGUI();
GUILayout.Space(10);
GUI.color = new Color(0, 1, 0, 0.5f);
GUILayout.Space(10);
if (GUILayout.Button("Reset Position", GUILayout.Height(30)))
{
Target.position = new Vector3(0, 0, 0);
Target.rotation = Quaternion.identity;
Target.localScale = new Vector3(1, 1, 1);
}
GUI.color = Color.white;
}
}
Transform的编辑器脚本外界并不能直接访问,这里通过反射Assembly.GetAssembly(typeof(Editor)).GetType(“UnityEditor.TransformInspector”, false);来获取到Transform的编辑器类型TransformInspector
同时通过调用m_Editor.OnInspectorGUI();来绘制原编辑器的GUI,保证原先布局不变。
三、自定义组件编辑器扩展
3.1、使用 UIElements 构建自定义 UI
在 Unity 中,使用 UIElements 可以更加直观和灵活地设计和实现自定义的 Inspector 面板。以下是一个完整的示例,展示如何使用 UIElements 和 UXML 文件来定义自定义 Inspector 布局,并将其绑定到一个 MyPlayer 对象。
3.1.1、创建 MyPlayer 类
using UnityEngine;
public class MyPlayer : MonoBehaviour
{
public int damage = 50;
public int armor = 30;
public GameObject gun;
}
3.1.2、创建自定义编辑器脚本MyPlayerEditor
首先,我们创建一个自定义编辑器脚本 MyPlayerEditor,继承自 Editor 类,并覆盖 CreateInspectorGUI 方法来加载和设置 UXML 文件和样式表。
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
[CustomEditor(typeof(MyPlayer))]
public class MyPlayerEditor : Editor
{
const string resourceFilename = "custom-editor-uie";
public override VisualElement CreateInspectorGUI()
{
// 创建一个新的 VisualElement 作为自定义 Inspector 的根元素
VisualElement customInspector = new VisualElement();
// 加载 UXML 资源文件
var visualTree = Resources.Load<VisualTreeAsset>(resourceFilename);
if (visualTree != null)
{
// 克隆 UXML 定义的层级视图到自定义 Inspector 中
visualTree.CloneTree(customInspector);
// 加载并添加样式表
var styleSheet = Resources.Load<StyleSheet>($"{resourceFilename}-style");
if (styleSheet != null)
{
customInspector.styleSheets.Add(styleSheet);
}
}
else
{
Debug.LogError($"Could not find UXML resource: {resourceFilename}");
}
return customInspector;
}
}
3.1.3、定义 UXML 文件
创建一个 UXML 文件来定义自定义 Inspector 面板的布局。将该文件保存为 custom-editor-uie.uxml,并将其放置在 Resources 文件夹中。
<UXML xmlns="UnityEngine.UIElements" xmlns:e="UnityEditor.UIElements">
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Damage"/>
<VisualElement class="input-container">
<SliderInt class="player-slider" name="damage-slider" high-value="100" direction="Horizontal" binding-path="damage"/>
<e:IntegerField class="player-int-field" binding-path="damage"/>
</VisualElement>
</VisualElement>
<e:ProgressBar class="player-property-progress-bar" name="damage-progress" binding-path="damage" title="Damage"/>
</VisualElement>
<VisualElement class="player-property">
<VisualElement class="slider-row">
<Label class="player-property-label" text="Armor"/>
<VisualElement class="input-container">
<SliderInt class="player-slider" name="armor-slider" high-value="100" direction="Horizontal" binding-path="armor"/>
<e:IntegerField class="player-int-field" binding-path="armor"/>
</VisualElement>
</VisualElement>
<e:ProgressBar class="player-property-progress-bar" name="armor-progress" binding-path="armor" title="Armor"/>
</VisualElement>
<e:PropertyField class="gun-field" binding-path="gun" label="Gun Object"/>
</UXML>
3.1.4、定义 USS 文件
创建一个 USS 文件来定义 Inspector 面板的样式。将该文件保存为 custom-editor-uie-style.uss,并将其放置在 Resources 文件夹中。
.slider-row {
flex-direction: row;
justify-content: space-between;
margin-top: 4px;
}
.input-container {
flex-direction: row;
flex-grow: .6;
margin-right: 4px;
}
.player-property {
margin-bottom: 4px;
}
.player-property-label {
flex: 1;
margin-left: 16px;
}
.player-slider {
flex: 3;
margin-right: 4px;
}
.player-property-progress-bar {
margin-left: 16px;
margin-right: 4px;
}
.player-int-field {
min-width: 48px;
}
.gun-field {
justify-content: space-between;
margin-left: 16px;
margin-right: 4px;
margin-top: 6px;
flex-grow: .6;
}
3.2、使用EditorGUILayout直接绘制
在 Unity 中,自定义 Inspector 面板时,可以直接通过编辑器修改脚本变量。虽然这种方式不支持多对象编辑、撤销和 Prefab 覆盖,但它实现起来更加简单直接,适用于一些简单的自定义编辑器需求。
using UnityEditor;
using UnityEngine;
// 自定义编辑器,直接修改脚本变量,不处理多对象编辑、撤销和 Prefab 覆盖
[CustomEditor(typeof(MyPlayer))]
public class MyPlayerEditorAlternative : Editor
{
public override void OnInspectorGUI()
{
MyPlayerAlternative mp = (MyPlayerAlternative)target;
mp.damage = EditorGUILayout.IntSlider("Damage", mp.damage, 0, 100);
ProgressBar(mp.damage / 100.0f, "Damage");
mp.armor = EditorGUILayout.IntSlider("Armor", mp.armor, 0, 100);
ProgressBar(mp.armor / 100.0f, "Armor");
bool allowSceneObjects = !EditorUtility.IsPersistent(target);
mp.gun = (GameObject)EditorGUILayout.ObjectField("Gun Object", mp.gun, typeof(GameObject), allowSceneObjects);
}
// 自定义 GUILayout 进度条
void ProgressBar(float value, string label)
{
// 获取进度条的矩形区域,使用与文本字段相同的边距
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, value, label);
EditorGUILayout.Space();
}
}
3.3、使用 SerializedObject 和 SerializedProperty绘制
使用 SerializedObject 和 SerializedProperty 提供了一种更结构化的方法,带来一些显著的优势:
-
自动管理数据更改:
无需手动调用 EditorUtility.SetDirty,因为 ApplyModifiedProperties 会自动处理数据更改。 -
撤销/重做支持:
自动支持撤销和重做功能,无需额外代码。 -
多对象编辑:
可以轻松实现多个对象的编辑,并自动处理它们的属性同步。 -
数据一致性:
确保数据的一致性和正确性,因为 SerializedObject 和 SerializedProperty 直接与 Unity 的序列化系统交互。
using UnityEditor;
using UnityEngine;
// 自定义编辑器,适用于 MyPlayer 脚本
[CustomEditor(typeof(MyPlayer))]
[CanEditMultipleObjects]
public class MyPlayerEditor : Editor
{
SerializedProperty damageProp;
SerializedProperty armorProp;
SerializedProperty gunProp;
// 在启用编辑器时初始化 SerializedProperties
void OnEnable()
{
// 获取 SerializedProperties
damageProp = serializedObject.FindProperty("damage");
armorProp = serializedObject.FindProperty("armor");
gunProp = serializedObject.FindProperty("gun");
}
// 绘制自定义 Inspector GUI
public override void OnInspectorGUI()
{
// 更新 serializedObject,在 OnInspectorGUI 开头调用
serializedObject.Update();
// 显示自定义 GUI 控件
EditorGUILayout.IntSlider(damageProp, 0, 100, new GUIContent("Damage"));
ProgressBar(damageProp.intValue / 100.0f, "Damage");
EditorGUILayout.IntSlider(armorProp, 0, 100, new GUIContent("Armor"));
ProgressBar(armorProp.intValue / 100.0f, "Armor");
EditorGUILayout.PropertyField(gunProp, new GUIContent("Gun Object"));
// 在 OnInspectorGUI 末尾应用属性更改
serializedObject.ApplyModifiedProperties();
}
// 自定义 GUILayout 进度条
void ProgressBar(float value, string label)
{
// 获取进度条的矩形区域,使用与文本字段相同的边距
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, value, label);
EditorGUILayout.Space();
}
}
每一次跌倒都是一次成长 每一次努力都是一次进步 |
如果您喜欢本博客,请点赞和分享给更多的朋友,让更多人受益。同时,您也可以关注我的博客,以便及时获取最新的更新和文章。
在未来的写作中,我将继续努力,分享更多有趣、实用的内容。再次感谢大家的支持和鼓励,期待与您在下一篇博客再见!