【Unity3D实用技巧·一】自定义快捷键
加藤龙儿
编辑于 2023年08月09日 08:27

如果说一定要选出世界上最喜欢用快捷键的群体的话,那么一定是程序员了。

——龙儿

    一方面为了激励自己保持学习,并将学习当中碰到的一些问题记录下来,另一方面也是感叹Unity中文互联网资料还是不够齐全繁多,有时候一个简单的问题要查询许久,并且查到的也经常是东一榔头西一棒子、不够系统化,有些资料甚至语焉不详,因此自然模仿无数前辈萌生了自己写Blog的想法,这便是《Unity3D实用技巧》。

    本栏目中我计划结合自身的实际工作、学习经历不断分享一些不复杂却实用的,能有效提高U3D工程效率的技巧。

    正如我一开始所说的,似乎习惯用键盘工作的人总是恨不得全面抛弃鼠标,最好工作时手完全不要离开键盘,其中程序员一定是佼佼者,毕竟思路到一半被烦人的移动鼠标打断总是痛苦的。而使用U3D过程中,自然而然的针对一些常用的需求,比如坐标归零、层级排序、切换正交视图等,Unity居然没有准备快捷键,真让人恼火,于是第一期就是分享如何定义自己的快捷键

    实际上自定义快捷键的本质是通过快捷键调用Unity编辑器菜单中的一个菜单项,而编辑器的菜单项是高度可自定义的,也就是说,我们可以编写一些自己的功能,自己的逻辑,并随时调用它。

    简单的介绍过后,我们先直接以一个实例开始!


快捷键与"Hello!World!&#​34;

代码块
C#
自动换行
复制代码
using UnityEditor;
using UnityEngine;
public class EditorShortcuts
{
    //使用了Unity的标签[MenuItem()],该标签来源于UnityEditor命名空间,它用于自定义菜单项
    //该标签有三个参数,最常用的是第一个参数string path
    //字符串用来表示菜单项位于Unity菜单中的位置,可以以一个空格作为分割加上自定义的快捷键
  	 //&代表Alt h代表键盘的h
    [MenuItem("U3D实用技巧/自定义快捷键 &h")]
    //我们的方法是针对于编辑器的自定义快捷键,显然他是编辑器的一个固有方法
    //所以他当然是一个静态方法而不是实例方法
    public static void SayHello() 
    {
        Debug.Log("Hello!World!");
    }
}
复制成功

    我们在Unity工程中创建好上面的脚本,此时在编辑器中按下Alt+Q就可以看到我们的控制台中已经打印出了"Hello!World!&#​34;字样。

    截止于此,我们就可以快速的打印"Hello!World&#​34;了。

    刚接触Unity的人可以先按照上面的步骤简单实践一下,加深理解。


进阶!实践!与[MenuItem()]标签

    下面先直接贴一段用于将单个选中物体transfrom重置的代码,然后结合代码我们进一步深入了解MeunItem是什么?能干什么?为什么?

代码块
JavaScript
自动换行
复制代码
    /// <summary>
    /// 按alt+r重置选中的对象
    /// </summary>
    [MenuItem("U3D实用技巧/重置Transform &r")]
    public static void ResetTransform() 
    {
        //获取场景内选中的对象
        var objs = Selection.transforms;
        if(objs.Length > 0) 
        {
            //向Unity注册Undo事件用于置脏和编辑器撤销
            Undo.RecordObjects(objs, "重置Transform");
            foreach (var obj in objs) 
            {
                obj.transform.localPosition = Vector3.zero;
                obj.localScale = Vector3.one;
                obj.localRotation = Quaternion.identity;
            }
        }
        Debug.Log("选中的物体的Transform已经重置了");
    }
复制成功

    这样我们就简单的实现了通过快捷键“Alt+r”使选中的物体transform置零的操作。他在表现上和我们手动在transform组件上点击Reset是一致的。

    相似的我们实际上可以在方法体中写任何的自定义内容。

    但是有一点需要注意,我们自定义的快捷键不能和Unity已经有的快捷键冲突,那么如何查看unity内置的快捷键呢?

我们可以在Edit的Shortcuts中打开unity的快捷键窗口,同时也可以在这里设置内置的快捷键命令。

    另外这里是一些常用的自定义快捷键:

    ctrl %

    shift #

    alt &

    单个字母快捷键前缀 _

    如果只是想了解如何在Unity中自定义快捷键那么到这里其实已经介绍完80%的内容了,剩下的细枝末节大家可以在网上检索Unity编辑器扩展自行查阅更多的相关内容。

    我们重点来关注一下背后的原理,为什么这样做就可以实现自定义快捷键了呢?从原理层面,我们到底做了什么?

    底层一点的层面讲,Unity是通过“反射”来遍历、检索、调用对应的函数来实现功能的,具体的机制我们可以搜索“Unity 反射”相关的字样进一步了解,我们的[MenuItem()]标签原理也是类似的,通过给自定义静态方法加标签,我们让编辑器自动生成了一个菜单项,并在触发这个菜单项时,回调对应的方法。


 PS:针对Unity反射机制与标签的原理的解析,这里推荐一下CSDN上,漫漫无期老师和用c#写unity的杰宝老师以及宿春磊Charles老师的文章,我们可以从中管中窥豹得以照见Unity的运行机理

