如果说一定要选出世界上最喜欢用快捷键的群体的话,那么一定是程序员了。
——龙儿
一方面为了激励自己保持学习,并将学习当中碰到的一些问题记录下来,另一方面也是感叹Unity中文互联网资料还是不够齐全繁多,有时候一个简单的问题要查询许久,并且查到的也经常是东一榔头西一棒子、不够系统化,有些资料甚至语焉不详,因此自然模仿无数前辈萌生了自己写Blog的想法,这便是《Unity3D实用技巧》。
本栏目中我计划结合自身的实际工作、学习经历不断分享一些不复杂却实用的,能有效提高U3D工程效率的技巧。
正如我一开始所说的,似乎习惯用键盘工作的人总是恨不得全面抛弃鼠标,最好工作时手完全不要离开键盘,其中程序员一定是佼佼者,毕竟思路到一半被烦人的移动鼠标打断总是痛苦的。而使用U3D过程中,自然而然的针对一些常用的需求,比如坐标归零、层级排序、切换正交视图等,Unity居然没有准备快捷键,真让人恼火,于是第一期就是分享如何定义自己的快捷键。
实际上自定义快捷键的本质是通过快捷键调用Unity编辑器菜单中的一个菜单项,而编辑器的菜单项是高度可自定义的,也就是说,我们可以编写一些自己的功能,自己的逻辑,并随时调用它。
简单的介绍过后,我们先直接以一个实例开始!
快捷键与"Hello!World!"
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!"字样。

截止于此,我们就可以快速的打印"Hello!World"了。
刚接触Unity的人可以先按照上面的步骤简单实践一下,加深理解。
进阶!实践!与[MenuItem()]标签
下面先直接贴一段用于将单个选中物体transfrom重置的代码,然后结合代码我们进一步深入了解MeunItem是什么?能干什么?为什么?
/// <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的运行机理。
https://blog.csdn.net/weixin_50746193/article/details/124534553 https://blog.csdn.net/dmk17771552304/article/details/119951589
https://blog.csdn.net/Lanco_er/article/details/109926898
因为不是此处讨论的重点,所以不再过多深入解析,而是我们直接来看我们实用的特性标签[MenuItem()]
面对不了解的API,我们首先查询一下手册,查询Unity官方手册可以看到如下的解释

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

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

实践证明通过快捷键或者手动点击得到的结果完全一致。
此时可能有些写过编辑器脚本的朋友也想起了,我们创建自定义窗口时,也是通过类似的方法创建的,我们也可以在此试验一下。
[MenuItem("U3D实用技巧/创建空白窗体")]
public static void ShowWindow()
{
var window = new EditorWindow();
window.Show();
} 点击创建出来的菜单项后,得到了这样的结果。

结果很好的符合了我们的预期,这个例子意味着我们可以通过类似的方式来DIY自己的U3DEditor。
继续回头看MenuItem标签,我们发现标签其实是有三个重载的,除了除了菜单项的路径,我们还可以显式的指定他是否是验证函数,指定他的优先级。

官方手册一贯的不说人话,我们直接上实例来说明第二个参数的用途。
[MenuItem("菜单项测试/TestTrue",true,10)]
public static void TestTrue()
{
Debug.Log("测试true");
}
[MenuItem("菜单项测试/TestFalse", false, 10)]
public static void TestFalse()
{
Debug.Log("测试False");
}

发现只创建了名为TestFalse的菜单项,这是怎么回事呢?我们直接查看一下MenuITem的源码。
//
// 摘要:
// 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 的菜单函数被调用之前执行。
似乎暗示了我们需要一个相同名称的菜单项,那么我们接着实践一下。
[MenuItem("菜单项测试/TestTrue",true,10)]
public static void TestTrue()
{
Debug.Log("测试true");
}
[MenuItem("菜单项测试/TestTrue", false, 10)]
public static void TestTrue2()
{
Debug.Log("测试true");
}

此时创建了菜单项,但他无法点击,永远是Disabled状态,我们回头继续查阅一下Unity的手册。
// 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;
}
// 验证过的菜单项。
// 在菜单栏的 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则启用我们的菜单项,反之不启用,我们实践一下。
[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时,会分组
[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:验证函数的调用问题
上面虽然说了验证函数,但是较真的人肯定会好奇,我们究竟是什么时候调用验证函数的呢?不妨测试一下。
[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的右键菜单了,下面是示例
[MenuItem("GameObject/右键菜单测试", false, 10)]
public static void MouseTest()
{
Debug.Log("测试右键");
}
[MenuItem("Assets/Assets右键菜单测试", false, 10)]
public static void AssetsMouseTest()
{
Debug.Log("测试右键");
}


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