Unity编辑器拓展参考代码:

      Editor下的脚本资源都不会打包。

      优先级相隔11就有有中线分割,每一个菜单栏的priority优先级默认为1000

具有一些参考价值的代码,如果读者需要详细信息,就到官方文档:

https://docs.unity3d.com/Manual/30_search.html?q=MenuItem

1.自定义窗口:

自定义一个跟Unity原生窗口一样的窗口:EditorWindow

file:///D:/Program64/Unity20190304f1/Unity/Editor/Data/Documentation/en/ScriptReference/EditorWindow.html

using UnityEditor;
using UnityEngine;

public class MyEditorWindow : EditorWindow
{
    [MenuItem("Window/MyEditorWindow")]
    static void ShowMyEditorWindow()
    {
        MyEditorWindow window = EditorWindow.GetWindow<MyEditorWindow>();
        window.Show();
    }

    private string name = "";
    private string myString = "My String";
    private bool groupEnabled;
    private bool myBool = true;
    private float myFloat = 1.23f;

    private void OnGUI()
    {
        GUILayout.Label("这是我的窗口");
        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);
        if (GUILayout.Button("创建"))
        {
            GameObject go = new GameObject(name);
            Undo.RegisterCreatedObjectUndo(go, "create a gameObject");
        }
        EditorGUILayout.EndToggleGroup();

        if (GUILayout.Button("博客地址"))
        {
            System.Diagnostics.Process.Start(blogURL);
        }
    }
}

2.MenuItem的各类用法:

using UnityEditor;
using UnityEngine;

namespace KervenTest
{
    // MenuItem之间相差11就会加一条横线区分
    // %=ctrl    #=shift    &=alt
    public class MenuTest
    {
        [MenuItem("Assets/KervenTest #s", false, 1101)]
        private static void KervenTestFunc()
        {
            Debug.Log("KervenTest Start: ");
        }

        [MenuItem("MyMenu/Log Selected Transform Nam")]
        private static void LogSelectedTransformName()
        {
            Debug.Log("Selected Transform is on " + Selection.activeTransform.gameObject.name + ".");
        }
        
        // 如果此函数返回false,则菜单栏将被禁用
        [MenuItem("MyMenu/Log Selected Transform Nam", true)]
        private static bool ValidateLogSelectedTransformName()
        {
            return Selection.activeTransform != null;
        }

        [MenuItem("MyMenu/Do Something with a Shortcut key %g")]
        private static void DoSomethingWithAShortcutKey()
        {
            Debug.Log("Doing something with a Shortcut Key ...");
        }

        // 给组件右键菜单栏添加按钮
        // MenuCommand当前正在操作的组件
        [MenuItem("CONTEXT/Rigidbody/Double Mass %r")]
        private static void DoubleMass(MenuCommand command)
        {
            Rigidbody body = (Rigidbody)command.context;
            body.mass *= 2;
            Debug.Log("Doubled Rigidbody's Mass to " + body.mass + " from Context Menu");
        }

        // (Rigidbody)command.context
        // menuCommand.context as GameObject
        // 括号和as转类型的区别:()是强制、转不成功会报错,as更为温和、转不成功不会报错
        [MenuItem("GameObject/MyCategory/Custom Game Object", false, 10)]
        private static void CreateCustomGameObject(MenuCommand menuCommand)
        {
            GameObject go = new GameObject("Custom Game Object");
            GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
            Undo.RegisterCompleteObjectUndo(go, "Create " + go.name);
            Selection.activeObject = go;
        }

        [MenuItem("GameObject/my delete", false, 11)]
        private static void MyDelete()
        {
            foreach (var obj in Selection.objects)
            {
                // GameObject.DestroyImmediate(obj);
                // 利用Undo进行可撤销的删除操作
                // 需要把删除操作注册到操作记录里面
                Undo.DestroyObjectImmediate(obj);
            }
        }
    }
}

3.生成一个对话框,与1有区别的对话框:ScriptableWizard

using UnityEngine;
using UnityEditor;

