Unity编辑器扩展实战:MenuItem与ContextMenu高效开发指南

📅 2026/7/13 6:43:41 👁️ 阅读次数 📝 编程学习
Unity编辑器扩展实战:MenuItem与ContextMenu高效开发指南

1. 项目概述:为什么MenuItem和ContextMenu是Unity编辑器扩展的基石

在Unity编辑器里做开发,时间长了你会发现,很多重复性的操作特别磨人。比如,给一堆选中的图片批量生成材质球,或者快速为某个脚本生成配套的编辑器工具。每次都要手动点开菜单、找到功能、执行,效率低不说,还容易出错。这时候,MenuItemContextMenu这两个特性(Attribute)的价值就凸显出来了。它们不是什么高深莫测的黑科技,而是Unity提供给开发者,用来“装修”自己编辑器工作流的最实用工具。

简单来说,MenuItem让你能在Unity顶部菜单栏(如AssetsGameObjectWindow)添加自定义菜单项,而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添加特定组件或进行其他初始化 } }

关键点解析:

  1. 路径定义"Assets/My Tools/Process Selected Textures"Assets是顶级菜单名,My Tools是子菜单,Process Selected Textures是最终的菜单项。Unity会自动根据/来创建层级结构。
  2. 静态方法:被MenuItem修饰的方法必须是static。因为菜单项属于编辑器本身,不依赖于任何特定的游戏对象实例。
  3. 菜单验证MenuItem构造函数的第二个参数(isValidateFunction)默认为false。如果设为true,则该方法不会执行操作,而是用于验证对应菜单项是否应该启用(变灰)。通常需要成对使用。
  4. 优先级:第三个参数(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("值已重置"); } }

关键点解析:

  1. 实例方法:被ContextMenu修饰的方法可以是private的,但它是一个实例方法。当你在Inspector中右键点击该组件并选择菜单项时,Unity会调用当前选中对象上该组件实例的这个方法。
  2. 作用域局限:它的功能只对挂载了该脚本的特定游戏对象有效,非常适合封装一些针对该组件数据的快捷操作,比如调试、重置、或执行某个依赖当前状态的计算。
  3. 无需Editor文件夹ContextMenu是运行时特性,脚本可以放在任何地方(如Scripts文件夹),在Play模式和编辑模式下都可用。

核心区别总结表:

特性MenuItemContextMenu
作用域编辑器全局(主菜单栏)单个组件实例(Inspector右键)
修饰方法static方法实例方法(非静态)
脚本位置必须放在Editor文件夹下可放在任何文件夹(如Scripts
主要用途创建通用的编辑器工具、批量处理资源为特定组件添加调试、初始化或快捷操作
访问数据通过SelectionAssetDatabase等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.objectsSelection.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,与上面相差11>10,所以这里上面会出现一条分隔线 [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.LoadAssetAtPath<BatchProcessorConfig>(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 性能考量与最佳实践

编辑器扩展虽然方便,但如果编写不当,也可能拖慢编辑器速度。

  1. 避免在验证方法中进行昂贵操作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; // 快速检查 }
  2. 对批量操作使用进度条和延迟操作:处理成百上千个资源时,使用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("批量处理", $"正在处理纹理 ({i+1}/{total})", (float)i / total)) { // 用户点击了取消 break; } // 处理单个纹理... // 如果处理非常耗时,可以考虑每处理N个就 yield 一下(需要协程,在编辑器扩展中稍复杂) // 模拟耗时操作 // System.Threading.Thread.Sleep(10); } EditorUtility.ClearProgressBar(); // 处理完成后务必清理进度条 AssetDatabase.Refresh(); }
  3. 合理使用InitializeOnLoadMethod:这个特性用于在Unity加载编辑器或重新编译脚本后自动执行一些初始化。不要在这里做耗时操作,它会影响编辑器的启动速度。通常只用于注册简单的回调或设置静态变量。

5. 常见问题排查与调试技巧实录

即使经验丰富,在开发编辑器菜单时也会遇到各种奇怪的问题。这里记录了一些典型坑位和解决方法。

5.1 菜单项不显示或变灰

这是最常见的问题,可以按照以下清单排查:

  1. 脚本位置:包含MenuItem的脚本是否放在了Editor文件夹下?这是硬性要求。可以检查脚本文件的Inspector,看其Location是否在Editor中。
  2. 编译错误:控制台是否有任何编译错误?任何一个脚本错误都可能导致整个编辑器脚本编译失败,所有自定义菜单消失。先确保控制台是干净的。
  3. 方法签名:被MenuItem修饰的方法是否是staticprivate且返回void?验证方法(第二个参数为true)是否返回bool
  4. 菜单路径冲突:是否与其他菜单项路径完全相同?优先级是否设置异常?可以尝试注释掉其他可能冲突的菜单项代码。
  5. 编辑器未刷新:有时代码编译成功后,菜单栏不会立即更新。尝试重新聚焦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(如GameObjectComponent)状态的操作,都应该用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 工具设计与核心思路

  1. 功能定义:通过MenuItem在Assets右键菜单添加“Quick Preview”项。点击后,根据选中资源的类型(纹理、模型、预制体),在一个自定义的EditorWindow中显示预览。
  2. 关键技术点
    • 使用Selection获取选中资源。
    • 使用AssetPreview.GetAssetPreview获取资源的预览图(纹理、模型等)。
    • 对于预制体,可能需要实例化到一个临时场景进行预览(更复杂,本例简化为显示图标)。
    • 创建一个继承自EditorWindow的浮动窗口。
  3. 菜单定义[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 = GetWindow<QuickPreviewWindow>("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 功能扩展与优化思考

这个基础版本已经可用,但还有巨大的优化空间:

  1. 预览类型扩展:目前主要预览纹理。可以扩展为:
    • 预制体:在窗口内创建一个小的预览场景(PreviewRenderUtility),实时渲染预制体。
    • 音频:显示波形图,并集成一个简单的播放按钮。
    • 动画片段:显示一个可拖动的进度条来预览动画。
  2. 性能优化AssetPreview.GetAssetPreview可能会阻塞主线程直到预览生成完毕。对于大型模型,这可能导致窗口卡顿。可以考虑异步加载或在后台线程生成预览。
  3. UI美化:使用EditorGUILayoutEditorGUI的更多控件来美化窗口,比如添加一个资源图标列表,支持同时预览多个资源。
  4. 快捷键集成:为菜单项分配快捷键(如%#p),并实现窗口的快捷键(如按ESC关闭)。

通过这个案例,你将MenuItemEditorWindowAssetPreviewSelectionAssetDatabase等多个编辑器API串联了起来,构建了一个真正提升工作流效率的实用工具。这比任何零散的知识点都更有价值。