☰
u3d IMGUI[二] 编辑器扩展
2026/10/24 18:00:23 网站建设 项目流程

本文意在精简和整合IMGUI的官方文档的编辑器扩展部分,抽出重点;我的体会是编辑器扩展处处都是规范,学习的过程就是掌握规范,API,DEMO

一.IMGUI 创建自定义Editor窗口

可通过以下官方定义的固定步骤,实现自定义编辑器窗口

1.实现一个继承EditorWindow的脚本,并放在Editor目录下

2.通过特性实现一个菜单

3.调用EditorWindow.GetWindow创建窗口

4.通过OnGUI绘制窗口内UI

using UnityEngine; using UnityEditor; using System.Collections; public class IMGUIEdt : EditorWindow { string myString = "Hello World"; bool groupEnabled; bool myBool = true; float myFloat = 1.23f; [MenuItem("Window/My Window")] public static void ShowWindow() { EditorWindow.GetWindow(typeof(IMGUIEdt)); } void OnGUI() { GUILayout.Label ("Base Settings", EditorStyles.boldLabel); myString = EditorGUILayout.TextField ("Text Field", myString); groupEnabled = EditorGUILayout.BeginToggleGroup ("Optional Settings", groupEnabled); myBool = EditorGUILayout.Toggle ("Toggle", myBool); myFloat = EditorGUILayout.Slider ("Slider", myFloat, -3, 3); EditorGUILayout.EndToggleGroup (); } }

参考:使用 IMGUI 扩展 Editor

二.在Inspector定制指定类型在所有 Inspector 中的显示

下面DEMO对比一个类在Inspector中得默认显示和定制化显示,默认显示的代码:

using System; using UnityEngine; public enum IngredientUnit { Spoon, Cup, Bowl, Piece } //用于让自定义类在Inspector中显示 [Serializable] public class Ingredient { public string name; public int amount = 1; public IngredientUnit unit; } public class Recipe : MonoBehaviour { public Ingredient potionResult; public Ingredient[] potionIngredients; }

2.1 特性[CustomPropertyDrawer]和继承PropertyDrawer

通过特性CustomPropertyDrawer和继承PropertyDrawer,unity显示Ingredient时将不再使用默认绘制方式,而是调用IngredientDrawer的OnGUI方法(约定写法)

注意IngredientDrawer的OnGUI 和MonoBehaviour的OnGUI完全不同

MonoBehaviour的OnGUI无参,是Unity的事件函数,每帧调用,参与打包构建;

IngredientDrawer的OnGUI有参(Rect, SerializedProperty, GUIContent),是编辑器扩展专用方法,仅在Inspector面板绘制该序列化属性时触发,不参与打包构建

定制化显示代码(这段代码行数不多,但是信息量较大):

using UnityEditor; using UnityEngine; //特性CustomPropertyDrawer:约定写法 [CustomPropertyDrawer(typeof(Ingredient))] public class IngredientDrawer : PropertyDrawer//继承PropertyDrawer:约定写法 { //OnGUI函数原型:约定写法, 定义在父类PropertyDrawer中 public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var a = property.type; if (Event.current.type == EventType.Repaint) { Debug.Log(label.text+"," + position.x +","+ position.y+"," + position.width+","+ position.height); // EditorGUI.DrawRect(position, new Color(1, 0, 0, 0.3f)); } EditorGUI.BeginProperty(position, label, property); //前缀标签 position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); //获取缩进 var indent = EditorGUI.indentLevel; //设置缩进 EditorGUI.indentLevel = 0; //获取矩形区域 var amountRect = new Rect(position.x, position.y, 30, position.height); var unitRect = new Rect(position.x + 35, position.y, 50, position.height); var nameRect = new Rect(position.x + 90, position.y, position.width - 90, position.height); //绘制字段 EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("amount"), GUIContent.none); EditorGUI.PropertyField(unitRect, property.FindPropertyRelative("unit"), GUIContent.none); EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none); //还原缩进 EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } }

默认显示:

定制化显示:

2.2OnGUI的Rect参数

通过选中Ingredient类所在脚本所在的对象,会执行OnGUI方法,加上打印信息和绘制position代表的矩形区域,来看效果

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { Debug.Log(label.text+","+Event.current.type+"," + position.x +","+ position.y+"," + position.width+","+ position.height); EditorGUI.DrawRect(position, new Color(1, 0, 0, 0.3f)); }

这里给出结论:

OnGUI会多次执行,其中position.x和position.y为0的时候,是处于计算布局阶段(Layout);当处于repaint阶段时,Rect所代表的矩形才是真正绘制区域

