Unity编辑器扩展实战:MenuItem与ContextMenu高效开发指南
1. 项目概述为什么MenuItem和ContextMenu是Unity编辑器扩展的基石在Unity编辑器里做开发时间长了你会发现很多重复性的操作特别磨人。比如给一堆选中的图片批量生成材质球或者快速为某个脚本生成配套的编辑器工具。每次都要手动点开菜单、找到功能、执行效率低不说还容易出错。这时候MenuItem和ContextMenu这两个特性Attribute的价值就凸显出来了。它们不是什么高深莫测的黑科技而是Unity提供给开发者用来“装修”自己编辑器工作流的最实用工具。简单来说MenuItem让你能在Unity顶部菜单栏如Assets、GameObject、Window添加自定义菜单项而ContextMenu则允许你为组件脚本的Inspector面板右键菜单添加专属功能。这不仅仅是加个按钮那么简单它意味着你可以将任何一段逻辑比如资源处理、场景操作、数据导出封装成一个直观的点击操作极大地提升了开发效率和工具链的自动化程度。无论是独立开发者还是团队中的TA技术美术或工具程序员掌握它们的实战应用与扩展技巧都是迈向高效开发的关键一步。2. 核心概念与基础用法拆解在深入实战之前我们必须把这两个特性的基础用法和核心区别吃透。很多新手容易混淆用错了地方导致功能不生效白白浪费时间。2.1 MenuItem编辑器全局菜单的定制入口MenuItem是一个类级别的特性它修饰的是一个static方法。这个方法会成为Unity编辑器主菜单栏中的一个选项。它的核心参数是菜单路径。using UnityEditor; using UnityEngine; public class CustomMenuExamples { // 基础用法在 Assets 菜单下创建子菜单 [MenuItem(Assets/My Tools/Process Selected Textures)] private static void ProcessTextures() { // 获取在Project窗口中选择的所有对象 foreach (var selectedObj in Selection.objects) { if (selectedObj is Texture2D texture) { Debug.Log($处理纹理: {texture.name}); // 这里可以添加实际的纹理处理逻辑例如修改导入设置、生成材质等 } } } // 添加分隔符和快捷键 [MenuItem(MyCustomMenu/功能A %#a)] // Ctrl/Cmd Shift A private static void FunctionA() { Debug.Log(快捷键触发了功能A); } // 在 GameObject 菜单中添加项 [MenuItem(GameObject/MyCategory/Create Special Object, false, 10)] private static void CreateSpecialObject() { var go new GameObject(SpecialObject); // 可在此处为GameObject添加特定组件或进行其他初始化 } }关键点解析路径定义Assets/My Tools/Process Selected Textures。Assets是顶级菜单名My Tools是子菜单Process Selected Textures是最终的菜单项。Unity会自动根据/来创建层级结构。静态方法被MenuItem修饰的方法必须是static的。因为菜单项属于编辑器本身不依赖于任何特定的游戏对象实例。菜单验证MenuItem构造函数的第二个参数isValidateFunction默认为false。如果设为true则该方法不会执行操作而是用于验证对应菜单项是否应该启用变灰。通常需要成对使用。优先级第三个参数priority用于控制菜单项在同一个菜单下的显示顺序。数字越小位置越靠上。注意所有使用MenuItem的脚本必须放在项目的Editor文件夹或其子目录下否则Unity在编译时不会将其识别为编辑器代码菜单项也就不会出现。这是新手最容易踩的坑。2.2 ContextMenu组件脚本的专属右键菜单与MenuItem的全局性不同ContextMenu是成员级别的特性它修饰的是一个普通的实例方法非静态。这个菜单项只会出现在挂载了该脚本的组件的Inspector面板的右键菜单中。using UnityEngine; public class MyComponent : MonoBehaviour { public int someValue 10; // 基础用法为这个组件添加一个右键菜单项 [ContextMenu(Do Something Special)] private void DoSpecialAction() { Debug.Log($在 {gameObject.name} 上执行特殊操作当前值: {someValue}); someValue * 2; // 注意这里可以访问this操作当前实例的数据 } // 带参数的菜单项通过方法名模拟 [ContextMenu(Log Current Value)] private void LogValue() { Debug.Log(someValue); } // 另一个菜单项 [ContextMenu(Reset Value)] private void ResetToDefault() { someValue 10; Debug.Log(值已重置); } }关键点解析实例方法被ContextMenu修饰的方法可以是private的但它是一个实例方法。当你在Inspector中右键点击该组件并选择菜单项时Unity会调用当前选中对象上该组件实例的这个方法。作用域局限它的功能只对挂载了该脚本的特定游戏对象有效非常适合封装一些针对该组件数据的快捷操作比如调试、重置、或执行某个依赖当前状态的计算。无需Editor文件夹ContextMenu是运行时特性脚本可以放在任何地方如Scripts文件夹在Play模式和编辑模式下都可用。核心区别总结表特性MenuItemContextMenu作用域编辑器全局主菜单栏单个组件实例Inspector右键修饰方法static方法实例方法非静态脚本位置必须放在Editor文件夹下可放在任何文件夹如Scripts主要用途创建通用的编辑器工具、批量处理资源为特定组件添加调试、初始化或快捷操作访问数据通过Selection、AssetDatabase等API获取可直接访问this及组件成员变量理解了这个根本区别你就能在正确的场景选择正确的工具避免“为什么我的菜单出不来”这类基础问题。3. 实战进阶让菜单变得更智能基础功能只是开始真正提升效率在于让菜单“智能”起来例如根据选择状态动态启用/禁用、处理不同类型资源、甚至创建复杂的多级菜单。3.1 动态验证与条件式菜单项我们经常希望某个菜单项只在特定条件下可用比如“优化纹理”菜单只在选中了纹理文件时才亮起。这需要用到MenuItem的验证功能。using UnityEditor; using UnityEngine; public class SmartMenuExamples { // 执行菜单功能的函数 [MenuItem(Assets/Advanced/Create Material From Texture, false, 100)] private static void CreateMaterialForTexture() { Texture2D tex Selection.activeObject as Texture2D; if (tex ! null) { var material new Material(Shader.Find(Standard)); material.mainTexture tex; // 生成唯一路径并保存资源 string path AssetDatabase.GetAssetPath(tex); string newPath System.IO.Path.ChangeExtension(path, .mat); newPath AssetDatabase.GenerateUniqueAssetPath(newPath); AssetDatabase.CreateAsset(material, newPath); AssetDatabase.SaveAssets(); Debug.Log($已创建材质球: {newPath}); } } // 验证函数决定上述菜单项是否可用 [MenuItem(Assets/Advanced/Create Material From Texture, true, 100)] private static bool ValidateCreateMaterialForTexture() { // 仅当选中单个对象且该对象是Texture2D时启用菜单 return Selection.activeObject ! null Selection.activeObject is Texture2D; } // 更复杂的验证仅当选中了预制体时才启用菜单 [MenuItem(GameObject/My Tools/Analyze Prefab, false, 0)] private static void AnalyzePrefab() { /* 分析逻辑 */ } [MenuItem(GameObject/My Tools/Analyze Prefab, true, 0)] private static bool ValidateAnalyzePrefab() { if (Selection.activeGameObject null) return false; // 检查选中对象是否是预制体实例并且不是场景中的普通对象 return PrefabUtility.IsPartOfAnyPrefab(Selection.activeGameObject); } }实操心得验证函数isValidateFunction设为true必须与对应的执行函数具有相同的菜单路径和优先级。它的返回值是bool类型true表示菜单项可用可点击false则表示禁用变灰。Unity会先调用所有验证函数再根据结果刷新菜单界面。这个机制非常关键它能有效防止用户误操作并清晰地提示当前可用的功能。3.2 处理多种资源类型与批量操作Selection类是我们与编辑器交互的桥梁。Selection.objects或Selection.gameObjects可以获取所有当前选中的对象从而实现批量处理。[MenuItem(Assets/My Tools/Batch Rename Selected)] private static void BatchRename() { // 获取所有选中的资产 Object[] selectedAssets Selection.GetFiltered(typeof(Object), SelectionMode.Assets); if (selectedAssets.Length 0) { EditorUtility.DisplayDialog(提示, 请在Project窗口中选择至少一个资产。, 确定); return; } // 弹出一个输入框让用户输入基础名称 string baseName EditorUtility.DisplayDialog(批量重命名, 请输入基础名称, 确定, 取消); if (string.IsNullOrEmpty(baseName)) return; int counter 1; foreach (Object asset in selectedAssets) { string path AssetDatabase.GetAssetPath(asset); string extension System.IO.Path.GetExtension(path); string newFileName ${baseName}_{counter:000}{extension}; string newPath System.IO.Path.Combine(System.IO.Path.GetDirectoryName(path), newFileName); // 避免名称冲突使用唯一路径 string error AssetDatabase.RenameAsset(path, newFileName); if (!string.IsNullOrEmpty(error)) { Debug.LogWarning($重命名 {asset.name} 失败: {error}); } else { Debug.Log($已重命名: {asset.name} - {newFileName}); } counter; } AssetDatabase.Refresh(); // 重要刷新数据库使更改在Project窗口中立即可见 }注意事项使用AssetDatabase进行任何创建、移动、重命名或删除操作后务必调用AssetDatabase.Refresh()。否则Project窗口可能不会立即更新导致看到的状态与实际文件系统不一致。此外批量操作涉及文件I/O在处理大量资源时可以考虑加入进度条EditorUtility.DisplayProgressBar来提升用户体验。3.3 创建复杂的多级菜单与分隔符通过精心设计菜单路径可以创建清晰、有组织的工具集。使用路径中的/来创建子菜单而MenuItem的优先级参数可以精确控制顺序和插入分隔符。// 利用优先级插入分隔符。同一菜单下优先级相邻项差值大于10时Unity会自动添加分隔符。 [MenuItem(MyCompany/工具集/常用功能/快速截图, false, 0)] private static void TakeScreenshot() { /* ... */ } [MenuItem(MyCompany/工具集/常用功能/清理空文件夹, false, 1)] private static void CleanEmptyFolders() { /* ... */ } // 优先级设为11与上面相差1110所以这里上面会出现一条分隔线 [MenuItem(MyCompany/工具集/高级功能/资源依赖分析, false, 11)] private static void AnalyzeDependencies() { /* ... */ } [MenuItem(MyCompany/工具集/高级功能/性能快照, false, 12)] private static void TakePerformanceSnapshot() { /* ... */ } // 另一个顶级菜单 [MenuItem(MyCompany/设置/项目配置, false, 100)] private static void OpenProjectSettings() { /* ... */ }通过这种方式你可以将团队内部使用的各种工具分门别类地组织起来形成一个专业且易用的编辑器扩展套件。4. 高级扩展技巧与性能优化掌握了基础与常规应用后我们来探讨一些能让你工具更强大、更专业的高级技巧。4.1 利用反射实现动态菜单生成有时我们可能希望根据项目中的某些条件如所有实现了特定接口的类来动态生成菜单项。这需要结合C#的反射Reflection功能。using System; using System.Linq; using System.Reflection; public class DynamicMenuGenerator { // 假设我们有一个接口所有希望提供“生成”菜单的类都实现了它 public interface IGeneratable { void Generate(); } // 一个查找并注册动态菜单的方法可以在[InitializeOnLoadMethod]中调用 [UnityEditor.InitializeOnLoadMethod] private static void RegisterDynamicMenus() { // 查找当前AppDomain中所有实现了IGeneratable接口的非抽象类 var generatableTypes AppDomain.CurrentDomain.GetAssemblies() .SelectMany(asm asm.GetTypes()) .Where(t typeof(IGeneratable).IsAssignableFrom(t) !t.IsAbstract !t.IsInterface) .ToList(); foreach (var type in generatableTypes) { // 为每个类型创建一个菜单项 string menuPath $Assets/Create/Generate/{type.Name}; // 注意由于MenuItem特性需要在编译时确定我们不能直接动态添加。 // 更常见的做法是创建一个统一的菜单项然后根据选择调用不同的逻辑。 } } } // 替代方案使用一个统一的菜单通过反射调用指定方法 [MenuItem(Assets/My Dynamic/Invoke Custom Method)] private static void InvokeCustomMethod() { var selected Selection.activeObject; if (selected null) return; // 假设我们约定在选中对象的同名脚本中找一个名为“CustomMenuAction”的方法 string scriptName selected.name; // ... 这里通过反射查找并执行方法的逻辑略 }重要提示MenuItem特性是编译时决定的无法在运行时通过代码动态添加或移除。上述“动态”通常指的是菜单项的逻辑是动态的或者通过一个统一的入口分发到不同的处理函数。真正的动态菜单需要更底层的EditorGUI API这超出了MenuItem特性的范畴。4.2 结合ScriptableObject创建数据驱动的工具ScriptableObject是Unity中用于存储数据的神器。我们可以创建一种“工具配置”资产然后用菜单项去读取并执行它。using UnityEngine; using UnityEditor; // 1. 创建一个可配置的工具数据 [CreateAssetMenu(fileName BatchProcessorConfig, menuName Tools/Batch Processor Config)] public class BatchProcessorConfig : ScriptableObject { public string searchPattern *.png; public int targetMaxSize 1024; public TextureImporterFormat targetFormat TextureImporterFormat.ASTC_6x6; } // 2. 创建使用该配置的菜单项 public class ConfigDrivenTools { [MenuItem(Tools/Run Batch Processor (Config))] private static void RunWithConfig() { // 让用户选择一个配置文件 string configPath EditorUtility.OpenFilePanel(选择批处理配置, Assets/, asset); if (string.IsNullOrEmpty(configPath)) return; // 将文件路径转换为相对于项目的路径并加载资产 configPath Assets configPath.Substring(Application.dataPath.Length); BatchProcessorConfig config AssetDatabase.LoadAssetAtPathBatchProcessorConfig(configPath); if (config ! null) { Debug.Log($开始批处理模式{config.searchPattern} 目标尺寸{config.targetMaxSize}); // 这里根据config中的参数执行实际的批处理逻辑... // 例如查找所有匹配的纹理并修改其导入设置 ProcessTexturesWithConfig(config); } } private static void ProcessTexturesWithConfig(BatchProcessorConfig config) { // 实现具体的处理逻辑 string[] allTextureGUIDs AssetDatabase.FindAssets($t:Texture2D); foreach (string guid in allTextureGUIDs) { string path AssetDatabase.GUIDToAssetPath(guid); if (System.IO.Path.GetExtension(path).ToLower() .png) // 简单示例实际应使用config.searchPattern { var importer AssetImporter.GetAtPath(path) as TextureImporter; if (importer ! null) { // 根据config修改importer设置... // importer.maxTextureSize config.targetMaxSize; // importer.textureCompression TextureImporterCompression.Compressed; // importer.SaveAndReimport(); } } } AssetDatabase.Refresh(); } }这种方法将工具的逻辑与配置分离。美术或策划人员可以通过创建和修改不同的BatchProcessorConfig资产文件来适应不同的处理需求如针对UI的压缩配置、针对场景贴图的配置而无需程序员修改代码。4.3 性能考量与最佳实践编辑器扩展虽然方便但如果编写不当也可能拖慢编辑器速度。避免在验证方法中进行昂贵操作Validate方法可能会被频繁调用例如每次打开菜单时。确保其中的逻辑尽可能轻量只做简单的类型检查和状态判断。绝对不要在验证方法中进行文件遍历、复杂计算或资源加载。// 差在验证时进行全文搜索 [MenuItem(Assets/Heavy Validate, true)] private static bool BadValidate() { string[] allFiles System.IO.Directory.GetFiles(Application.dataPath, *.prefab, System.IO.SearchOption.AllDirectories); return allFiles.Length 0; // 每次打开菜单都会遍历整个Assets文件夹 } // 好只做快速检查 [MenuItem(Assets/Light Validate, true)] private static bool GoodValidate() { return Selection.activeObject ! null; // 快速检查 }对批量操作使用进度条和延迟操作处理成百上千个资源时使用EditorUtility.DisplayProgressBar显示进度并使用EditorApplication.delayCall或将任务拆分到多个帧中执行防止编辑器卡死无响应。[MenuItem(Assets/Process Massive Batch)] private static void ProcessMassiveBatch() { var allTextures AssetDatabase.FindAssets(t:Texture2D); int total allTextures.Length; for (int i 0; i total; i) { // 更新进度条 if (EditorUtility.DisplayCancelableProgressBar(批量处理, $正在处理纹理 ({i1}/{total}), (float)i / total)) { // 用户点击了取消 break; } // 处理单个纹理... // 如果处理非常耗时可以考虑每处理N个就 yield 一下需要协程在编辑器扩展中稍复杂 // 模拟耗时操作 // System.Threading.Thread.Sleep(10); } EditorUtility.ClearProgressBar(); // 处理完成后务必清理进度条 AssetDatabase.Refresh(); }合理使用InitializeOnLoadMethod这个特性用于在Unity加载编辑器或重新编译脚本后自动执行一些初始化。不要在这里做耗时操作它会影响编辑器的启动速度。通常只用于注册简单的回调或设置静态变量。5. 常见问题排查与调试技巧实录即使经验丰富在开发编辑器菜单时也会遇到各种奇怪的问题。这里记录了一些典型坑位和解决方法。5.1 菜单项不显示或变灰这是最常见的问题可以按照以下清单排查脚本位置包含MenuItem的脚本是否放在了Editor文件夹下这是硬性要求。可以检查脚本文件的Inspector看其Location是否在Editor中。编译错误控制台是否有任何编译错误任何一个脚本错误都可能导致整个编辑器脚本编译失败所有自定义菜单消失。先确保控制台是干净的。方法签名被MenuItem修饰的方法是否是static、private且返回void验证方法第二个参数为true是否返回bool菜单路径冲突是否与其他菜单项路径完全相同优先级是否设置异常可以尝试注释掉其他可能冲突的菜单项代码。编辑器未刷新有时代码编译成功后菜单栏不会立即更新。尝试重新聚焦Unity编辑器窗口或者轻微修改脚本如加个空格触发重新编译。5.2 ContextMenu在预制体模式下不生效这是一个特定场景下的坑。当你选中一个预制体实例并在Inspector中右键时ContextMenu可能不会出现。这是因为Unity在预制体模式下Prefab Mode的上下文处理有所不同。通常ContextMenu在常规场景视图和预制体编辑模式打开预制体单独编辑中工作良好但在项目视图的预制体资产上可能无效。如果需要处理项目视图中的资产应优先考虑使用MenuItem(“Assets/…” )。5.3 Selection对象为null或不是预期类型在MenuItem方法中Selection.activeObject可能为null尤其是在菜单项不是由右键点击触发而是从顶层菜单栏点击时。永远不要假设一定有选中对象。[MenuItem(Assets/My Tool)] private static void MyTool() { // 不安全的做法 // Texture2D tex Selection.activeObject as Texture2D; // activeObject可能为null // 安全的做法使用GetFiltered或检查长度 Object[] selectedTextures Selection.GetFiltered(typeof(Texture2D), SelectionMode.Assets); if (selectedTextures.Length 0) { EditorUtility.DisplayDialog(错误, 请至少选择一个纹理文件。, 确定); return; } // 处理selectedTextures数组... }使用Selection.GetFiltered可以一次性筛选出特定类型的选中对象代码更健壮。5.4 处理Undo操作编辑器工具修改了场景或资源状态必须支持撤销Undo。否则对用户来说将是灾难性的。[MenuItem(GameObject/My Tools/Center Children)] private static void CenterAllChildren() { if (Selection.activeGameObject null) return; Transform parent Selection.activeGameObject.transform; // 在操作开始前记录Undo Undo.RecordObject(parent, Center Children under parent.name); Vector3 avgPos Vector3.zero; foreach (Transform child in parent) { avgPos child.position; } avgPos / parent.childCount; foreach (Transform child in parent) { // 对每个子对象的修改也要记录Undo Undo.RecordObject(child, Adjust child position); child.position - avgPos; } // 修改父对象自身的位置来“抵消”偏移 parent.position avgPos; }核心原则任何会修改场景中UnityEngine.Object如GameObject、Component状态的操作都应该用Undo.RecordObject()包裹起来。对于创建新对象使用Undo.RegisterCreatedObjectUndo()。对于资源Assets的修改AssetDatabase的某些操作会自动集成Undo但复杂操作仍需手动处理。5.5 调试编辑器脚本调试编辑器扩展与调试游戏脚本略有不同。最有效的方法是使用Debug.Log。但请注意在编辑器模式下Debug.Log的输出会显示在Console窗口并且对象会以可点击链接的形式呈现点击可以Ping到该资源或选中该对象。对于更复杂的调试可以开启Editor Log在Windows上可以在Unity编辑器启动时添加-logFile参数日志文件通常位于C:\Users\[用户名]\AppData\Local\Unity\Editor\Editor.log。或者你也可以使用Visual Studio或Rider等IDE附加到Unity编辑器进程进行断点调试步骤与调试游戏模式类似只是需要附加到Unity Editor进程而非游戏进程。6. 综合实战案例构建一个资源快速预览工具让我们将以上所有知识点融会贯通创建一个实用的工具资源快速预览工具。这个工具的目标是在Project窗口右键点击图片、模型、预制体等资源时能在一个弹出的浮动窗口中快速预览其内容而无需双击打开或拖入场景。6.1 工具设计与核心思路功能定义通过MenuItem在Assets右键菜单添加“Quick Preview”项。点击后根据选中资源的类型纹理、模型、预制体在一个自定义的EditorWindow中显示预览。关键技术点使用Selection获取选中资源。使用AssetPreview.GetAssetPreview获取资源的预览图纹理、模型等。对于预制体可能需要实例化到一个临时场景进行预览更复杂本例简化为显示图标。创建一个继承自EditorWindow的浮动窗口。菜单定义[MenuItem(“Assets/Quick Preview”, false, 20)]并添加验证函数确保只在选中单个资源时可用。6.2 核心代码实现首先创建菜单项和验证逻辑。// QuickPreviewMenu.cs using UnityEditor; using UnityEngine; public static class QuickPreviewMenu { [MenuItem(Assets/Quick Preview, false, 20)] private static void OpenQuickPreview() { // 获取选中的资源 Object selectedAsset Selection.activeObject; if (selectedAsset null) return; // 打开预览窗口并传入选中的资源 QuickPreviewWindow.ShowWindow(selectedAsset); } [MenuItem(Assets/Quick Preview, true, 20)] private static bool ValidateOpenQuickPreview() { // 只允许在选中单个资源时显示菜单 return Selection.objects.Length 1; } }接下来实现预览窗口本身。// QuickPreviewWindow.cs using UnityEditor; using UnityEngine; public class QuickPreviewWindow : EditorWindow { private Object targetAsset; private Texture2D previewTexture; private Vector2 scrollPosition; private string assetInfo; public static void ShowWindow(Object asset) { // 获取现有窗口如果没有则创建一个 var window GetWindowQuickPreviewWindow(Quick Preview); window.targetAsset asset; window.RefreshPreview(); window.Show(); } private void RefreshPreview() { if (targetAsset null) return; // 获取资产路径和类型信息 string path AssetDatabase.GetAssetPath(targetAsset); System.IO.FileInfo fileInfo new System.IO.FileInfo(path); assetInfo $名称: {targetAsset.name}\n类型: {targetAsset.GetType().Name}\n路径: {path}\n大小: {(fileInfo.Exists ? (fileInfo.Length / 1024.0).ToString(F2) : N/A)} KB; // 尝试获取资产的预览图 previewTexture AssetPreview.GetAssetPreview(targetAsset); // 如果GetAssetPreview返回null可能资产还未生成预览尝试获取较小的图标 if (previewTexture null) { previewTexture AssetPreview.GetMiniThumbnail(targetAsset); } // 强制重绘窗口 Repaint(); } private void OnGUI() { if (targetAsset null) { EditorGUILayout.LabelField(未选择任何资源。); return; } EditorGUILayout.LabelField(快速预览, EditorStyles.boldLabel); EditorGUILayout.Space(); // 显示资产信息 scrollPosition EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.TextArea(assetInfo, GUILayout.Height(60)); EditorGUILayout.EndScrollView(); EditorGUILayout.Space(); // 显示预览图 if (previewTexture ! null) { // 计算一个合适的显示区域大小保持宽高比 float maxWidth position.width - 40; float maxHeight 300; float ratio (float)previewTexture.width / previewTexture.height; float displayWidth previewTexture.width; float displayHeight previewTexture.height; if (displayWidth maxWidth) { displayWidth maxWidth; displayHeight displayWidth / ratio; } if (displayHeight maxHeight) { displayHeight maxHeight; displayWidth displayHeight * ratio; } Rect previewRect GUILayoutUtility.GetRect(displayWidth, displayHeight); EditorGUI.DrawTextureTransparent(previewRect, previewTexture, ScaleMode.ScaleToFit); } else { EditorGUILayout.HelpBox(无法生成预览。, MessageType.Info); } EditorGUILayout.Space(); // 一些操作按钮 EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(在Finder/Explorer中显示)) { EditorUtility.RevealInFinder(AssetDatabase.GetAssetPath(targetAsset)); } if (GUILayout.Button(Ping到项目视图)) { EditorGUIUtility.PingObject(targetAsset); } if (GUILayout.Button(刷新预览)) { RefreshPreview(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.LabelField($选中对象: {targetAsset.name}, EditorStyles.centeredGreyMiniLabel); } // 当窗口获得焦点时检查当前选中资源是否变化 private void OnFocus() { // 如果窗口打开时用户又选了其他资源可以在这里更新 // 本例中我们不做自动更新保持打开时的状态 } }6.3 功能扩展与优化思考这个基础版本已经可用但还有巨大的优化空间预览类型扩展目前主要预览纹理。可以扩展为预制体在窗口内创建一个小的预览场景PreviewRenderUtility实时渲染预制体。音频显示波形图并集成一个简单的播放按钮。动画片段显示一个可拖动的进度条来预览动画。性能优化AssetPreview.GetAssetPreview可能会阻塞主线程直到预览生成完毕。对于大型模型这可能导致窗口卡顿。可以考虑异步加载或在后台线程生成预览。UI美化使用EditorGUILayout和EditorGUI的更多控件来美化窗口比如添加一个资源图标列表支持同时预览多个资源。快捷键集成为菜单项分配快捷键如%#p并实现窗口的快捷键如按ESC关闭。通过这个案例你将MenuItem、EditorWindow、AssetPreview、Selection、AssetDatabase等多个编辑器API串联了起来构建了一个真正提升工作流效率的实用工具。这比任何零散的知识点都更有价值。