代码块
C#
自动换行
复制代码
https://blog.csdn.net/weixin_50746193/article/details/124534553
复制成功
代码块
C#
自动换行
复制代码
https://blog.csdn.net/dmk17771552304/article/details/119951589
复制成功

代码块
C#
自动换行
复制代码
https://blog.csdn.net/Lanco_er/article/details/109926898
复制成功


    因为不是此处讨论的重点,所以不再过多深入解析,而是我们直接来看我们实用的特性标签[MenuItem()]

    面对不了解的API,我们首先查询一下手册,查询Unity官方手册可以看到如下的解释

    直接看重点,用于向主菜单和上下文检视菜单创建菜单项,仅可对静态函数使用,与我们上文的实践相符这说明了该标签的作用确实是创建菜单项,我们回头看看上面实例在工程中的体现。

    确实创建了两个菜单项,接下来我们删除掉实例中的快捷键,并手动点击它试试。

    实践证明通过快捷键或者手动点击得到的结果完全一致。

    此时可能有些写过编辑器脚本的朋友也想起了,我们创建自定义窗口时,也是通过类似的方法创建的,我们也可以在此试验一下。

代码块
JavaScript
自动换行
复制代码
    [MenuItem("U3D实用技巧/创建空白窗体")]
    public static void ShowWindow() 
    {
        var window = new EditorWindow();
        window.Show();
    }
复制成功

    点击创建出来的菜单项后,得到了这样的结果。

    结果很好的符合了我们的预期,这个例子意味着我们可以通过类似的方式来DIY自己的U3DEditor。

    继续回头看MenuItem标签,我们发现标签其实是有三个重载的,除了除了菜单项的路径,我们还可以显式的指定他是否是验证函数,指定他的优先级。

    官方手册一贯的不说人话,我们直接上实例来说明第二个参数的用途。

代码块
JavaScript
自动换行
复制代码
    [MenuItem("菜单项测试/TestTrue",true,10)]
    public static void TestTrue() 
    {
        Debug.Log("测试true");
    }
    [MenuItem("菜单项测试/TestFalse", false, 10)]
    public static void TestFalse()
    {
        Debug.Log("测试False");
    }
复制成功

        发现只创建了名为TestFalse的菜单项,这是怎么回事呢?我们直接查看一下MenuITem的源码。

代码块
C#
自动换行
复制代码
        //
        // 摘要:
        //     Creates a menu item and invokes the static function that follows it when the
        //     menu item is selected.
        //
        // 参数:
        //   itemName:
        //     The itemName is the menu item represented like a pathname. For example, the menu
        //     item could be "GameObject/Do Something".
        //
        //   isValidateFunction:
        //     If isValidateFunction is true, this is a validation function and is called before
        //     the menu function with the same itemName is invoked.
        //
        //   priority:
        //     The order by which the menu items are displayed.
        public MenuItem(string itemName, bool isValidateFunction)
            : this(itemName, isValidateFunction, itemName.StartsWith("GameObject/Create Other") ? 10 : 1000)
        {
        }
复制成功

    翻译一下:

    //   isValidateFunction: 

    //     如果 isValidateFunction 为 true,这是一个验证函数,在与相同 itemName 的菜单函数被调用之前执行。

    似乎暗示了我们需要一个相同名称的菜单项,那么我们接着实践一下。

代码块
C#
自动换行
复制代码
    [MenuItem("菜单项测试/TestTrue",true,10)]
    public static void TestTrue() 
    {
        Debug.Log("测试true");
    }
    [MenuItem("菜单项测试/TestTrue", false, 10)]
    public static void TestTrue2()
    {
        Debug.Log("测试true");
    }
复制成功

    此时创建了菜单项,但他无法点击,永远是Disabled状态,我们回头继续查阅一下Unity的手册。

代码块
C#
自动换行
复制代码
    // Validated menu item.
    // Add a menu item named "Log Selected Transform Name" to MyMenu in the menu bar.
    // We use a second function to validate the menu item
    // so it will only be enabled if we have a transform selected.
    [MenuItem("MyMenu/Log Selected Transform Name")]
    static void LogSelectedTransformName()
    {
        Debug.Log("Selected Transform is on " + Selection.activeTransform.gameObject.name + ".");
    }

    // Validate the menu item defined by the function above.
    // The menu item will be disabled if this function returns false.
    [MenuItem("MyMenu/Log Selected Transform Name", true)]
    static bool ValidateLogSelectedTransformName()
    {
        // Return false if no transform is selected.
        return Selection.activeTransform != null;
    }
复制成功