public class BatchChangeTestComponent : ScriptableWizard
{
    [MenuItem("MyMenu/BatchChangeTestComponent")]
    static void OpenBatchChangeWizard()
    {
        DisplayWizard<BatchChangeTestComponent>("批量修改选中的TestComponent", "ChangeAndClose", "Change");
    }

    public int startHealthAddValue = 0;
    public int currentHealthAddValue = 0;
    public Color lightColorChangeValue = Color.yellow;

    private const string StartHealthAddValueKey = "BatchChangeTestComponent.startHealthAddValue";
    private const string CurrentHealthAddValueKey = "BatchChangeTestComponent.currentHealthAddValue";
    private const string LightColorChangeValueKeyR = "BatchChangeTestComponent.lightColorChangeValue_R";
    private const string LightColorChangeValueKeyG = "BatchChangeTestComponent.lightColorChangeValue_G";
    private const string LightColorChangeValueKeyB = "BatchChangeTestComponent.lightColorChangeValue_B";
    private const string LightColorChangeValueKeyA = "BatchChangeTestComponent.lightColorChangeValue_A";

    private void OnEnable()
    {
        startHealthAddValue = EditorPrefs.GetInt(StartHealthAddValueKey);
        currentHealthAddValue = EditorPrefs.GetInt(CurrentHealthAddValueKey);
        lightColorChangeValue = GetLightColorPrefs();
    }

    private void OnWizardCreate()
    {
        GameObject[] testCompsGo = Selection.gameObjects;
        // 显示进度条
        EditorUtility.DisplayProgressBar("修改进度", "已完成数量:" + "0/" + testCompsGo.Length, 0);
        int count = 0;
        foreach (var go in testCompsGo)
        {
            TestComponent tComp = go.GetComponent<TestComponent>();
            Undo.RecordObject(tComp, "batch change tComps");
            tComp.staringHealth += startHealthAddValue;
            tComp.currentHealth += currentHealthAddValue;
            tComp.lightColor = lightColorChangeValue;
            count++;
            EditorUtility.DisplayProgressBar("修改进度", "已完成数量:" + count + "/" + testCompsGo.Length, 0);
        }
        EditorUtility.ClearProgressBar();
        // 使用ShowNotification显示提示信息
        ShowNotification(new GUIContent(Selection.gameObjects.Length + "个游戏物体的值被修改了"));
    }

    private void OnWizardOtherButton()
    {
        OnWizardCreate();
    }

    // 当字段值修改时会被调用
    private void OnWizardUpdate()
    {
        errorString = null;
        helpString = null;
        if (Selection.gameObjects.Length > 0)
            helpString = "当前选择了" + Selection.gameObjects.Length + "个物体";
        else
            errorString = "请至少选择一个物体";
        // 使用EditorPrefs保存数据
        EditorPrefs.SetInt(StartHealthAddValueKey, startHealthAddValue);
        EditorPrefs.SetInt(CurrentHealthAddValueKey, currentHealthAddValue);
        SetLightColorPrefs(lightColorChangeValue);
    }

    private void SetLightColorPrefs(Color color)
    {
        EditorPrefs.SetFloat(LightColorChangeValueKeyR, color.r);
        EditorPrefs.SetFloat(LightColorChangeValueKeyG, color.g);
        EditorPrefs.SetFloat(LightColorChangeValueKeyB, color.b);
        EditorPrefs.SetFloat(LightColorChangeValueKeyA, color.a);
    }

    private Color GetLightColorPrefs()
    {
        Color color = new Color();
        color.r = EditorPrefs.GetFloat(LightColorChangeValueKeyR);
        color.g = EditorPrefs.GetFloat(LightColorChangeValueKeyG);
        color.b = EditorPrefs.GetFloat(LightColorChangeValueKeyB);
        color.a = EditorPrefs.GetFloat(LightColorChangeValueKeyA);
        return color;
    }

    private void OnSelectionChange()
    {
        OnWizardUpdate();
    }
}

4.创建窗口,自动生成Service文件夹及下面的文件:

using UnityEngine;
using UnityEditor;
using System.IO;