2.3 通过EventType过滤阶段

无论MonoBehaviour的无参OnGUI或 PropertyDrawer的有参OnGUI,都可能因为各种事件执行。若需要将逻辑放在需要的事件中,可用if (Event.current.type == EventType.XX)判断,这么做还可优化性能

仅在MouseDown事件打印:

void OnGUI() { if (Event.current.type == EventType.MouseDown) { Debug.Log("Mouse Down."); } }

仅在Repaint事件执行代码:

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (Event.current.type == EventType.Repaint) { EditorGUI.DrawRect(position, new Color(1, 0, 0, 0.3f)); } }

参考:EventType

2.4 EditorGUI.BeginProperty

官方说明:BeginProperty和EndProperty自动处理默认标签,预制件覆盖的粗体字体,恢复到预制件右键菜单,如果多对象编辑时属性值不同,则将showMixedValue设为true

自己理解:BeginProperty是让Inspector自定义控件获得Inspector原生控件能力(最主要是右键菜单)。

实际开发中,先无需加BeginProperty开发第一版,开发完后哪些控件缺原生功能,再调用BeginProperty往上加

编辑器扩展创建的控件,若支持右击菜单,要反应过来,其内部调用了BeginProperty

EditorGUI.BeginProperty(position, label, property); //... EditorGUI.EndProperty();

对于判断什么情况下要调用BeginProperty至关重要,首先看下源码,BeginProperty内部调用BeginPropertyInternal,在EditorGUI中搜索这两个函数有若干处调用,也就是有的控件内部会调用BeginProperty,无需重复调用

下面列出两种需要加BeginProperty的情况:

1.创建控件的函数签名中没有SerializedProperty参数

调用BeginProperty时需要传SerializedProperty类型参数,因此这种情况函数内部由于没有获取SerializedProperty参数,一定不会调用BeginProperty;

反之,若函数签名中有 SerializedProperty参数,其内部往往会调用BeginProperty。

2.多个控件放一行,共享一个主标签,这时无需看控件的函数签名了,因为需要BeginProperty来修饰主标签,以及让其中控件被视作整体

2.5 参数SerializedProperty

OnGUI的property的类型是SerializedProperty,它是上下文对象。可将其理解为序列化句柄或序列化访问器,如果翻译成序列化属性容易和C#的get/set属性概念混淆,两者没有关联。

类型名称:type

类型枚举:propertyType

字段名:name

获取子属性(耗性能):

SerializedProperty levelProp = property.FindPropertyRelative("level");

修改值后生效:property.serializedObject.ApplyModifiedProperties();

2.6 GUIUtility.GetControlID

获取unity分配的控件唯一id,用来控制该控件是否接受某些事件,如键盘tab切换控件

不接受事件:

GUIUtility.GetControlID(FocusType.Passive)

接受事件:

GUIUtility.GetControlID(FocusType.Keyboard)

2.7 EditorGUI.PrefixLabel

绘制一个前缀标签

totalPosition:整个控件的Rect

id:通过GUIUtility.GetControlID(FocusType.Passive)获取的控件id

label:OnGUI的参数label

返回值:控件剩余可用区域

public static Rect PrefixLabel(Rect totalPosition, int id, GUIContent label)

2.7 EditorGUI.indentLevel

用来控制缩进,常见写法:

EditorGUI.indentLevel++; EditorGUI.indentLevel--; //EditorGUI.indentLevel是全局静态属性,所以在修改它时必须保存和恢复 var indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; //code here EditorGUI.indentLevel = indent;

2.8 EditorGUI.PropertyField

用于在编辑器中为SerializedProperty创建一个字段。

var amountRect = new Rect(position.x, position.y, 30, position.height); EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("amount"), GUIContent.none);

2.9 PropertyAttribute类

自定义特性继承PropertyAttribute后,可以与ProperyDrawer的子类关联起来,用于控制脚本变量在Inspector中如何显示

using UnityEditor; using UnityEngine; public class MyRangeAttribute : PropertyAttribute { public float min; public float max; public MyRangeAttribute(float min, float max) { this.min = min; this.max = max; } } [CustomPropertyDrawer(typeof(MyRangeAttribute))] public class RangeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { MyRangeAttribute range = (MyRangeAttribute)attribute; if (property.propertyType == SerializedPropertyType.Float) EditorGUI.Slider(position, property, range.min, range.max, label); else if (property.propertyType == SerializedPropertyType.Integer) EditorGUI.IntSlider(position, property, (int) range.min, (int) range.max, label); else EditorGUI.LabelField(position, label.text, "Use MyRange with float or int."); } }
using System; using UnityEngine; public class Recipe : MonoBehaviour { [Range(0f,10f)] public float speed = 0f; [MyRange(0f,10f)] public int age = 0; [MyRange(0f,10f)] public bool isNew = false; }