代码块
C#
自动换行
复制代码
// 验证过的菜单项。 
// 在菜单栏的 MyMenu 中添加一个名为 “Log Selected Transform Name” 的菜单项。 
// 我们使用一个第二个函数来验证菜单项 
// 这样它只有在我们选择了一个 transform 时才会被启用。 
[MenuItem(“MyMenu/Log Selected Transform Name”)] 
public static void LogSelectedTransformName() { 
// 在控制台输出 "Selected Transform is on " + 选择的 transform 的游戏对象的名字 + “.” 
Debug.Log("Selected Transform is on " + Selection.activeTransform.gameObject.name + “.”); 
}

// 验证上面定义的函数对应的菜单项。 
// 如果这个函数返回 false,菜单项将被禁用。 
[MenuItem(“MyMenu/Log Selected Transform Name”, true)] 
public static bool ValidateLogSelectedTransformName() { 
// 如果没有选择任何 transform,返回 false。 
return Selection.activeTransform != null; 
}
复制成功

    也就是说,[MenuItem()]特性要求我们的验证函数返回一个Bool值,如果这个值是true则启用我们的菜单项,反之不启用,我们实践一下。

代码块
C#
自动换行
复制代码
    [MenuItem("菜单项测试/TestTrue",false,10)]
    public static void TestTrue() 
    {
        Debug.Log("测试true");
    }
    [MenuItem("菜单项测试/TestTrue", true, 10)]
    public static bool TestTrue2()
    {
        //获取当前活动场景
        var scene = SceneManager.GetActiveScene();
        //获取当前场景的根物体的数量
        int nums = scene.rootCount;
        //如果当前场景的根物体数量大于6,那么返回true
        return nums>6;
    }
复制成功

    至于最后一个优先级,我们直接看一个例子就能了解清楚。

    说明了两件事:1、优先级越小越靠上 2、当优先级间隔大于10时,会分组

代码块
C#
自动换行
复制代码
    [MenuItem("菜单项测试/优先级11", false, 11)]
    public static void TestFalse1()
    {
        Debug.Log("测试False");
    }
    [MenuItem("菜单项测试/优先级21", false, 21)]
    public static void TestFalse2()
    {
        Debug.Log("测试False");
    }
    [MenuItem("菜单项测试/优先级22", false, 22)]
    public static void TestFalse3()
    {
        Debug.Log("测试False");
    }
    [MenuItem("菜单项测试/优先级23", false, 23)]
    public static void TestFalse4()
    {
        Debug.Log("测试False");
    }
    [MenuItem("菜单项测试/优先级34", false, 34)]
    public static void TestFalse5()
    {
        Debug.Log("测试False");
    }
复制成功

    到此,我们队[MenuItem()]的了解和应用也说得七七八八了,只剩下一些小问题写在最后。


番外1:验证函数的调用问题

    上面虽然说了验证函数,但是较真的人肯定会好奇,我们究竟是什么时候调用验证函数的呢?不妨测试一下。

代码块
C#
自动换行
复制代码
    [MenuItem("菜单项测试/TestTrue", true, 10)]
    public static bool TestTrue2()
    {
        //获取当前活动场景
        var scene = SceneManager.GetActiveScene();
        //获取当前场景的根物体的数量
        int nums = scene.rootCount;
        Debug.Log("现在有" + nums + "个物体");
        //如果当前场景的根物体数量大于6,那么返回true
        return nums>6;
    }
复制成功

    这样一来,我们只需要观察Unity在什么时候会打印消息就可以知道了。

    只是创建新物体,当数量为5的时候,并未调用验证函数。

    依然没调用。

    数量到了7,此时毫无疑问验证函数的结果从false变为了true,但是他依然没有调用。

    我们尝试看一下菜单项。

我们发现,当我们“试图调用被验证函数时”,验证函数被调用了,无论我们是否实际调用了被验证函数。有些类似对象的构造方法,当我们实例化类时,调用了类的构造方法,无论我们是否实际使用了这个类的对象。


番外2:物体右键菜单与Assets窗口的右键菜单

    1、只需要将路径设为“GameObject/实际菜单项名称”,并且优先级小于等于49就可以添加右键菜单了

    2、只需要将路径设为“Assets/自定义菜单_Assets”,就可以添加Assets的右键菜单了,下面是示例

代码块
C#
自动换行
复制代码
    [MenuItem("GameObject/右键菜单测试", false, 10)]
    public static void MouseTest()
    {
        Debug.Log("测试右键");
    }
    [MenuItem("Assets/Assets右键菜单测试", false, 10)]
    public static void AssetsMouseTest()
    {
        Debug.Log("测试右键");
    }
复制成功


结语:

    写的有些啰嗦,但是需要了解的和一些拓展应用应该都介绍到位了,希望能有机会帮到一些人;完整的代码因为懒就不放了,实际起作用的代码都在文中了,有需要的自行取用即可。

    转载自便,标不标出处都可以。