public class CreateService : ScriptableWizard
{
    [MenuItem("Assets/AutoCreateService #c", false, 1102)]
    static void OpenCreateServiceWizard()
    {
        DisplayWizard<CreateService>("生成Service", "生成Service");
    }

    public string serviceName = "Test";
    
    // "Create" 按钮点击执行的事件
    private void OnWizardCreate()
    {
        CreateAService();
    }
    
    private void CreateAService()
    {
        string folderPath = Application.dataPath + "/Scripts/ToLua/Services/" + serviceName;
        if (!Directory.Exists(folderPath))
        {
            Directory.CreateDirectory(folderPath);
            AssetDatabase.Refresh();
        }
            
        string filePathWithFileName = folderPath + "/" + serviceName + "Service.lua";
        CreateAndWriteFile(filePathWithFileName, "KingOfStrategyService");
    }

    private void CreateAndWriteFile(string filePathWithFileName, string msg)
    {
        var fileWriter = File.CreateText(filePathWithFileName);
        fileWriter.WriteLine(msg);
        fileWriter.Close();
        AssetDatabase.Refresh();
    }
}

5.对应组件添加右键菜单项:ContextMenuItem

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    [ContextMenuItem("开始血量增加10", "AddStartHp_10")]
    public int staringHealth = 100;
    public int currentHealth;
    [ContextMenuItem("重置颜色为yellow", "SetLightColor2Yellow")]
    [ContextMenuItem("重置颜色为green", "SetLightColor2Green")]
    public Color lightColor = Color.yellow;

    void AddStartHp_10()
    {
        staringHealth += 10;
    }

    void SetLightColor2Yellow()
    {
        lightColor = Color.yellow;
    }

    [ContextMenu("SetColor2Green")]
    void SetLightColor2Green()
    {
        lightColor = Color.green;
    }
}

6.扩展在脚本上点击右键执行修改脚本属性的方法

[MenuItem("CONTEXT/PlayerHealth/InitHealthAndSpeed")]// CONTEXT 组件名 按钮名
    static void InitHealthAndSpeed(MenuCommand cmd)//menucommand是当前正在操作的组件
    {
        //Debug.Log(cmd.context.GetType().FullName);
        CompleteProject.PlayerHealth health = cmd.context as CompleteProject.PlayerHealth;
        health.startingHealth = 200;
        health.flashSpeed = 10;
        Debug.Log("Init");
    }

7.学习使用selection获取选择的游戏物体,制作删除功能(可以撤销的)删除操作

[MenuItem("GameObject/my delete", false, 11)]
    static void Mydelete()
    {
        foreach (Object o in Selection.objects)
        {
            //GameObject.DestroyImmediate(o);
            Undo.DestroyObjectImmediate(o);//利用Undo进行的删除操作 是可以撤销的
        }
        //需要把删除操作注册到 操作记录里面
    }

8.给菜单项添加快捷键

//%=ctrl #=shift &=alt
    [MenuItem("Tools/test2 %q",false,100)]
    static void Test2()
    {
        Debug.Log("Test2");
    }
    [MenuItem("Tools/test3 %t",false,0)]
    static void Test3()
    {
        Debug.Log("Test3");
    }

9.控制菜单项是否启用的功能

[MenuItem("GameObject/my delete", true, 11)]
    static bool MyDeleteValidate()
    {
        if (Selection.objects.Length > 0)
            return true;
        else
            return false;
    }

    [MenuItem("GameObject/my delete", false, 11)]
    static void Mydelete()
    {
        foreach (Object o in Selection.objects)
        {
            //GameObject.DestroyImmediate(o);
            Undo.DestroyObjectImmediate(o);//利用Undo进行的删除操作 是可以撤销的
        }
        //需要把删除操作注册到 操作记录里面
    }

10.Demo

