注意:考虑到编辑器扩展的内容比较多,我将编辑器扩展的内容分开,并全部整合放在【unity游戏开发——编辑器扩展】专栏里,感兴趣的小伙伴可以前往逐一查看学习。

前言

1、如何自定义Inspector窗口

我们可以为继承Editor的脚本添加[CustomEditor(typeof(想要自定义Inspector窗口的脚本))]特性,在该脚本中按照一定的规则进行编写,便可为Inspector窗口中的某个脚本自定义窗口布局。

2、SerializedObject和SerializedProperty的作用

SerializedObject 和 SerializedProperty 主要用于在 Unity 编辑器中操作和修改序列化对象的属性。它们通常在自定义编辑器中使用,以创建更灵活、可定制的属性面板。

  • SerializedObject:代表脚本(组件)对象。
  • SerializedProperty:代表脚本(组件)对象中的属性。

一、基础字段处理

1、默认Inspector窗口显示效果

我们随便创建继承MonoBehaviour的脚本

using UnityEngine;

public class TestInspectorMono : MonoBehaviour
{
    public int atk;

    public float def;

    public GameObject obj;
}

挂载脚本,Inspector窗口效果如下
在这里插入图片描述

2、自定义Inspector窗口中的显示效果

下面我们通过Inspector窗口拓展,自定义Inspector窗口中的显示效果

2.1 新增编辑器脚本,并加特性

(1)介绍

通过添加CustomEditor特性,我们就可以为TestInspectorMono脚本自定义Inspector窗口中的显示了

CustomEditor(想要自定义脚本类名的Type)

注意编辑器脚本要放在Editor文件夹下

(2)示例
using UnityEditor;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor1 : Editor
{ 

}

2.2 编辑器脚本声明测试脚本对应变量并获取

(1)介绍

声明对应SerializedProperty序列化属性对象。主要通过它和自定义脚本中的成员进行关联。

  • 可以利用继承Editor后的成员serializedObject中的FindProperty(“成员变量名”)方法关联对应成员。比如:

    SerializedProperty mySerializedProperty;
    mySerializedProperty = serializedObject.FindProperty(“自定义脚本中的成员名”);
    
  • 一般在OnEnable函数中初始化。

(2)示例
using UnityEditor;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor1 : Editor
{
    private SerializedProperty atk;
    private SerializedProperty def;
    private SerializedProperty obj;
    private bool foldOut;

    private void OnEnable()
    {
        // 这样就得到与测试脚本对应的字段 注意传入的字符串自定义脚本中的成员名一致
        atk = serializedObject.FindProperty("atk");
        def = serializedObject.FindProperty("def");
        obj = serializedObject.FindProperty("obj");
    }
}

2.3 重写OnInspectorGUI函数

  • 重写OnInspectorGUI函数。该函数控制了Inspector窗口中显示的内容。只需要在其中重写内容便可以自定义窗口。

    public override void OnInspectorGUI()
    { }
    
  • 注意:其中的逻辑需要包裹在这两句代码之间。这里自定义绘制内容其实就是运用我们之前学习的【EditorGUI】知识。

    serializedObject.Update();// 更新序列化对象
    // 。。。
    serializedObject.ApplyModifiedProperties();// 应用修改
    
  • Editor中的target成员变量,可以得到当前正在被自定义编辑器检视的组件对象

2.4 最终代码

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor1 : Editor
{
    private SerializedProperty atk;
    private SerializedProperty def;
    private SerializedProperty obj;

    private void OnEnable()
    {
        // 这样就得到与测试脚本对应的字段 注意传入的字符串自定义脚本中的成员名一致
        atk = serializedObject.FindProperty("atk");
        def = serializedObject.FindProperty("def");
        obj = serializedObject.FindProperty("obj");
    }

    //重写OnInspectorGUI
    public override void OnInspectorGUI()
    {
        // 更新序列化对象
        serializedObject.Update();

        //修改成整型滑动条
        EditorGUILayout.IntSlider(atk, 0, 100, "攻击力");
        //修改成Float输入框
        def.floatValue = EditorGUILayout.FloatField("防御力", def.floatValue);
        //修改对象关联控件标题
        EditorGUILayout.ObjectField(obj, new GUIContent("敌对对象"));
        //自定义按钮
        if (GUILayout.Button("打印当前对象信息"))
        {
            Debug.Log("组件类型" + target.GetType());
            Debug.Log("组件依附的游戏对象名" + target.name);
        }
		
		// 应用修改
        serializedObject.ApplyModifiedProperties();
    }
}

