尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity编辑器扩展终极指南:50+实战工具提升开发效率

Unity编辑器扩展终极指南:50+实战工具提升开发效率 1. 项目概述为什么我们需要一本“终极”编辑器扩展指南如果你在Unity开发这条路上已经摸爬滚打了一段时间大概率会和我有同样的感受每天重复的机械操作比如批量重命名资源、手动调整预制体引用、一遍遍检查场景设置正在一点点吞噬你的创造力和开发热情。Unity编辑器本身很强大但它毕竟是一个通用平台不可能为每个团队、每个项目量身定制所有功能。这时候编辑器扩展Editor Extensions就不再是“锦上添花”的炫技而是实实在在的生产力倍增器是资深开发者与初级开发者拉开效率差距的关键分水岭。我见过太多开发者包括几年前的我自己对编辑器扩展抱有敬畏之心觉得那是引擎源码贡献者或者工具程序员才需要涉足的领域。实际上Unity为我们提供了极其友好且强大的UnityEditorAPI它就像一套乐高积木允许我们根据自己的工作流搭建出专属的自动化工具、自定义检视面板、场景辅助工具甚至是全新的编辑器窗口。这本“终极指南”的目的就是帮你彻底打破这层心理和技术壁垒将50多个经过实战检验的工具思路与实现方案系统地交付给你。这不是简单的API罗列而是聚焦于“解决实际开发中的高频痛点”从资源管理、场景编辑、工作流自动化到团队协作覆盖开发全链路。掌握之后你将能构建一个高度个性化、极度高效的开发环境把时间真正还给创意和逻辑本身。2. 核心设计思路从“用工具”到“造工具”的思维转变在深入具体工具之前我们必须先统一思想开发编辑器扩展的核心目标是什么我的答案很明确——消除重复劳动固化最佳实践并降低人为操作失误的风险。一个好的编辑器工具应该像一位不知疲倦的助手默默帮你处理好所有繁琐的细节。2.1 工具设计的三个层级根据复杂度和集成深度我将编辑器扩展工具分为三个层级菜单命令与快捷键MenuItem这是最轻量级的扩展。通过[MenuItem(“路径”)]属性你可以将任何静态方法挂载到Unity的菜单栏或右键菜单并绑定快捷键。它适合执行一些独立的、无状态的操作比如“一键清理空文件夹”、“批量修改材质球属性”。它的优势是开发简单、侵入性低。自定义Inspector与PropertyDrawer当默认的组件检视面板无法满足你的需求时就需要定制Inspector。例如为一个复杂的任务系统组件提供一个带折叠分类、颜色编码、一键配置的友好界面。而PropertyDrawer则用于自定义特定类型如枚举、结构体在Inspector中的绘制方式让序列化字段的编辑体验更佳。这个层级的核心是改善数据编辑体验。编辑器窗口EditorWindow与场景工具SceneView这是功能最强大的扩展形式。EditorWindow允许你创建全新的独立工具窗口像Animation窗口、Console窗口一样。你可以在这里集成复杂的工作流比如资源批量处理面板、配置表编辑器。SceneView扩展则允许你在场景视图中绘制Gizmos、处理鼠标事件实现可视化的编辑工具比如地形笔刷、路径点编辑器。2.2 工具开发的“第一性原理”在动手写任何代码之前先问自己几个问题这个操作我一天/一周要重复多少次频率越高工具的回报率越大。这个操作是否有固定的模式或规则如果是就非常适合自动化。手动操作容易在哪个环节出错工具的核心价值之一就是规避人为失误。这个工具是给我自己用还是给团队用个人使用的工具可以追求极致的快捷和个性化团队使用的工具则必须考虑稳定性、易用性和清晰的错误提示。基于这些原则我们设计的工具才会直击痛点而不是为了扩展而扩展。3. 核心细节解析UnityEditor API 关键模块剖析工欲善其事必先利其器。深入理解UnityEditor命名空间下的几个核心类是高效开发扩展的基础。3.1 Selection 与 AssetDatabase资源操作的基石几乎所有的资源管理类工具都绕不开这两个类。Selection它代表了在Project窗口或Hierarchy窗口中被选中的对象。Selection.activeObject获取主选中对象Selection.objects获取所有选中对象。通过它我们可以知道用户当前想对什么进行操作。注意Selection中的对象可能是场景中的GameObjectGameObject类型也可能是项目中的资源UnityEngine.Object类型如Texture、Material。在处理时需要做好类型判断。AssetDatabase这是访问和管理项目资源Assets的核心入口。它的功能包括路径与GUID转换AssetDatabase.AssetPathToGUID,GUIDToAssetPath。GUID是Unity内部识别资源的唯一ID比路径更可靠。加载与查找资源AssetDatabase.LoadAssetAtPath,AssetDatabase.FindAssets使用搜索过滤字符串。导入与刷新AssetDatabase.ImportAsset,AssetDatabase.Refresh。在代码中创建或修改资源后必须调用Refresh编辑器才能识别到变化。创建、删除与移动AssetDatabase.CreateAsset,DeleteAsset,MoveAsset。实操心得批量操作资源时务必在操作开始前调用AssetDatabase.StartAssetEditing()在操作结束后调用AssetDatabase.StopAssetEditing()。这会将多次资源导入合并为一次极大提升批量处理速度避免编辑器频繁刷新卡顿。3.2 EditorWindow 与 Editor用户界面的构建块EditorWindow创建工具窗口的基类。你需要继承它并实现OnGUI方法。using UnityEditor; using UnityEngine; public class MyToolWindow : EditorWindow { [MenuItem(“Tools/My Tool”)] static void ShowWindow() { var window GetWindowMyToolWindow(); window.titleContent new GUIContent(“我的工具”); window.Show(); } private string _inputText “”; void OnGUI() { // 使用GUILayout或EditorGUILayout绘制界面 _inputText EditorGUILayout.TextField(“输入内容”, _inputText); if (GUILayout.Button(“执行”)) { Debug.Log(_inputText); } } }OnGUI每帧都会被调用你需要在这里处理所有的界面绘制和用户交互逻辑。GUILayout和EditorGUILayout提供了自动布局和仿编辑器样式的控件是首选。Editor与EditorGUI/EditorGUILayoutEditor类用于为自定义的MonoBehaviour或ScriptableObject创建自定义Inspector。而EditorGUI和EditorGUILayout则是绘制这些UI控件的底层和自动布局版本。EditorGUILayout在大多数情况下更易用。重要技巧在自定义Inspector中如果要保持序列化字段的撤销Undo功能必须使用EditorGUI.BeginChangeCheck()和EditorGUI.EndChangeCheck()包裹对序列化属性的修改并在检测到变化时调用serializedObject.ApplyModifiedProperties()。3.3 SerializedObject 与 SerializedProperty安全操作序列化数据这是自定义Inspector时最重要且最安全的数据操作方式。直接修改目标对象的字段虽然简单但会绕过Unity的序列化系统和撤销系统。public override void OnInspectorGUI() { // 更新序列化对象表示 serializedObject.Update(); // 获取序列化属性并绘制 SerializedProperty nameProp serializedObject.FindProperty(“name”); EditorGUILayout.PropertyField(nameProp); SerializedProperty levelProp serializedObject.FindProperty(“level”); levelProp.intValue EditorGUILayout.IntSlider(“等级”, levelProp.intValue, 1, 100); // 应用修改 if (serializedObject.ApplyModifiedProperties()) { // 数据已成功应用可以在这里执行一些后续操作如标记场景为脏、保存资源等。 } }使用SerializedProperty你可以确保所有的修改都能被正确撤销CtrlZ并且能自动处理多对象编辑同时选中多个相同组件进行批量修改。4. 实战工具箱50效率工具分类详解与实现下面我将这些工具分为六大类并每类挑选几个最具代表性的讲解其实现思路与核心代码片段。4.1 资源管理与批量处理工具这是应用最广泛的工具类型能直接节省大量重复性体力劳动。工具1一键清理空文件夹痛点项目开发中经常会产生大量空文件夹手动查找删除极其麻烦。实现思路使用Directory.GetDirectories递归获取所有子目录再使用Directory.GetFiles判断文件夹是否为空。注意要排除Meta文件。核心代码片段[MenuItem(“Assets/工具/清理空文件夹”, false, 20)] static void DeleteEmptyFolders() { string[] allDirs Directory.GetDirectories(Application.dataPath, “*”, SearchOption.AllDirectories); Liststring emptyDirs new Liststring(); foreach (var dir in allDirs) { if (Directory.GetFiles(dir, “*”, SearchOption.AllDirectories).Length 0) { // 只包含.meta文件也算空文件夹 string[] files Directory.GetFiles(dir); if (files.Length 0 || (files.Length 1 files[0].EndsWith(“.meta”))) { emptyDirs.Add(dir); } } } // 从最深层的文件夹开始删除避免先删父文件夹导致路径无效 emptyDirs.Sort((a, b) b.Length.CompareTo(a.Length)); foreach (var dir in emptyDirs) { string assetPath “Assets” dir.Replace(Application.dataPath, “”).Replace(‘\\’, ‘/’); AssetDatabase.MoveAssetToTrash(assetPath); Debug.Log($“已删除空文件夹: {assetPath}”); } AssetDatabase.Refresh(); }工具2资源引用查找器痛点想删除一个材质或预制体但不确定哪些地方还在使用它怕引发引用丢失。实现思路利用AssetDatabase.GetDependencies可以找到一个资源的所有直接依赖。但要找到“谁引用了它”反向查找则需要遍历项目中的所有潜在引用资源如预制体、场景、ScriptableObject检查其序列化数据中是否包含目标资源的GUID。这是一个较耗时的操作通常需要搭配进度条EditorUtility.DisplayProgressBar使用。核心技巧遍历所有.prefab、.unity、.asset文件使用AssetDatabase.LoadAllAssetsAtPath加载然后通过SerializedObject遍历其所有SerializedProperty检查property.objectReferenceValue是否与目标资源匹配。更高效的做法是只检查property.propertyType SerializedPropertyType.ObjectReference的属性。工具3批量重命名与前缀/后缀工具痛点对一批资源进行规范化命名。实现思路获取Selection.objects过滤出需要重命名的资源类型如Texture然后使用AssetDatabase.RenameAsset。关键是要处理好名称冲突和撤销操作。扩展功能可以设计一个EditorWindow提供“添加前缀”、“添加后缀”、“替换字符串”、“序列数字编号”等多种规则并支持预览重命名结果。4.2 场景编辑与层级管理工具这类工具直接在场景视图SceneView或层级视图Hierarchy中提升操作效率。工具4场景物体快速对齐与分布工具痛点手动对齐多个GameObject的位置、旋转或缩放非常耗时且不精确。实现思路创建一个EditorWindow提供“顶对齐”、“底对齐”、“左对齐”、“水平居中分布”、“垂直等间距分布”等按钮。点击后获取Selection.transforms计算出基准值如平均位置、最小Y值等然后批量修改选中物体的Transform组件。核心代码水平居中分布if (Selection.transforms.Length 2) { var transforms Selection.transforms.OrderBy(t t.position.x).ToArray(); float minX transforms.First().position.x; float maxX transforms.Last().position.x; float spacing (maxX - minX) / (transforms.Length - 1); for (int i 0; i transforms.Length; i) { Vector3 pos transforms[i].position; pos.x minX i * spacing; transforms[i].position pos; } }工具5层级视图快速创建预设结构痛点某些类型的GameObject如UI面板、角色有固定的子节点结构每次手动创建很繁琐。实现思路通过[MenuItem(“GameObject/我的预设/创建UI面板”)]菜单在右键点击的父节点下或场景根节点用代码实例化一个预设的层级结构。这里不是指Prefab实例化而是直接用new GameObject创建一组有固定名称、组件和层级的空对象。[MenuItem(“GameObject/我的预设/创建UI面板”, false, 10)] static void CreateUIPanel() { GameObject panel new GameObject(“UIPanel”); panel.AddComponentCanvasRenderer(); RectTransform rt panel.AddComponentRectTransform(); rt.anchorMin Vector2.zero; rt.anchorMax Vector2.one; rt.sizeDelta Vector2.zero; // 设置选中状态方便后续操作 Selection.activeGameObject panel; }工具6场景视图辅助绘制Gizmos与Handles痛点需要在场景中可视化一些逻辑信息如敌人的巡逻范围、触发器的区域、技能作用距离。实现思路为组件编写OnDrawGizmos或OnDrawGizmosSelected方法。使用Gizmos.DrawWireSphere,Gizmos.DrawLine,Handles.DrawWireArc等API进行绘制。对于更复杂的交互如用鼠标拖拽Gizmo来修改参数需要使用HandlesAPI并在OnSceneGUI方法中处理事件。void OnDrawGizmosSelected() { Gizmos.color Color.yellow; Gizmos.DrawWireSphere(transform.position, attackRange); Handles.color new Color(1, 0, 0, 0.2f); Handles.DrawSolidArc(transform.position, Vector3.up, transform.forward, viewAngle / 2, viewDistance); Handles.DrawSolidArc(transform.position, Vector3.up, transform.forward, -viewAngle / 2, viewDistance); }4.3 工作流与自动化工具将一系列固定操作串联起来实现“一键完成”。工具7一键构建与后处理工具痛点构建应用Build后总有一些重复操作比如复制文件到特定目录、重命名输出包、上传到测试服务器等。实现思路利用Unity的IPostprocessBuildWithReport接口。创建一个脚本实现该接口Unity会在构建完成后自动调用它的OnPostprocessBuild方法。你可以在这里添加自定义逻辑。using UnityEditor.Build; using UnityEditor.Build.Reporting; public class MyBuildPostprocessor : IPostprocessBuildWithReport { public int callbackOrder { get { return 0; } } public void OnPostprocessBuild(BuildReport report) { string outputPath report.summary.outputPath; Debug.Log($“构建完成路径: {outputPath}”); // 在此处添加你的后处理逻辑例如 // FileUtil.CopyFileOrDirectory(outputPath, “../Backup/”); // 调用外部批处理脚本等。 } }注意构建后处理脚本必须放在Editor文件夹下。工具8自动化资源导入设置Postprocessor痛点不同类型的资源如图片、音频、模型需要不同的导入设置Max Size、压缩格式、Wrap Mode等。新加入的成员或从网上下载的资源经常忘记设置导致资源臃肿或表现不正确。实现思路使用AssetPostprocessor。你可以创建针对纹理、模型、音频的处理器在资源被导入时自动应用预设规则。using UnityEditor; public class TextureImportProcessor : AssetPostprocessor { void OnPreprocessTexture() { TextureImporter importer assetImporter as TextureImporter; if (importer null) return; // 根据路径规则设置 if (assetPath.Contains(“/UI/”)) { importer.textureType TextureImporterType.Sprite; importer.mipmapEnabled false; importer.maxTextureSize 2048; importer.SetPlatformTextureSettings(new TextureImporterPlatformSettings { name “Android”, overridden true, maxTextureSize 1024, format TextureImporterFormat.ASTC_6x6 }); } else if (assetPath.Contains(“/Textures/”)) { // 普通纹理设置... } } }工具9自定义项目初始化模板痛点每次开新项目都要手动创建一堆标准文件夹如Scripts,Art,Prefabs,Scenes配置一些通用设置非常浪费时间。实现思路创建一个InitializeProject工具通过菜单或第一次打开项目时弹出的窗口触发。工具用Directory.CreateDirectory创建标准目录结构用AssetDatabase.CreateAsset创建必要的配置资产如TagManager、InputManager的预设或项目专用的ScriptableObject配置表。4.4 调试与开发辅助工具帮助你在开发阶段更快地定位问题、测试功能。工具10运行时数据监视器痛点想在游戏运行时在编辑器里实时查看某个复杂对象如游戏状态机、背包系统、AI行为树的内部数据但不想每次都写一堆Debug.Log。实现思路创建一个EditorWindow在OnGUI中你可以通过Object.FindObjectOfType或静态实例访问到你的游戏管理器然后以友好的格式如折叠框、列表、图表将其关键数据打印出来。在Update方法中调用Repaint()可以使其每秒多次刷新实现实时监控。void OnGUI() { GameManager gm GameManager.Instance; if (gm null) return; EditorGUILayout.LabelField(“玩家金币”, gm.PlayerGold.ToString()); EditorGUILayout.LabelField(“游戏状态”, gm.CurrentState.ToString()); // 绘制一个List或Dictionary EditorGUILayout.LabelField(“背包物品:”); foreach (var item in gm.Inventory) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(item.Key); EditorGUILayout.LabelField(item.Value.ToString()); EditorGUILayout.EndHorizontal(); } } void Update() { // 每秒重绘10次 if (EditorApplication.timeSinceStartup % 0.1f Time.deltaTime) Repaint(); }工具11一键场景设置检查器痛点项目有严格的场景规范如必须有且只有一个GameManager灯光模式必须为Baked不能存在Missing的脚本组件手动检查容易遗漏。实现思路编写一个检查函数遍历当前场景的所有GameObject和组件检查预设的规则。将问题汇总到一个可滚动的列表中每个问题项旁边可以提供一个“定位”或“修复”按钮。void CheckScene() { _problems.Clear(); // 检查GameManager if (FindObjectOfTypeGameManager() null) { _problems.Add(new ProblemInfo(“场景中缺少GameManager”, ProblemType.Error)); } // 检查Missing脚本 var allObjs Resources.FindObjectsOfTypeAllGameObject(); foreach(var obj in allObjs) { Component[] comps obj.GetComponentsComponent(); foreach(var comp in comps) { if (comp null) { _problems.Add(new ProblemInfo($“对象 {obj.name} 存在Missing脚本”, ProblemType.Warning, obj)); } } } // 在OnGUI中绘制_problems列表 }工具12快速性能采样区块标记痛点使用Profiler进行性能分析时很难精确对应代码块与性能数据。实现思路利用Profiler.BeginSample和Profiler.EndSample。你可以创建一个静态工具类封装一些快捷方法在编辑器和开发模式下为特定的代码块打上标记。public static class PerfMarker { [Conditional(“UNITY_EDITOR”), Conditional(“DEVELOPMENT_BUILD”)] public static void Begin(string name) { Profiler.BeginSample(name); } [Conditional(“UNITY_EDITOR”), Conditional(“DEVELOPMENT_BUILD”)] public static void End() { Profiler.EndSample(); } } // 使用 void Update() { PerfMarker.Begin(“MyExpensiveFunction”); MyExpensiveFunction(); PerfMarker.End(); }在Unity Profiler的CPU Usage区域你就能清晰地看到“MyExpensiveFunction”这个采样区块所占用的时间。4.5 团队协作与规范工具确保团队代码和资源风格统一减少沟通成本。工具13代码风格与命名规范检查器初步痛点团队代码风格不一变量命名随意影响可读性和维护性。实现思路虽然完整检查需要Roslyn等编译器服务但我们可以做一个轻量级版本。利用AssetDatabase.FindAssets找到所有C#脚本用File.ReadAllText读取内容用简单的正则表达式检查常见的规范问题例如类名是否遵循PascalCase。私有字段是否以_开头或使用m_前缀根据团队规范。是否存在Debug.Log提交到了版本库应使用条件编译或自定义Logger。 将检查结果输出到一个可排序、可过滤的编辑器窗口中。工具14预制体依赖与变体分析工具痛点预制体Prefab嵌套过深或变体Variant过多时理清引用关系和修改影响范围非常困难。实现思路递归分析一个预制体所引用的所有其他预制体和资源使用PrefabUtility.GetCorrespondingObjectFromSource和AssetDatabase.GetDependencies。以树状图或图表的形式展示出来。可以扩展功能比如“查找所有使用此基础预制体的变体”、“批量替换预制体中的某个材质或组件”。工具15资源提交前预检Pre-commit Hook模拟痛点美术或策划同学提交的资源可能不符合项目规范如纹理尺寸非2的幂、音频采样率不对、模型面数超限导致需要反复修改提交。实现思路创建一个工具窗口让资源提交者可以选中要提交的资源运行一次“预检”。工具会调用一系列检查规则类似AssetPostprocessor中的逻辑生成一份报告列出所有不符合规范的地方及其修复建议。这能极大降低入库资源的错误率。4.6 高级与系统级工具这类工具更深地集成到编辑器系统中功能更强大。工具16自定义Project窗口过滤器与图标痛点在成百上千个资源中很难快速找到特定类型的资源如所有“可交互”的预制体所有“未完成的”材质球。实现思路使用ProjectWindowUtil注册一个Startup回调或者编写一个Editor脚本来修改Project窗口的绘制。更实用的方法是创建自定义的搜索过滤语法。例如通过继承SearchFilter类这属于较高级用法可能需要反射你可以添加像t:prefab is:interactive这样的自定义过滤器。同时可以使用EditorGUIUtility.SetIconForObject为特定类型的资源设置显眼的图标方便视觉区分。工具17动画片段Animation Clip批量处理工具痛点导入的FBX文件包含大量动画片段需要统一设置循环时间、去除根骨骼运动、应用人形骨骼掩码等。实现思路获取一个模型资产下的所有AnimationClip子资产通过AnimationUtility.GetAnimationClipSettings和AnimationUtility.SetAnimationClipSettings来批量修改其设置。也可以使用AnimationUtility.SetAnimationEvents来批量添加或移除动画事件。警告直接修改AnimationClip是永久性的操作前务必备份或确保在版本控制下进行。工具18Shader变体收集与打包检查工具痛点URP/HDRP项目或复杂Shader会产生大量变体容易导致构建包体膨胀且难以管理。实现思路这是一个高级话题。核心是利用ShaderUtil.GetShaderVariantEntries获取Shader的所有变体信息。你可以编写一个工具来扫描项目中的所有材质球和Shader统计实际使用的变体并与Shader中可能的变体总数进行对比找出那些“永远用不到”的变体并通过修改Shader的#pragma multi_compile或#pragma shader_feature指令来剔除它们从而优化构建。5. 常见问题与排查技巧实录即使掌握了API在实际开发编辑器扩展时你依然会遇到各种“坑”。下面是我总结的一些典型问题及解决方案。5.1 工具运行卡顿或无响应问题描述点击自定义菜单后编辑器卡住甚至弹出“无响应”提示。排查思路检查循环与递归首先检查你的工具代码中是否有死循环或深度递归。特别是在遍历大型目录或复杂数据结构时。优化资源操作你是否在循环内频繁调用AssetDatabase.Refresh()或AssetDatabase.SaveAssets()记住批量操作前用AssetDatabase.StartAssetEditing()包裹结束后再调用StopAssetEditing()。使用进度条对于耗时操作超过0.5秒务必使用EditorUtility.DisplayProgressBar显示进度条。这不仅用户体验好也能让你知道程序还在运行。try { for (int i 0; i total; i) { if (EditorUtility.DisplayCancelableProgressBar(“处理中”, $正在处理第{i1}个, (float)i/total)) { Debug.Log(“用户取消了操作”); break; } // ... 你的处理逻辑 } } finally { EditorUtility.ClearProgressBar(); // 确保无论如何都清除进度条 }考虑异步对于极其耗时的操作如处理数千个文件可以考虑使用EditorApplication.delayCall将任务分割到多帧执行或者在较新Unity版本中探索async/await与IEnumerator协程在编辑器下的使用但这需要更谨慎的处理。5.2 自定义Inspector不显示或显示异常问题描述为自定义组件编写了Editor脚本但Inspector没有任何变化或者布局错乱。排查步骤脚本位置Editor脚本必须放在名为Editor的文件夹下否则不会被加载。类名与特性确保你的Editor类继承了UnityEditor.Editor并且使用了[CustomEditor(typeof(你的组件类型))]特性。类名可以任意但特性中的类型必须完全匹配。OnInspectorGUI是否重写了OnInspectorGUI方法如果没有会使用默认绘制。序列化对象是否在OnInspectorGUI开头调用了serializedObject.Update()在结尾调用了serializedObject.ApplyModifiedProperties()这是正确绘制和保存数据的关键。绘制默认GUI如果你想在自定义UI的基础上保留一部分默认的绘制可以调用DrawDefaultInspector()。注意一旦你开始手动绘制属性就需要自己处理所有属性的绘制和保存。5.3 编辑器扩展在打包后失效或报错问题描述在编辑器中运行良好的工具在打包出的游戏里调用时找不到相关类或方法。根本原因所有在UnityEditor命名空间下的类、方法、属性都仅能在Unity编辑器环境中运行。它们不会被包含在游戏的运行时程序集中。解决方案条件编译将所有使用UnityEditorAPI的代码用#if UNITY_EDITOR和#endif包裹起来。这是最标准、最安全的做法。#if UNITY_EDITOR using UnityEditor; [MenuItem(“Tools/MyTool”)] static void MyToolMethod() { // 编辑器工具代码 } #endif分离程序集将编辑器相关的代码放在独立的程序集定义Assembly Definition中并为其添加UNITY_EDITOR编译符号。这样可以从物理和逻辑上隔离运行时和编辑器代码。运行时替代方案如果某些功能需要在游戏运行时也生效如调试绘制你需要准备两套实现一套在UNITY_EDITOR下使用Gizmos/Handles另一套在运行时使用GL或自定义的Mesh绘制。5.4 撤销Undo功能不起作用问题描述在自定义工具中修改了对象属性但按CtrlZ无法撤销。原因与解决对于Inspector编辑必须使用SerializedProperty系统并确保ApplyModifiedProperties被调用。这是最自动化的方式。对于场景中对象的直接修改在修改任何Component或GameObject的属性之前需要调用Undo.RecordObject来记录对象的状态。// 修改Transform前 Undo.RecordObject(transform, “Move Selected Objects”); transform.position newPosition; // 如果需要标记场景已更改使保存按钮生效 EditorUtility.SetDirty(transform);对于创建/销毁对象使用Undo.RegisterCreatedObjectUndo和Undo.DestroyObjectImmediate。GameObject newObj new GameObject(“New Object”); Undo.RegisterCreatedObjectUndo(newObj, “Create New Object”); // 销毁对象 Undo.DestroyObjectImmediate(oldObj);对于一组操作可以使用Undo.SetCurrentGroup和Undo.CollapseUndoOperations将一系列操作合并为一个可撤销的步骤。5.5 工具窗口布局与状态保存问题描述自定义的EditorWindow关闭后再次打开时之前输入的参数、滚动位置等都重置了。解决方案使用EditorPrefs或ScriptableObject来保存窗口状态。EditorPrefs适合保存简单的键值对如窗口位置、大小、最近使用的路径等。数据保存在本地注册表或用户偏好文件中。// 保存 EditorPrefs.SetFloat(“MyWindow_PosX”, position.x); // 读取 float savedX EditorPrefs.GetFloat(“MyWindow_PosX”, 100f);ScriptableObject适合保存复杂的配置数据如工具预设、规则列表、颜色方案等。创建一个继承自ScriptableObject的配置类将其作为资产保存在项目中。在窗口的OnEnable方法中加载它在修改时保存它。这种方式的好处是配置可以纳入版本控制并且能在团队成员间共享。public class MyToolSettings : ScriptableObject { public string defaultPath; public Liststring recentProjects; } // 在EditorWindow中 private MyToolSettings _settings; void OnEnable() { _settings AssetDatabase.LoadAssetAtPathMyToolSettings(“Assets/Editor/MyToolSettings.asset”); if (_settings null) { _settings CreateInstanceMyToolSettings(); AssetDatabase.CreateAsset(_settings, “Assets/Editor/MyToolSettings.asset”); } } void OnGUI() { // 使用_settings.defaultPath等 // 修改后如果需要立即保存到磁盘可以调用 // EditorUtility.SetDirty(_settings); // AssetDatabase.SaveAssets(); // 谨慎调用避免频繁保存 }开发编辑器扩展是一个持续学习和积累的过程。最好的学习方式就是从解决你当前项目中一个最具体的痛点开始动手实现一个小工具。当你亲眼看到它为你节省下第一个小时的时候你就会彻底爱上这种“创造工具”的感觉。记住这些工具最终是为了让你和你的团队更专注于游戏创作本身而不是迷失在重复的劳动中。希望这份指南能成为你编辑器扩展之旅的得力地图助你打造出独一无二的高效开发流水线。
返回列表