💡 菜单系统的力量:
- 想为团队打造统一的工具菜单入口?
- 快捷键怎么设置,哪些符号代表什么按键?
- 如何实现菜单项的动态启用/禁用?
- 怎样在右键菜单中添加自定义选项?
这篇文章! 将系统讲解 MenuItem 的所有高级用法,帮你打造专业级的编辑器菜单系统!
MenuItem 特性用于在 Unity 编辑器菜单栏或上下文菜单中添加自定义菜单项。
1.1 菜单位置
| 菜单位置 |
说明 |
示例路径 |
| 顶部菜单 |
编辑器顶部菜单栏 |
Tools/MyTool |
| 上下文菜单 |
右键菜单 |
CONTEXT/Component/MyAction |
| 资源菜单 |
Project 窗口右键 |
Assets/MyAction |
| 层级菜单 |
Hierarchy 窗口右键 |
GameObject/MyAction |
二、基本用法
2.1 创建顶部菜单项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| using UnityEngine; using UnityEditor;
public class MenuItemExample : Editor { [MenuItem("Tools/My Custom Tool")] static void MyCustomTool() { Debug.Log("执行自定义工具"); }
[MenuItem("Tools/MyMenu/Option 1")] static void Option1() { Debug.Log("选项1"); }
[MenuItem("Tools/MyMenu/Option 2")] static void Option2() { Debug.Log("选项2"); } }
|
2.2 菜单结构
1 2 3 4 5 6 7 8 9 10 11 12
| Unity 编辑器菜单栏 ├── File ├── Edit ├── Assets ├── GameObject ├── Components ├── Window └── Tools ├── My Custom Tool ← 自定义菜单项 └── MyMenu ← 自定义子菜单 ├── Option 1 └── Option 2
|
三、菜单优先级
使用 priority 参数控制菜单项的显示顺序,数值越小越靠前。
3.1 优先级规则
| 优先级范围 |
说明 |
示例分组 |
| 0 - 49 |
放在最前面,与内置菜单分开 |
自定义工具组 |
| 50 - 1000 |
按 50 的倍数分组 |
10、100、150 等 |
| 10 |
参考分组 |
常用工具 |
| 100 |
参考分组 |
一般工具 |
3.2 示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class MenuItemPriority : Editor { [MenuItem("Tools/Priority Test/A (优先级5)", false, 5)] static void TestA() => Debug.Log("A");
[MenuItem("Tools/Priority Test/B (优先级15)", false, 15)] static void TestB() => Debug.Log("B");
[MenuItem("Tools/Priority Test/C (优先级25)", false, 25)] static void TestC() => Debug.Log("C");
[MenuItem("Tools/Priority Test/D (优先级100)", false, 100)] static void TestD() => Debug.Log("D");
[MenuItem("Tools/Priority Test/E (优先级200)", false, 200)] static void TestE() => Debug.Log("E"); }
|
3.3 显示效果
1 2 3 4 5 6 7 8 9
| Tools 菜单 ├── Priority Test │ ├── A (优先级5) ← 最靠前 │ ├── B (优先级15) │ ├── C (优先级25) │ ├── ------------------- ← 50倍数分隔线 │ ├── D (优先级100) │ └── E (优先级200) ← 最后 └── (其他菜单项)
|
四、菜单验证
通过验证函数控制菜单项的启用/禁用状态。
4.1 基本语法
1 2 3 4 5 6 7 8 9
| [MenuItem("菜单路径", 验证函数名)] static void 菜单方法() { }
static bool 验证函数名() { return true; }
|
4.2 验证示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public class MenuItemValidation : Editor { [MenuItem("Tools/Create Cube", false, 10)] static void CreateCube() { GameObject.CreatePrimitive(PrimitiveType.Cube); }
[MenuItem("Tools/Create Cube", true, 11)] static bool ValidateCreateCube() { return Selection.activeGameObject != null; }
[MenuItem("Tools/Rename Selection", false, 20)] static void RenameSelection() { Selection.activeObject.name = "RenamedObject"; }
[MenuItem("Tools/Rename Selection", true, 20)] static bool ValidateRenameSelection() { return Selection.activeGameObject != null && Selection.gameObjects.Length == 1; }
[MenuItem("Tools/Add Rigidbody", true, 30)] static bool ValidateAddRigidbody() { if (Selection.activeGameObject) { return Selection.activeGameObject.GetComponent<Rigidbody>() == null; } return false; } }
|
4.3 验证函数返回值效果
| 返回值 |
菜单状态 |
true |
正常显示,可点击 |
false |
灰色显示,禁用点击 |
五、菜单勾选状态
为菜单项添加勾选标记(对号)。
5.1 基本用法
1 2 3 4 5 6 7 8 9 10 11 12
| public class MenuItemChecked : Editor { [MenuItem("Tools/Toggle Feature %#g")] static void ToggleFeature() { string menuPath = "Tools/Toggle Feature"; bool currentState = Menu.GetChecked(menuPath); Menu.SetChecked(menuPath, !currentState);
Debug.Log($"功能已{(!currentState ? "启用" : "禁用")}"); } }
|
5.2 快捷键说明
%#g 表示快捷键 Ctrl+G(macOS 为 Cmd+G)
| 符号 |
说明 |
% |
Ctrl / Cmd |
# |
Shift |
& |
Alt |
_ |
无修饰键 |
5.3 常用快捷键组合
1 2 3 4
| [MenuItem("Tools/Quick Action %g")] [MenuItem("Tools/Quick Action %#g")] [MenuItem("Tools/Quick Action &g")] [MenuItem("Tools/Quick Action _g")]
|
5.4 切换功能示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class EditorSettingsToggle : Editor { private const string DEBUG_MODE_PATH = "Tools/Debug Mode";
[MenuItem(DEBUG_MODE_PATH)] static void ToggleDebugMode() { bool currentState = Menu.GetChecked(DEBUG_MODE_PATH); Menu.SetChecked(DEBUG_MODE_PATH, !currentState);
EditorPrefs.SetBool("DebugMode_Enabled", !currentState); }
[MenuItem("Tools/Sync Debug Mode", false, 100)] static void SyncDebugMode() { bool enabled = EditorPrefs.GetBool("DebugMode_Enabled", false); Menu.SetChecked(DEBUG_MODE_PATH, enabled); } }
|
六、上下文菜单
为特定组件或资源添加右键菜单。
6.1 组件上下文菜单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class ComponentContextMenu { [MenuItem("CONTEXT/Transform/Reset to Zero")] static void ResetTransform(MenuCommand command) { Transform transform = command.context as Transform; if (transform != null) { Undo.RecordObject(transform, "Reset Transform"); transform.localPosition = Vector3.zero; transform.localRotation = Quaternion.identity; transform.localScale = Vector3.one; } }
[MenuItem("CONTEXT/Transform/Reset to Zero", true)] static bool ValidateResetTransform() { return Selection.activeTransform != null; }
[MenuItem("CONTEXT/Rigidbody/Set Mass to 1")] static void SetRigidbodyMass(MenuCommand command) { Rigidbody rb = command.context as Rigidbody; if (rb != null) { Undo.RecordObject(rb, "Set Mass"); rb.mass = 1f; } }
[MenuItem("CONTEXT/Component/Copy Component Name")] static void CopyComponentName(MenuCommand command) { Component component = command.context as Component; if (component != null) { GUIUtility.systemCopyBuffer = component.GetType().Name; Debug.Log($"已复制: {component.GetType().Name}"); } } }
|
6.2 资源上下文菜单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class AssetContextMenu { [MenuItem("Assets/Select Dependencies")] static void SelectDependencies() { string path = AssetDatabase.GetAssetPath(Selection.activeObject); string[] dependencies = AssetDatabase.GetDependencies(path, true);
List<Object> dependencyObjects = new List<Object>(); foreach (string dep in dependencies) { Object obj = AssetDatabase.LoadMainAssetAtPath(dep); if (obj != null) dependencyObjects.Add(obj); }
Selection.objects = dependencyObjects.ToArray(); }
[MenuItem("Assets/Select Dependencies", true)] static bool ValidateSelectDependencies() { return Selection.activeObject != null; } }
|
6.3 GameObject 上下文菜单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class GameObjectContextMenu { [MenuItem("GameObject/Custom/Group Under Empty", false, 0)] static void GroupUnderEmpty() { GameObject[] selected = Selection.gameObjects; if (selected.Length == 0) return;
GameObject parent = new GameObject("Group"); Undo.RegisterCreatedObjectUndo(parent, "Create Group");
Vector3 center = Vector3.zero; foreach (GameObject obj in selected) { center += obj.transform.position; } center /= selected.Length;
parent.transform.position = center;
foreach (GameObject obj in selected) { Undo.SetTransformParent(obj.transform, parent.transform, "Group Objects"); } }
[MenuItem("GameObject/Custom/Group Under Empty", true)] static bool ValidateGroupUnderEmpty() { return Selection.gameObjects.Length > 0; } }
|
七、MenuCommand 参数
MenuCommand 参数提供了上下文信息。
7.1 MenuCommand 属性
| 属性 |
类型 |
说明 |
context |
Object |
被右键点击的对象 |
userData |
object |
用户数据 |
7.2 使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class MenuCommandExample { [MenuItem("CONTEXT/Transform/Log Info")] static void LogTransformInfo(MenuCommand command) { Transform transform = command.context as Transform; if (transform != null) { Debug.Log($"Transform 名称: {transform.name}"); Debug.Log($"位置: {transform.position}"); Debug.Log$"层级: {transform.hierarchyCount}"); } }
[MenuItem("CONTEXT/Component/Show Type")] static void ShowComponentType(MenuCommand command) { Component component = command.context as Component; if (component != null) { Debug.Log($"组件类型: {component.GetType().FullName}"); Debug.Log($"所在的 GameObject: {component.gameObject.name}"); } } }
|
八、完整示例:批量操作工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| using UnityEngine; using UnityEditor;
public class BatchOperations { [MenuItem("Tools/Batch/Rename Selected")] static void BatchRenameSelected() { string prefix = "Object_"; int startNumber = 1;
GameObject[] selected = Selection.gameObjects; if (selected.Length == 0) { EditorUtility.DisplayDialog("提示", "请先选中 GameObject", "确定"); return; }
Undo.RecordObjects(selected, "Batch Rename");
for (int i = 0; i < selected.Length; i++) { selected[i].name = $"{prefix}{startNumber + i}"; }
Debug.Log($"已重命名 {selected.Length} 个物体"); }
[MenuItem("Tools/Batch/Rename Selected", true)] static bool ValidateBatchRenameSelected() { return Selection.gameObjects.Length > 0; }
[MenuItem("Tools/Batch/Add Rigidbody to Selection")] static void BatchAddRigidbody() { GameObject[] selected = Selection.gameObjects; if (selected.Length == 0) return;
Undo.RecordObjects(selected, "Add Rigidbody");
foreach (GameObject obj in selected) { if (obj.GetComponent<Rigidbody>() == null) { obj.AddComponent<Rigidbody>(); } }
Debug.Log($"已为 {selected.Length} 个物体添加 Rigidbody"); }
[MenuItem("Tools/Batch/Set Layer")] static void BatchSetLayer() { int selectedLayer = EditorGUILayout.LayerField("目标层", 0);
if (!EditorUtility.DisplayDialog("确认", $"将所有选中物体设置到层 {LayerMask.LayerToName(selectedLayer)}?", "确定", "取消")) { return; }
GameObject[] selected = Selection.gameObjects; Undo.RecordObjects(selected, "Set Layer");
foreach (GameObject obj in selected) { obj.layer = selectedLayer; }
Debug.Log($"已将 {selected.Length} 个物体设置到层 {LayerMask.LayerToName(selectedLayer)}"); }
[MenuItem("Tools/Batch/Set Layer", true)] static bool ValidateBatchSetLayer() { return Selection.gameObjects.Length > 0; } }
|
九、快捷键速查表
| 快捷键代码 |
Windows |
macOS |
说明 |
% |
Ctrl |
Cmd |
Control |
# |
Shift |
Shift |
Shift |
& |
Alt |
Alt |
Alt |
_%g |
Ctrl+Shift+G |
Cmd+Shift+G |
组合键 |
#_g |
Shift+G |
Shift+G |
Shift组合 |
F1 |
F1 |
F1 |
功能键 |
LEFT |
左方向键 |
左方向键 |
方向键 |
常用快捷键示例
1 2 3 4 5 6
| [MenuItem("Tools/Save %s")] [MenuItem("Tools/Open %o")] [MenuItem("Tools/Save %#s")] [MenuItem("Tools/Undo %z")] [MenuItem("Tools/Redo %#z")] [MenuItem("Tools/Delete #d")]
|
十、总结
本文介绍了 Unity MenuItem 的开发要点:
| 主题 |
要点 |
| 基本用法 |
[MenuItem("路径")] 静态方法 |
| 优先级 |
priority 参数,越小越靠前 |
| 验证函数 |
同名 bool 方法控制启用状态 |
| 勾选状态 |
Menu.GetChecked/SetChecked |
| 快捷键 |
路径后添加特殊字符 |
| 上下文菜单 |
CONTEXT/... 路径 |
| 资源菜单 |
Assets/... 路径 |
| GameObject菜单 |
GameObject/... 路径 |
💡 开发建议:
- 使用有意义的菜单路径,便于组织
- 优先级使用 10、100 等标准间隔
- 总是为菜单项添加验证函数
- 快捷键选择不常用组合,避免冲突
- 批量操作使用 Undo 记录,支持撤销
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com