2.5 最终效果

在这里插入图片描述

二、数组和List字段处理

使用EditorGUILayout.PropertyField会按照属性类型自己去处理控件绘制的逻辑。

EditorGUILayout.PropertyField(SerializedProperty对象, 标题)

1、默认Inspector窗口显示效果

定义一些数组和List字段数据

using System.Collections.Generic;
using UnityEngine;

public class TestInspectorMono : MonoBehaviour
{
    public string[] strs;
    public int[] ints;
    public GameObject[] gameObjects;
    public List<GameObject> listObjs;
}

显示效果
在这里插入图片描述

2、修改Inspector窗口显示数组和List标题

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor : Editor
{
    private SerializedProperty strs;
    private SerializedProperty ints;
    private SerializedProperty gameObjects;
    private SerializedProperty listObjs;

    private void OnEnable()
    {
        // 默认得到的数组和List容量为空
        strs = serializedObject.FindProperty("strs");
        ints = serializedObject.FindProperty("ints");
        gameObjects = serializedObject.FindProperty("gameObjects");
        listObjs = serializedObject.FindProperty("listObjs");
    }

    //重写OnInspectorGUI
    public override void OnInspectorGUI()
    {
        // 更新序列化对象的表示形式
        serializedObject.Update();

        EditorGUILayout.PropertyField(strs, new GUIContent("字符串数组"));
        EditorGUILayout.PropertyField(ints, new GUIContent("整形数组"));
        EditorGUILayout.PropertyField(gameObjects, new GUIContent("游戏对象数组"));
        EditorGUILayout.PropertyField(listObjs, new GUIContent("游戏对象List"));

        serializedObject.ApplyModifiedProperties();
    }
}

效果
在这里插入图片描述

3、自定义Inspector窗口显示数组和List的样式

如果我们不想要Unity默认的绘制方式去显示数组、List相关内容,我们也可以完全自定义布局方式。

3.1 介绍

主要是利用SerializedProperty中数组相关的API来完成自定义

  • arraySize:获取数组或List容量
  • InsertArrayElementAtIndex(索引):为数组在指定索引插入默认元素(容量会变化)
  • DeleteArrayElementAtIndex(索引):为数组在指定索引删除元素(容量会变化)
  • GetArrayElementAtIndex(索引):获取数组中指定索引位置的 SerializedProperty 对象

3.2 实战:自定义List显示

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor : Editor
{
    private SerializedProperty listObjs;
    private int count;//当前容量
    private bool arrayAndListFoldOut;//是否展开

    private void OnEnable()
    {
        // 默认得到的List容量为空
        listObjs = serializedObject.FindProperty("listObjs");
        // 初始化当前容量 否则 每次一开始都是0
        count = listObjs.arraySize;
    }

    //重写OnInspectorGUI
    public override void OnInspectorGUI()
    {
        arrayAndListFoldOut = EditorGUILayout.BeginFoldoutHeaderGroup(arrayAndListFoldOut, "数组和List属性");
        if (arrayAndListFoldOut)
        {
            // 容量设置
            count = EditorGUILayout.IntField("List容量", count);

            // 是否要缩减 移除尾部的内容
            // 从后往前去移除 避免移除不干净
            // 当容量变少时 才会走这的逻辑
            for (int i = listObjs.arraySize - 1; i >= count; i--)
                listObjs.DeleteArrayElementAtIndex(i);

            // 根据容量绘制需要设置的每一个索引位置的对象
            for (int i = 0; i < count; i++)
            {
                // 去判断如果数组或者LIst容量不够 去通过插入的形式去扩容
                if (listObjs.arraySize <= i)
                    listObjs.InsertArrayElementAtIndex(i);

                SerializedProperty indexPro = listObjs.GetArrayElementAtIndex(i);
                EditorGUILayout.ObjectField(indexPro, new GUIContent($"索引{i}"));
            }
        }

        EditorGUILayout.EndFoldoutHeaderGroup();
    }
}

