Unity-编辑器扩展(Editor)

1. 基本介绍

1. MenuItem
  1. 使用 MenuItem 特性,让静态函数作为一个菜单栏功能

[MenuItem(“Tools/mytest”)]

  1. 第三个参数层级,可控制显示的顺序。不同级别的参数最小值和最大值之间最小为11
  2. 添加快捷键
  • %-CTRL
  • # -Shift
  • & -Alt

[MenuItem( “Tools/New Option %#a” )] 即Alt+Shift+A
[MenuItem( “Tools/Item2 _g” )] 即G

  1. 特殊路径
  • Assets/[] -添加到“Assets”菜单下,同时也显示在右键单击项目视图时弹出的菜单中。
  • Asset/Create/[] - 添加在“Assets ->Create”子菜单中。
  • CONTEXT/[任意组件名]/[] - 菜单项将出现在给定组件的右键单击菜单中。
[MenuItem("CONTEXT/Rigidbody/Do Something")]
public static void DoSomething (MenuCommand command) {
	Rigidbody body= command.context  as  Rigidbody ;
	body.mass =  5 ;
}
  • GameObject/[] 菜单项会出现在层级的右键菜单中。但你需要给定一个层级 < 50

[MenuItem(“GameObject/mytest2”, false, 12)]

  1. 限定
    通过第二个参数来限定该按钮是否启用
// 效果
[MenuItem("Assets/SetMass", false, 12)]
static void SetMass()
{
    Rigidbody rigidbody = Selection.gameObjects[0].GetComponent<Rigidbody>();
    rigidbody.mass = 5;

}

// 约束
[MenuItem("Assets/SetMass", true)]
static bool SetMassOption()
{
    return Selection.gameObjects[0].GetComponent<Rigidbody>() != null;
}
2. AddComponentMenu

添加组件到“添加组件”菜单下。

3. ContextMenu、ContextMenuItem

在继承MonoBehaviour时:

特性描述
ContextMenu添加到自身的右键菜单中
ContextMenuItem添加到菜单列表
public class MyObject : MonoBehaviour
{
    [ContextMenuItem("+1", "AddValue")]
    public int value;
    public string data;

    [ContextMenu("重置")]
    void ResetData()
    {
        value = 0;
        data = "";
    }

    void AddValue()
    {
        value++;
    }
}
4. ScriptableWizard

继承该类可生成一个点击后关闭并重置的面板,可进行方便的对象创建修改操作

using UnityEngine;
using UnityEditor;

public class MyObjectEditor : ScriptableWizard
{
    // 参数
    public string changeName = "未命名";

    // 创建按钮到编辑器,并设置按钮名称以及标题
    [MenuItem("Tools/CreateWizard")]
    public static void CreateWizard()
    {
        // 一个点击后关闭并重置的面板
        DisplayWizard<MyObjectEditor>("统一修改", "修改", "第二个按钮");
    }

    // 点击后关闭界面并执行
    private void OnWizardCreate()
    {
        // 修改选取对象的名字
        foreach (var obj in Selection.gameObjects)
        {
            // 操作记录到unity记录中,以便ctrl+z撤回
            Undo.RecordObject(obj, "修改名字");
            obj.name = changeName;
        }

        // 在界面显示提示信息,仅在未关闭时显示
        ShowNotification(new GUIContent(Selection.gameObjects.Length + "个对象被修改了"));
    }

    // 点击另一个按钮,这不会关闭界面
    private void OnWizardOtherButton() => OnWizardCreate();

    // 仅在初始化和值改变时变化
    private void OnWizardUpdate() => HelpInfor();

    // 选中物体时调用
    private void OnSelectionChange() => HelpInfor();

    // 用于提示信息
    void HelpInfor()
    {
        if (Selection.gameObjects.Length == 0)
        {
            // 错误信息
            errorString = "请选择一个对象!";
            helpString = "";
        }
        else
        {
            // 帮助信息
            errorString = "";
            helpString = "您选择了" + Selection.gameObjects.Length + "个对象";
        }
    }
}
5. [UnityEditor.InitializeOnLoadMethod]

每次Unity编译都会运行一次

2. 编译器绘图

1. 绘图方法
  1. GUILayout
方法描述
Label写入一个文本
BeginVertical开始垂直绘图
BeginHorizontal开始水平绘图
BeginScrollView开始滚动视图
Button按钮控件,放在IF语句中判断点击
  1. EditorGUILayout
方法描述
TextField文本框
EnumPopup枚举选项
IntField整数选项
ObjectField(object, typeof, false)一个Unity对象
PropertyField一个SerializedObject类中的对象
2. 示例:窗口形式

主面板

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class AudioWindowEditor : EditorWindow
{
    static SaveData data;
    string path = "Assets/Resources/MainData.asset";
    string text;
    string search = "";

    static GUIStyle textStyle;
    static AudioWindowEditor window;

    [MenuItem("Tools/test")]
    public static void CreateWindow()
    {
        // 固定大小窗口
        // Rect rect = new Rect(0, 0, 800, 600);
        // AudioWindowEditor window = GetWindowWithRect(typeof(AudioWindowEditor), rect) as AudioWindowEditor;
        if (window == null)
        {
            window = GetWindow<AudioWindowEditor>("数据管理");
            textStyle = new GUIStyle("HeaderLabel");
            textStyle.fontSize = 24;
        }
        window.Show();
    }

    /// <summary>
    /// 每秒调用十次
    /// </summary>
    private void OnInspectorUpdate()
    {
    }

    private void OnGUI()
    {
        // 设置标题和其显示的值,将其值返回
        // text = EditorGUILayout.TextField("输入文字", text);

        if (data == null) data = AssetDatabase.LoadAssetAtPath<SaveData>(path);
        if (data == null)
        {
        	// new SaveData()可替换为 CreateInstance<SaveData>()
            AssetDatabase.CreateAsset(new SaveData(), path);
            AssetDatabase.SaveAssets();
            data = AssetDatabase.LoadAssetAtPath<SaveData>(path);
        }
        var settings = new SerializedObject(data);

        GUI.skin.label.fontSize = 16;
        GUILayout.Label("基本内容");
        EditorGUILayout.PropertyField(settings.FindProperty("key"), new GUIContent("名称"));

        // 第一栏
        // GUILayout.BeginHorizontal("Wizard Box");
        search = EditorGUILayout.TextField("", search, "SearchTextField");
        // GUILayout.EndHorizontal();

        // 第二栏
        GUILayout.BeginVertical("Wizard Box", GUILayout.MaxHeight(1));
        GUILayout.BeginHorizontal(new GUIStyle("HelpBox")
        {
            padding = new RectOffset(10, 0, 4, 4)
        });
        GUILayout.Label("名称", new GUIStyle("SettingsHeader")
        {
            fontSize = 16,
            alignment = TextAnchor.MiddleLeft,
        });
        GUILayout.Label("值", new GUIStyle("SettingsHeader")
        {
            fontSize = 16,
            alignment = TextAnchor.MiddleLeft,
        });
        GUILayout.EndHorizontal();
        KeyValue removeOne = null;
        KeyValue moveUp = null;
        KeyValue moveDown = null;
        // 这其实是错误的写法,不应该直接操纵数据源,而是通过settings.FindProperty("datas")arraySize和settings.FindProperty("datas").GetArrayElementAtIndex(i)
        for (int i = 0; i < data.keyvalues.Count; i++)
        {
            var dataSlot = data.keyvalues[i];
            if (dataSlot.key == null) dataSlot.key = "";
            if (dataSlot.key.ToLower().Contains(search.ToLower()))
            {
                GUILayout.Space(2);
                GUILayout.BeginHorizontal("HelpBox");
                dataSlot.key = EditorGUILayout.TextField(dataSlot.key);
                dataSlot.value = (GameObject)EditorGUILayout.ObjectField(dataSlot.value, typeof(GameObject), false);
                if (i != 0)
                {
                    if (GUILayout.Button("↑", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18))) moveUp = dataSlot;
                }
                else GUILayout.Button("", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18));
                if (i != data.keyvalues.Count - 1)
                {
                    if (GUILayout.Button("↓", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18))) moveDown = dataSlot;
                }
                else GUILayout.Button("", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18));
                if (GUILayout.Button("-", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18))) removeOne = dataSlot;
                GUILayout.EndHorizontal();
            }
        }
        if (removeOne != null) data.keyvalues.Remove(removeOne);
        if (moveUp != null)
        {
            int index = -1;
            for (int i = 0; i < data.keyvalues.Count; i++) if (data.keyvalues[i] == moveUp) index = i;
            if (index != -1)
            {
                var move = moveUp;
                data.keyvalues.Remove(moveUp);
                data.keyvalues.Insert(index - 1, move);
            }
        }
        if (moveDown != null)
        {
            int index = -1;
            for (int i = 0; i < data.keyvalues.Count; i++) if (data.keyvalues[i] == moveDown) index = i;
            if (index != -1)
            {
                var move = moveDown;
                data.keyvalues.Remove(moveDown);
                data.keyvalues.Insert(index + 1, move);
            }
        }
        if (GUILayout.Button("+"))
        {
            data.keyvalues.Add(new KeyValue());
        }
        GUILayout.EndVertical();

        // 应用修改
        settings.ApplyModifiedPropertiesWithoutUndo();
    }
    
	// 修改asset文件
	[MenuItem("Tools/CardDataArraysLoad")]
    public static void CardDataArraysLoad()
    {
        string path = "Assets/Configs/CardDataArrayConfig.asset";
        CardDataArrayConfig config = AssetDatabase.LoadAssetAtPath<CardDataArrayConfig>(path);

        config.baseCardDatas = new CardData[baseCardDatas.Count];

        EditorUtility.SetDirty(config);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
    }
}

