u3d IMGUI[二] 编辑器扩展

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

一.IMGUI 创建自定义Editor窗口

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

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

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

3.调用EditorWindow.GetWindow创建窗口

4.通过OnGUI绘制窗口内UI

cs 复制代码
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中得默认显示和定制化显示,默认显示的代码:

cs 复制代码
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面板绘制该序列化属性时触发,不参与打包构建

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

cs 复制代码
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.2 **OnGUI的**Rect参数

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

cs 复制代码
    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事件打印:

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

仅在Repaint事件执行代码:

cs 复制代码
    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

cs 复制代码
        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切换控件

不接受事件:

cs 复制代码
GUIUtility.GetControlID(FocusType.Passive)

接受事件:

cs 复制代码
GUIUtility.GetControlID(FocusType.Keyboard)

2.7 EditorGUI.PrefixLabel

绘制一个前缀标签

totalPosition:整个控件的Rect

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

label:OnGUI的参数label

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

cs 复制代码
public static Rect PrefixLabel(Rect totalPosition, int id, GUIContent label)

2.7 EditorGUI.indentLevel

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

cs 复制代码
EditorGUI.indentLevel++;
EditorGUI.indentLevel--;

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

2.8 EditorGUI.PropertyField

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

cs 复制代码
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中如何显示

cs 复制代码
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.");
    }
}
cs 复制代码
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定制化前后的显示对比

cs 复制代码
using System;
using UnityEngine;
[ExecuteInEditMode]
public class LookAtPoint : MonoBehaviour
{
    public Vector3 lookAtPoint = Vector3.zero;

    public void Update()
    {
        transform.LookAt(lookAtPoint);
    }
}

LookAtPoint对应的编辑器脚本:LookAtPointEditor

cs 复制代码
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(LookAtPoint))]
public class LookAtPointEditor : Editor
{
    public override void OnInspectorGUI()
    {

    }
}

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

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

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

通常** **CustomEditor+:Editor+OnInspectorGUI一起使用

cs 复制代码
public override void OnInspectorGUI()

3.2 OnEnable,OnDisable

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

cs 复制代码
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 成员

cs 复制代码
var t = (target as LookAtPoint);

3.4 Editor.serializedObject

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

3.4.1 serializedObject.FindProperty

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

cs 复制代码
public SerializedProperty FindProperty(string propertyPath);

3.4.2 serializedObject.Update

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

cs 复制代码
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;常见写法:

cs 复制代码
        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.1 TreeView

规范:创建一个继承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

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

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

相关推荐
两水先木示9 小时前
【Unity】探索小地图迷雾散开Shader效果(基础版)
unity·游戏引擎
WarrenMondeville16 小时前
AI复刻街头霸王到 Unity
unity·游戏引擎
玖玥拾1 天前
Lua 基础语法(六)xLua Lua 调用 C# 交互
开发语言·unity·c#·lua
xcLeigh1 天前
Unity基础:MonoBehaviour的生命周期——Awake、OnEnable、Start、Update执行顺序
unity·游戏引擎
qq_213157892 天前
其域lcc2丢失部分lod/远处显示空洞的解决方式
unity·lcc2
XR技术研习社2 天前
关于 PICO 串流测试中报错 IndexOutOfRangeException: renderPassIndex 的两个排障方法
unity·ar·xr·vr
想做后端的前端2 天前
Unity · 性能优化:内存管理完全指南:从托管堆到跨桥开销
unity·性能优化·游戏引擎
平行云2 天前
实时云渲染信创架构解析:从GPU池化到全栈适配的技术演进
linux·unity·docker·ue5·webgl·数字孪生·实时云渲染
玖玥拾2 天前
Lua 基础语法(五)Unity xLua基础配置与 C# 访问 Lua
开发语言·unity·c#·lua
玖玥拾3 天前
Lua 基础语法(二)
开发语言·unity·lua