三.在Inspector中自定义整个组件

3.1 定制组件基础

下面demo演示脚本LookAtPoint定制化前后的显示对比

using System; using UnityEngine; [ExecuteInEditMode] public class LookAtPoint : MonoBehaviour { public Vector3 lookAtPoint = Vector3.zero; public void Update() { transform.LookAt(lookAtPoint); } }

LookAtPoint对应的编辑器脚本:LookAtPointEditor

using UnityEngine; using UnityEditor; [CustomEditor(typeof(LookAtPoint))] public class LookAtPointEditor : Editor { public override void OnInspectorGUI() { } }

特性CustomEditor用于绑定编辑器类和MonoBehaviour

从Editor类继承表示这个类用于定制inspector

OnInspectorGUI:固定写法,注意要加ovveride:

通常[CustomEditor]+:Editor+OnInspectorGUI一起使用

public override void OnInspectorGUI()

前

后

3.2 OnEnable,OnDisable

接下来看一个更复杂的demo,选中LookAtPoint所在的gameObject时执行LookAtPointEditor的OnEnable,取消选中该gameObject,执行OnDisable

using UnityEngine; using UnityEditor; [CustomEditor(typeof(LookAtPoint))] [CanEditMultipleObjects] public class LookAtPointEditor : Editor { SerializedProperty lookAtPoint; void OnEnable() { lookAtPoint = serializedObject.FindProperty("lookAtPoint"); Debug.Log("#OnEnable"); } void OnDisable() { Debug.Log("#OnDisable"); } public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.PropertyField(lookAtPoint); if (lookAtPoint.vector3Value.y > (target as LookAtPoint).transform.position.y) { EditorGUILayout.LabelField("(Above this object)"); } if (lookAtPoint.vector3Value.y < (target as LookAtPoint).transform.position.y) { EditorGUILayout.LabelField("(Below this object)"); } serializedObject.ApplyModifiedProperties(); } public void OnSceneGUI() { var t = (target as LookAtPoint); EditorGUI.BeginChangeCheck(); Vector3 pos = Handles.PositionHandle(t.lookAtPoint, Quaternion.identity); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(target, "Move point"); t.lookAtPoint = pos; t.Update(); } } }

3.3 Editor.target

target是[CustomEditor(typeof(A))]中A的对象引用,使用时需要进行类型转换,之后可以访问类A中的public 成员

var t = (target as LookAtPoint);

3.4 Editor.serializedObject

SerializedObject是用来读写对象(MonoBehaviour)可序列化字段的工具类,通过SerializedObject修改字段会自动支持撤销操作、显示出Scene的*表示出现改动、发生预制体覆盖时Inspector中会正确显示。

3.4.1 serializedObject.FindProperty

通过属性名称获取SerializedProperty类型的序列化属性

public SerializedProperty FindProperty(string propertyPath);

3.4.2 serializedObject.Update

为了同步SerializedProperty的数据,来看下面这个demo(将官方demo简化)

public class SerializeObjectUpdateMB : MonoBehaviour { public int m_Field = 1; [MenuItem("Example/SerializedObject Update (MonoBehaviour)")] static void UpdateExample() { var monoBehaviour = FindObjectOfType<SerializeObjectUpdateMB>(); if (monoBehaviour == null) { Debug.LogError("No SerializeObjectUpdateMB component found in the scene!"); return; } using (var serializedObject = new SerializedObject(monoBehaviour)) { SerializedProperty sp = serializedObject.FindProperty("m_Field"); monoBehaviour.m_Field = 5; Debug.Log("before Update:"+sp.intValue); serializedObject.Update(); Debug.Log("after Update:"+sp.intValue); } } }

常见做法是在OnInspectorGUI下第一行加上serializedObject.Update();

但是实测不加未能出现不同步的情况(在inspector中改值和通过代码改值)

3.4.3 serializedObject.ApplyModifiedProperties

通过代码或inspector改的序列化属性需要调用ApplyModifiedProperties才会反馈在inspector上

3.5 OnSceneGUI

负责在Scene视图中绘制交互元素,调用机制和OnInspectorGUI一样,选中关联的gameObject时调用,选中其他gameObject时停止调用

