📋 Unity MenuItem 完全指南:打造高效编辑器菜单系统

💡 菜单系统的力量

  • 想为团队打造统一的工具菜单入口?
  • 快捷键怎么设置,哪些符号代表什么按键?
  • 如何实现菜单项的动态启用/禁用?
  • 怎样在右键菜单中添加自定义选项?

这篇文章! 将系统讲解 MenuItem 的所有高级用法,帮你打造专业级的编辑器菜单系统!

一、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
{
// 在 Tools 菜单下添加菜单项
[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 验证函数名()
{
// 返回 true:菜单项启用
// 返回 false:菜单项禁用(灰色显示)
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()
{
// 选中物体必须没有 Rigidbody 组件
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")]   // Ctrl+G
[MenuItem("Tools/Quick Action %#g")] // Ctrl+Shift+G
[MenuItem("Tools/Quick Action &g")] // Alt+G
[MenuItem("Tools/Quick Action _g")] // 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
{
// Transform 组件的右键菜单
[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;
}
}

// 验证:只对 Transform 组件有效
[MenuItem("CONTEXT/Transform/Reset to Zero", true)]
static bool ValidateResetTransform()
{
return Selection.activeTransform != null;
}

// Rigidbody 组件的右键菜单
[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
{
// Project 窗口右键菜单
[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
{
// Hierarchy 窗口右键菜单
[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")]           // Ctrl+S
[MenuItem("Tools/Open %o")] // Ctrl+O
[MenuItem("Tools/Save %#s")] // Ctrl+Shift+S
[MenuItem("Tools/Undo %z")] // Ctrl+Z
[MenuItem("Tools/Redo %#z")] // Ctrl+Shift+Z
[MenuItem("Tools/Delete #d")] // Shift+D

十、总结

本文介绍了 Unity MenuItem 的开发要点:

主题 要点
基本用法 [MenuItem("路径")] 静态方法
优先级 priority 参数,越小越靠前
验证函数 同名 bool 方法控制启用状态
勾选状态 Menu.GetChecked/SetChecked
快捷键 路径后添加特殊字符
上下文菜单 CONTEXT/... 路径
资源菜单 Assets/... 路径
GameObject菜单 GameObject/... 路径

💡 开发建议

  • 使用有意义的菜单路径,便于组织
  • 优先级使用 10、100 等标准间隔
  • 总是为菜单项添加验证函数
  • 快捷键选择不常用组合,避免冲突
  • 批量操作使用 Undo 记录,支持撤销

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com