效果
在这里插入图片描述

三、自定义数据字段处理

使用EditorGUILayout.PropertyField会按照属性类型自己去处理控件绘制的逻辑。

EditorGUILayout.PropertyField(SerializedProperty对象, 标题)

1、默认Inspector窗口显示效果

声明自定义数据类变量

using System;
using UnityEngine;

//声明自定义数据类,需要添加序列化特性
[Serializable]
public class TestCustomClass
{
    public int i;
    public float f;
}

public class TestInspectorMono : MonoBehaviour
{
    //声明自定义数据类变量
    public TestCustomClass myCustom;
}

效果
在这里插入图片描述

2、修改Inspector窗口显示自定义数据标题

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor : Editor
{
    private SerializedProperty myCustom;

    private void OnEnable()
    {
        myCustom = serializedObject.FindProperty("myCustom");
    }

    //重写OnInspectorGUI
    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUILayout.PropertyField(myCustom, new GUIContent("我的自定义属性"));

        serializedObject.ApplyModifiedProperties();
    }
}

效果
在这里插入图片描述

3、自定义Inspector窗口显示自定义数据的样式

如果我们不想要Unity默认的绘制方式去显示自定义数据结构类相关内容,我们也可以完全自定义布局方式。

3.1 介绍

主要API:

SerializedProperty.FindPropertyRelative(属性)
serializedObject.FindProperty(属性.子属性)

3.2 实战:自定义显示数据结构类中的变量

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor : Editor
{
    private SerializedProperty myCustom;

    private SerializedProperty myCustomI;
    private SerializedProperty myCustomF;

    private void OnEnable()
    {
        myCustom = serializedObject.FindProperty("myCustom");

        // 以下两种方式二选其一
        // myCustomI = myCustom.FindPropertyRelative("i");
        // myCustomF = myCustom.FindPropertyRelative("f");
        myCustomI = serializedObject.FindProperty("myCustom.i");
        myCustomF = serializedObject.FindProperty("myCustom.f");
    }

    //重写OnInspectorGUI
    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUILayout.PropertyField(myCustom, new GUIContent("我的自定义属性"));
        myCustomI.intValue = EditorGUILayout.IntField("自定义属性中的I", myCustomI.intValue);
        myCustomF.floatValue = EditorGUILayout.FloatField("自定义属性中的F", myCustomF.floatValue);

        serializedObject.ApplyModifiedProperties();
    }
}

效果,两个变量指向的是同一份内存,改动其中一个会实时同步
在这里插入图片描述

四、字典处理

Unity默认是不支持Dictionary在Inspector窗口被显示的。不过幸运的是,ISerializationCallbackReceiver的出现解决了这个问题。我们可以利用两个List(或数组)成员来间接设置Dictionary。

1、ISerializationCallbackReceiver接口

该接口是Unity提供的用于序列化和反序列化时执行自定义逻辑的接口。实现该接口的类能够在对象被序列化到磁盘或从磁盘反序列化时执行一些额外代码。

  • 接口中函数:
    • OnBeforeSerialize: 在对象被序列化之前调用
    • OnAfterDeserialize: 在对象从磁盘反序列化后调用
  • 由于我们需要用两个List存储Dictionary的具体值,相当于字典中的真正内容是存储在两个List中的,所以我们需要在OnBeforeSerialize序列化之前将Dictionary里的数据存入List中进行序列化,在OnAfterDeserialize反序列化之后将List中反序列化出来的数据存储到Dictionary中。

2、实战

2.1 自定义脚本存储读取字典的键值对

在自定义脚本中继承ISerializationCallbackReceiver接口,声明字典和两个序列化的List(存储键值对),在序列化前和反序列化后分别存储读取字典的键值对。

using UnityEngine;
using System.Collections.Generic;

public class TestInspectorMono : MonoBehaviour, ISerializationCallbackReceiver
{
    public Dictionary<int, string> myDic = new Dictionary<int, string>();
    [SerializeField] private List<int> keys = new List<int>();
    [SerializeField] private List<string> values = new List<string>();