数据类

using UnityEngine;
using System.Collections.Generic;

public class SaveData : ScriptableObject
{
    public string key;
    public List<KeyValue> keyvalues;
}

[System.Serializable]
public class KeyValue
{
    public string key;
    public GameObject value;
}

3. 示例:项目设置中添加项形式
using UnityEditor;
using UnityEngine;

public class MainDataEditor
{
    static MainData data;
    [SettingsProvider]
    public static SettingsProvider MyCustom()
    {
        string path = "Assets/Resources/Data/MainData.asset";

        var provider = new SettingsProvider("MainData", SettingsScope.Project)
        {
            // 默认情况下,如果未提供标签,则路径的最后一个标记将用作显示名称。
            label = "主线资源库",
            // 填充搜索关键字
            // 创建SettingsProvider并将其绘图(IMGUI)功能初始化
            guiHandler = (searchContext) =>
            {
                if (data == null) data = AssetDatabase.LoadAssetAtPath<MainData>(path);
                if (data == null)
                {
        			// new MainData()可替换为 CreateInstance<MainData>()
                    AssetDatabase.CreateAsset(new MainData(), path);
                    AssetDatabase.SaveAssets();
                    data = AssetDatabase.LoadAssetAtPath<MainData>(path);
                }
                var settings = new SerializedObject(data);

                GUI.skin.label.fontSize = 16;
                GUILayout.Label("基本扩展");
                EditorGUILayout.PropertyField(settings.FindProperty("blood"), new GUIContent("溅血特效"));
                EditorGUILayout.PropertyField(settings.FindProperty("spark"), new GUIContent("火星特效"));
                EditorGUILayout.PropertyField(settings.FindProperty("sharp"), new GUIContent("锋利特效"));

                // 应用修改
                settings.ApplyModifiedPropertiesWithoutUndo();
            }
        };
        return provider;
    }
}