3.6 EditorGUI.BeginChangeCheck和EditorGUI.EndChangeCheck

BeginChangeCheck与EndChangeCheck成对使用,用来检测他们之间的GUI的状态变化,若发生变化EndChangeCheck返回true;常见写法:

EditorGUI.BeginChangeCheck(); //GUI code here if (EditorGUI.EndChangeCheck()) { Debug.Log("** EndChangeCheck true"); }

3.7 Handles.PositionHandle

在一个点绘制一个位置控制控件

Handles类用于在Scene视图绘制3D GUI控件

3.8 Undo.RecordObject

记录 RecordObject 函数之后对对象所做的任何更改,以便撤销。注意必须把Undo.RecordObject放在修改对象代码的前面

3.9 特性[ExecuteInEditMode]

加了特性[ExecuteInEditMode]的MonoBehaviour在编辑模式下也会执行

3.10 特性[CanEditMultipleObjects]

[CanEditMultipleObjects]可以选定多个挂了相同脚本的对象并编译,放在继承Editor脚本上面

四.创建树形UI

unity提供了编辑器中用于创建树形控件的类TreeView、TreeViewState、TreeViewIte

4.1TreeView

规范:创建一个继承TreeView的类SimpleTreeView,构造函数中要调用父类的1参构造函数:base(treeViewState),不加的化会报错,因为TreeView没有无参构造函数

构造函数中调用Reload:固定写法,不然BuildRoot不会调用;每次调用Reload都会调用BuildRoot一次

BuildRoot:TreeView中的抽象函数,SimpleTreeView必须实现,在此创建树形结构

SetupParentsAndChildrenFromDepths:用已设置的顺序和深度值来初始化所有行的通用方法

OnGUI:TreeView创建完成后需要调用OnGUI(Rect rect)在rect区域显示TreeView

4.2 TreeViewItem

TreeViewItem包含有关单个项的数据,代表一行;TreeView 有一个隐藏的根TreeViewItem,

TreeViewItem必须以唯一的整数 ID(用于查找项、选择状态、展开状态)进行构造。

depth属性:表示视觉缩进

4.3 TreeViewState

TreeViewState 包含 TreeView 的可序列化状态信息,可在EditorWindow中持有,并当作参数传给TreeView的构造函数

demo:

SimpleTreeView.cs

using UnityEditor.IMGUI.Controls; using System.Collections.Generic; public class SimpleTreeView : TreeView { public SimpleTreeView(TreeViewState treeViewState): base(treeViewState) { Reload(); } protected override TreeViewItem BuildRoot () { var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"}; var allItems = new List<TreeViewItem> { new TreeViewItem {id = 1, depth = 0, displayName = "Animals"}, new TreeViewItem {id = 2, depth = 1, displayName = "Mammals"}, new TreeViewItem {id = 3, depth = 2, displayName = "Tiger"}, new TreeViewItem {id = 4, depth = 2, displayName = "Elephant"}, new TreeViewItem {id = 5, depth = 2, displayName = "Okapi"}, new TreeViewItem {id = 6, depth = 2, displayName = "Armadillo"}, new TreeViewItem {id = 7, depth = 1, displayName = "Reptiles"}, new TreeViewItem {id = 8, depth = 2, displayName = "Crocodile"}, new TreeViewItem {id = 9, depth = 2, displayName = "Lizard"}, }; SetupParentsAndChildrenFromDepths (root, allItems); return root; } }

SimpleTreeViewWindow.cs

using System.Collections.Generic; using UnityEngine; using UnityEditor.IMGUI.Controls; using UnityEditor; class SimpleTreeViewWindow : EditorWindow { [SerializeField] TreeViewState m_TreeViewState; SimpleTreeView m_SimpleTreeView; void OnEnable() { if (m_TreeViewState == null) m_TreeViewState = new TreeViewState (); m_SimpleTreeView = new SimpleTreeView(m_TreeViewState); } void OnGUI() { m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height)); } [MenuItem ("TreeView Examples/Simple Tree Window")] static void ShowWindow () { var window = GetWindow<SimpleTreeViewWindow> (); window.titleContent = new GUIContent ("My Window"); window.Show (); } }

五. IMGUI用于编辑器扩展的类

EditorGUI

EditorGUILayout

EditorStyles

SerializedObject

SerializedProperty

GUIContent

PropertyAttribute

PropertyDrawer

Editor

EditorWindow

Undo

Handles

TreeView

TreeViewState

TreeViewItem

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询