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

相关推荐
五仁烧饼12 小时前
Unity性能优化系列内存篇 - 移动端内存优化
unity·性能优化·游戏引擎
zhchyun20083 天前
# 【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(06)
ui·unity·游戏引擎
Behaviour4 天前
Unity游戏HybridCLR 热更新接入实战
游戏·unity·游戏引擎
LONGZETECH4 天前
新能源汽车动力电池实训教学痛点与虚拟仿真技术解决方案
c语言·3d·unity·架构·汽车·汽车教学软件
王维志4 天前
UiSplineRenderer
unity·游戏引擎
FairGuard手游加固4 天前
iOS 游戏加固技术解析:代码混淆、资源加密、内存保护与重签名检测
安全·游戏·macos·unity·ios·objective-c
UWA5 天前
GPM 2.0 WebGL 监测能力扩容|覆盖 Unity/Cocos/LayaAir 多引擎微信 & 抖音小游戏
unity·微信·性能优化·小游戏·webgl
Behaviour5 天前
Unity 手游网络同步技术
网络·unity·c#·游戏引擎
niuniudengdeng6 天前
Unity手游APK去广告实战:华为渠道包反编译、广告拦截与“网络不佳“弹窗修复全记录
unity·游戏引擎