数据类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

[CreateAssetMenu(fileName = "MainData", menuName = "Data/MainData", order = 1)]
public class MainData : ScriptableObject
{
    /// <summary>
    /// 溅血特效
    /// </summary>
    public GameObject blood;
    /// <summary>
    /// 火星特效
    /// </summary>
    public GameObject spark;
    /// <summary>
    /// 锋利特效
    /// </summary>
    public GameObject sharp;
    /// <summary>
    /// 结束的线
    /// </summary>
    public GameObject endLine;
    /// <summary>
    /// 结束光环
    /// </summary>
    public GameObject endNova;
    /// <summary>
    /// 孔明灯
    /// </summary>
    public GameObject KongMingLight;
    /// <summary>
    /// 枪线
    /// </summary>
    public GameObject weaponLine;
    public TileBase[] tileBases;
    public AudioClip[] audioClips;
    /// <summary>
    /// 背包
    /// </summary>
    public Bag bag;
    /// <summary>
    /// 提示信息
    /// </summary>
    public Infor infor;
    public Dialog dialog;
    /// <summary>
    /// 失败面板
    /// </summary>
    public Fail failPanel;
    public Fail winPanel;
    public LoadScene loadScenePanel;
    public Sprite[] move_Sprs;
    public WeaponData[] weapons;
    /// <summary>
    /// 保存值
    /// </summary>
    public SaveData saveData;
    public Player player;
    /// <summary>
    /// 物品列表
    /// </summary>
    public Good[] goods;
}