    // 序列化前的回调函数
    public void OnBeforeSerialize()
    {
        // 清空键列表和值列表
        keys.Clear();
        values.Clear();

        // 遍历字典,将键和值分别添加到对应的列表中
        foreach (var item in myDic)
        {
            keys.Add(item.Key);
            values.Add(item.Value);
        }
    }

    // 反序列化后的回调函数
    public void OnAfterDeserialize()
    {
        // 清空字典
        myDic.Clear();

        // 遍历键列表和值列表,添加到字典中
        for (int i = 0; i < keys.Count; i++)
        {
            if (!myDic.ContainsKey(keys[i]))
                myDic.Add(keys[i], values[i]);
            else
                // 如果字典中已存在相同的键,发出警告
                Debug.LogWarning("字典Dictionary容器中不允许有相同的键");
        }
    }
}

2.2 利用两个List在Inspector窗口中自定义Dictionary显示

由于我们在Inspector窗口中显示的信息的数据来源是List,因此我们只需要利用List在Inspector窗口中自定义显示即可。

编辑器脚本中声明属性得到两个List,声明字典长度初始化赋值,写字典缩减扩容逻辑。

using UnityEditor;

[CustomEditor(typeof(TestInspectorMono))]
public class TestInspectorMonoEditor : Editor
{
    private SerializedProperty keys; // 保存字典键的序列化属性
    private SerializedProperty values; // 保存字典值的序列化属性
    private int dicCount; // 字典容量

    private void OnEnable()
    {
        // 查找字典键和值的序列化属性
        keys = serializedObject.FindProperty("keys");
        values = serializedObject.FindProperty("values");

        // 获取字典当前容量
        dicCount = keys.arraySize;
    }

    //重写OnInspectorGUI
    public override void OnInspectorGUI()
    {
        // 更新序列化对象
        serializedObject.Update();

        // 显示字典容量的字段
        dicCount = EditorGUILayout.IntField("字典容量", dicCount);

        // 当容量减少时,删除多余的键值对
        for (int i = keys.arraySize - 1; i >= dicCount; i--)
        {
            keys.DeleteArrayElementAtIndex(i);
            values.DeleteArrayElementAtIndex(i);
        }

        // 遍历字典
        for (int i = 0; i < dicCount; i++)
        {
            // 如果容量不够,扩容
            if (keys.arraySize <= i)
            {
                keys.InsertArrayElementAtIndex(i);
                values.InsertArrayElementAtIndex(i);
            }

            // 获取当前键值对的序列化属性
            SerializedProperty indexKey = keys.GetArrayElementAtIndex(i);
            SerializedProperty indexValue = values.GetArrayElementAtIndex(i);

            // 显示键值对的编辑界面
            EditorGUILayout.BeginHorizontal();
            indexKey.intValue = EditorGUILayout.IntField("字典的键", indexKey.intValue);
            indexValue.stringValue = EditorGUILayout.TextField("字典的值", indexValue.stringValue);
            EditorGUILayout.EndHorizontal();
        }

        // 应用修改
        serializedObject.ApplyModifiedProperties();
    }
}

效果
在这里插入图片描述

2.3 测试数据是否存储成功

我们可以打印字典数据

private void Start()
{
    foreach (var item in myDic)
    {
        print($"Dic:{item.Key} - {item.Value}");
    }
}

运行游戏效果,能正常打印说明数据存储到字典成功
在这里插入图片描述


专栏推荐

地址
【unity游戏开发入门到精通——C#篇】
【unity游戏开发入门到精通——unity通用篇】
【unity游戏开发入门到精通——unity3D篇】
【unity游戏开发入门到精通——unity2D篇】
【unity实战】
【制作100个Unity游戏】
【推荐100个unity插件】
【实现100个unity特效】
【unity框架/工具集开发】
【unity游戏开发——模型篇】
【unity游戏开发——InputSystem】
【unity游戏开发——Animator动画】
【unity游戏开发——UGUI】
【unity游戏开发——联网篇】
【unity游戏开发——优化篇】
【unity游戏开发——shader篇】
【unity游戏开发——编辑器扩展】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述

Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