[MenuItem("Assets/PackageTools/SetCastShadowsOff", priority = 506)]
    static void SetCastShadowsOff()
    {
        Debug.Log("SetCastShadowsOff");
        var select = Selection.activeObject;
        if (!select)
        {
            return;
        }
        var folderPath = AssetDatabase.GetAssetPath(select);
        if (Directory.Exists(folderPath))
        {
 
            var fbxs = Directory.GetFiles(folderPath, "*.FBX", SearchOption.AllDirectories);
            foreach (var item in fbxs)
            {
                Transform fbx = AssetDatabase.LoadAssetAtPath<Transform>(item);
                ///下面代码有点鬼畜,.On正常,.Off就只能在本级目录
                var renderers = fbx.GetComponentsInChildren<SkinnedMeshRenderer>();
                foreach (var one in renderers)
                {
                    string lowerChildName = one.transform.name.ToLower();
                    if (lowerChildName.Contains("head") || lowerChildName.Contains("face") || lowerChildName.Contains("lian"))
                    {
                        Debug.Log(fbx.name);
                        Debug.Log(lowerChildName);
 
                        one.shadowCastingMode = ShadowCastingMode.Off;
                        Debug.Log(one.shadowCastingMode);
                    }
                }
                AssetDatabase.SaveAssets();
            }
 
            var prefabs = Directory.GetFiles(folderPath, "*.prefab", SearchOption.AllDirectories);
            foreach (var item in prefabs)
            {
                Transform prefab = AssetDatabase.LoadAssetAtPath<Transform>(item);
                var renderers = prefab.GetComponentsInChildren<SkinnedMeshRenderer>();
                foreach (var one in renderers)
                {
                    string lowerChildName = one.transform.name.ToLower();
                    if (lowerChildName.Contains("head") || lowerChildName.Contains("face") || lowerChildName.Contains("lian"))
                    {
                        one.shadowCastingMode = ShadowCastingMode.Off;
                    }
                }
            }
 
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }
    }
 
    [MenuItem("Assets/PackageTools/FindIncorrectShader", priority = 507)]
    static void FindIncorrectShader()
    {
        Debug.Log("FindIncorrectShader");
        var select = Selection.activeObject;
        if (!select)
        {
            return;
        }
        var folderPath = AssetDatabase.GetAssetPath(select);
        if (Directory.Exists(folderPath))
        {
            var mats = Directory.GetFiles(folderPath, "*.mat", SearchOption.AllDirectories);
            foreach (var item in mats)
            {
                Material material = AssetDatabase.LoadAssetAtPath<Material>(item);
                
                if (material.shader.ToString().IndexOf("PJAQ/Character/ToonV5")==-1)
                {
                    Debug.Log(item.ToString());//
                }
            }
        }
 
    }

11.把Wizard写成一个有取消确认的Tip

using UnityEngine;
using UnityEditor;

public class LocalizationTip : ScriptableWizard
{
    private static bool exporting = false;
    private static bool translating = false;
    private static string content;
    private static bool isInitPos = false;
    private static Rect initPos;
    private static LocalizationTip window = null;

    public static void OpenLocalizationTipWizard(bool export, bool translate, string value, Rect pos)
    {
        isInitPos = false;
        initPos = pos;
        exporting = export;
        translating = translate;
        content = value;
        if (window == null)
            window = DisplayWizard<LocalizationTip>("提示框");
        window.Focus();
    }

    private void OnGUI()
    {
        if (!isInitPos)
        {
            isInitPos = true;
            float width = 320f;
            float heigth = 150f;
            var xOffset = (initPos.width - width) / 2;
            var yOffset = (initPos.height - heigth) / 2;
            position = new Rect(initPos.x + xOffset, initPos.y + yOffset, width, heigth);
        }

        GUILayout.Label(content + "?", EditorStyles.boldLabel);

        GUILayout.FlexibleSpace();

        GUILayout.BeginHorizontal();
        if (GUILayout.Button("取消"))
            OnCancel();
        if (GUILayout.Button("确定"))
            OnConfirm();
        GUILayout.EndHorizontal();
    }

    private void OnConfirm()
    {
        if (exporting)
        {
            // LocalizationEditor.Instance.Export();
        }
        
        if (translating)
        {
            // LocalizationEditor.Instance.Translate();
        }
    }

    private void OnCancel()
    {
        this.Close();
        GUIUtility.ExitGUI();
    }
}