3. 实例化自定义-PropertyDrawer

数据类

using System;
using UnityEngine;

[Serializable]
public class MyCustomVariable
{
    public string name;
    public int key;
    public float value;
    public bool flag;
    public KeyCode input;
    public Color color;
    public GameObject obj;
    public Vector3 dir;
}

使用PropertyDrawer实例化到一行中

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

//给MyCustomVariable指定编译器实例化渲染方式
[CustomPropertyDrawer(typeof(MyCustomVariable))]
public class MyCustomVariableDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        //重载BeginProperty
        EditorGUI.BeginProperty(position,label,property);  

        //绘制单元格头的标签名,不需要可以去掉
        position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
        
        //Unity默认的每个属性字段都会占用一行,我们这里希望一条自定义Property占一行
        //要是实现这个要求我们分三步: 1. 取消缩进  2. 设置PropertyField 3.还原缩进
        
        //不要缩进子字段,由我们自定义间距
        var indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;

        //计算要用到的属性显示rect   Rect(x,y,width,height)x,y是左顶点
        var nameRect = new Rect(position.x, position.y, 50, position.height);
        var keyRect = new Rect(position.x + 60, position.y, 40, position.height);
        var flagRect = new Rect(position.x + 110, position.y, 20, position.height);
        var inputRect = new Rect(position.x + 140, position.y, 60, position.height);
        var colorRect = new Rect(position.x + 210, position.y, 50, position.height);
        var dirRect = new Rect(position.x + 270, position.y, 100, position.height);
        var objRect = new Rect(position.x + 380, position.y, position.width - 390, position.height);

        //绘制字段 - 将GUIContent.none传递给每个字段,以便绘制它们而不是用标签
        //属性绘制器不支持布局来创建GUI;
        //因此,您必须使用的类是EditorGUI而不是EditorGUILayout。这就是为什么要给每个属性指定Rect
        EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none);
        EditorGUI.PropertyField(keyRect, property.FindPropertyRelative("key"), GUIContent.none);
        EditorGUI.PropertyField(flagRect, property.FindPropertyRelative("flag"), GUIContent.none);
        EditorGUI.PropertyField(inputRect, property.FindPropertyRelative("input"), GUIContent.none);
        EditorGUI.PropertyField(colorRect, property.FindPropertyRelative("color"), GUIContent.none);
        EditorGUI.PropertyField(dirRect, property.FindPropertyRelative("dir"), GUIContent.none);
        EditorGUI.PropertyField(objRect, property.FindPropertyRelative("obj"), GUIContent.none);
        //将缩进还原
        EditorGUI.indentLevel = indent;

        EditorGUI.EndProperty();
    }

    //设定单元格高度,默认返回为默认高度
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return base.GetPropertyHeight(property, label);
    }
}
列表元素的编译器扩展-PropertyDrawer
using System;
using Cinemachine.Editor;
using Framework.GameObjectBinder.Views.Variables;
using UnityEditor;
using UnityEditor.Rendering;
using UnityEditorInternal;
using UnityEngine;

[CustomPropertyDrawer(typeof(MyCustomVaribleArray))]
public class MyCustomVariableArrayDrawer : PropertyDrawer
{
    private ReorderableList list;
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        //获取要渲染的列表
        property = property.FindPropertyRelative("variables");
        //设定动作函数
        if (list == null)
        {
            list = new ReorderableList(property.serializedObject, property, true, true, true, true)
            {
                multiSelect = true,
                elementHeight = 22,
                drawElementCallback = DrawElement,
                drawHeaderCallback = DrawHeader,
                // onAddCallback = 
                onAddDropdownCallback = OnAddElement,
                onRemoveCallback = OnRemoveElement,
                drawElementBackgroundCallback = DrawElementBackground
            };
        }
        else
        {
            list.serializedProperty = property;
        }
        list.DoList(position);
    }
    
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        // 基础高度
        var height = base.GetPropertyHeight(property, label) + 60;
        // 计算列表
        var variables = property.FindPropertyRelative("variables");
        for (var i = 0; i < variables.arraySize; i++)
            height += EditorGUI.GetPropertyHeight(variables.GetArrayElementAtIndex(i)) + 5;
        return height;
    }

    
    private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
    {
        var variables = list.serializedProperty;
        if (index < 0 || index >= variables.arraySize)
            return;

        var variable = variables.GetArrayElementAtIndex(index);

        var x = rect.x;
        var y = rect.y + 2;
        var width = rect.width - 20;
        var height = rect.height - 4;

        var variableRect = new Rect(x, y, width, height);
        EditorGUI.PropertyField(variableRect, variable, GUIContent.none);

        var buttonRect = new Rect(variableRect.xMax + 5, y + 1, 18, 18);

        if (GUI.Button(buttonRect, new GUIContent("+")))
        {
            Debug.Log("点击了按钮");
        }
    }
    
    private void DrawHeader(Rect rect)
    {
        GUI.Label(rect, "数据列表");
    }

    private void OnAddElement(Rect rect, ReorderableList list)
    {
        var variables = list.serializedProperty;
        //当前列表数量
        var index = variables.arraySize > 0 ? variables.arraySize : 0;
        //生成多选菜单
        var menu = new GenericMenu();
        //根据枚举项,决定生成菜单选项名称
        foreach (KeyCode variableType in Enum.GetValues(typeof(KeyCode)))
        {
            var type = variableType;
            menu.AddItem(new GUIContent(variableType.ToString()), false,
                context =>
                {
                    AddVariable(variables, index, type);
                }, null);
        }

        menu.ShowAsContext();
    }

    //根据选项,生成元素
    protected virtual void AddVariable(SerializedProperty variables, int index, KeyCode type)
    {
        if (index < 0 || index > variables.arraySize)
            return;
        
        variables.serializedObject.Update();
        //在在列表底插入元素,相当于追加元素
        variables.InsertArrayElementAtIndex(index);
        //获取到最新添加的元素,并根据选项修改枚举值
        var variableProperty = variables.GetArrayElementAtIndex(index);
        variableProperty.FindPropertyRelative("input").SetEnumValue(type);
        //写入数据到实例化对象
        variables.serializedObject.ApplyModifiedProperties();
        //清除焦点
        GUI.FocusControl(null);
    }

    private void OnRemoveElement(ReorderableList list)
    {
        var variables = list.serializedProperty;
        var selectedIndices = list.selectedIndices;
        //如果没选择任何表单元素,那么删除最后一个
        if (selectedIndices.Count == 0)
        {
            AskRemoveVariable(variables, variables.arraySize - 1);
        }
        //删除所有选中的对象
        for (var i = selectedIndices.Count - 1; i >= 0; i--)
        {
            var index = selectedIndices[i];
            list.Deselect(index);
            AskRemoveVariable(variables, index);
        }
    }
    protected virtual void AskRemoveVariable(SerializedProperty variables, int index)
    {
        if (variables == null || index < 0 || index >= variables.arraySize)
            return;

        var variable = variables.GetArrayElementAtIndex(index);
        var name = variable.FindPropertyRelative("name").stringValue;
        // 某项被填入值时,弹出提示避免直接清除
        if (string.IsNullOrEmpty(name))
        {
            RemoveVariable(variables, index);
            return;
        }

        if (EditorUtility.DisplayDialog("Confirm delete",
                string.Format("Are you sure you want to delete the item named \"{0}\"?", name), "Yes", "Cancel"))
            RemoveVariable(variables, index);
    }
    protected virtual void RemoveVariable(SerializedProperty variables, int index)
    {
        if (index < 0 || index >= variables.arraySize)
            return;

        variables.serializedObject.Update();
        //清除表单元素
        variables.DeleteArrayElementAtIndex(index);
        //写入数据到实例化对象
        variables.serializedObject.ApplyModifiedProperties();
        GUI.FocusControl(null);
    }


    private void DrawElementBackground(Rect rect, int index, bool isActive, bool isFocused)
    {
        ReorderableList.defaultBehaviours.DrawElementBackground(rect, index, isActive, false, true);
    }
}
  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莉萝爱萝莉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值