1. 项目概述为什么我们需要一个更智能的Hierarchy如果你在Unity里做过稍微复杂点的项目尤其是那种有成百上千个预制体实例和场景物件的项目肯定对那个默认的Hierarchy面板又爱又恨。爱的是它结构清晰恨的是它一旦内容多了找东西就像大海捞针。默认的Hierarchy把所有东西都混在一起——场景根节点、空物体、各种预制体实例、特效、UI元素……全都挤在一个列表里。更头疼的是当你从Project窗口拖入一堆预制体时它们在Hierarchy里的顺序完全是随机的毫无逻辑可言。这个项目要解决的就是这两个最折磨人的痛点分类和排序。我们不是简单地写个Editor脚本而是打造一个能深度集成到Unity编辑器工作流中的扩展工具。它的核心目标有两个第一在Hierarchy视图中用清晰的视觉标识比如不同的图标、颜色或分组将“预制体实例”和“原生场景物件”彻底区分开第二提供一键或自动化的排序功能可以按照名称、类型、创建时间甚至是自定义的标签规则让Hierarchy变得井然有序。这不仅仅是让界面好看一点。在实际的团队协作和大型项目开发中一个混乱的Hierarchy会直接导致场景加载变慢因为查找和引用效率低、团队成员难以理解场景结构、以及频繁的误操作比如不小心删了不该删的东西。通过代码实现这个扩展意味着我们可以将一些最佳实践和项目规范固化到工具里让所有使用者都能受益提升整个开发管线的效率和可维护性。2. 核心需求与设计思路拆解2.1 需求一实现预制体与场景物件的视觉区隔这是最基础也是最核心的需求。Unity默认只会在预制体实例的图标旁边加一个蓝色的小箭头但在密密麻麻的列表中并不显眼。我们需要更强烈的视觉反馈。设计思路图标叠加这是最直观的方式。我们可以为所有预制体实例的GameObject在Hierarchy中额外绘制一个自定义图标。这个图标可以放在名称前面或者覆盖在默认图标上。文本颜色/背景色改变预制体实例或特定类型场景物件的文本颜色或行背景色。例如所有预制体实例的名字显示为蓝色而手动创建的“空物体”显示为灰色。分组与折叠更激进但非常高效的做法是在Hierarchy中自动创建虚拟的“文件夹”或“分组”将预制体实例和场景物件分别收纳到不同的父级下。不过这会改变场景的实际父子节点关系需要谨慎处理通常采用“虚拟分组”技术只影响显示不影响实际Transform层级。为什么选择图标颜色组合单纯改颜色可能对色盲开发者不友好单纯改图标可能不够醒目。两者结合能提供双重保险。我们不会去动实际的Transform父子关系因为那会破坏场景原有的逻辑依赖比如一个场景物件是某个预制体实例的子物体。我们的扩展应该是一个“非侵入式”的视图增强工具。2.2 需求二提供灵活强大的排序功能排序不仅仅是按字母A-Z。在项目开发中我们可能有多种排序维度按类型排序将所有MeshRenderer的物体排在一起所有Light排在一起。这在调整场景光照和检查模型时非常有用。按创建顺序/导入顺序排序有时我们希望保持物件在场景中创建的先后逻辑。按自定义标签或层排序这是最强大的功能。我们可以定义一套项目内的标签系统不是Unity的Tag而是我们自己的分类如“环境-静态”、“交互-可拾取”、“UI-世界空间”然后让Hierarchy按这些自定义分类来分组排序。按引用关系排序将拥有相同预制体源即来自同一个.prefab文件的实例排列在一起。设计思路我们将构建一个可配置的“排序规则引擎”。这个引擎由多条排序规则组成每条规则包含匹配条件判断一个GameObject是否适用此规则例如isPrefabInstance true。排序键决定在这个规则组内物体如何排序例如按name 按GetComponentRenderer().material.name。显示优先级规则之间的执行顺序。编辑器扩展将提供一个窗口让开发者可以像搭积木一样配置这些规则。最终的Hierarchy顺序将是这些规则依次应用的结果。2.3 技术选型为什么是EditorWindow Hierarchy重绘要实现这个工具我们主要依赖Unity Editor的两个核心扩展点EditorWindow用于创建我们的工具配置面板。在这里用户可以勾选需要的视觉区分选项定义和管理复杂的排序规则。ScriptableObject将是我们保存这些配置的最佳选择因为它可以生成一个配置文件.asset方便在团队内共享和版本控制。EditorApplication.hierarchyChanged 与 EditorApplication.RepaintHierarchyWindow这是动态排序和刷新的关键。我们需要监听Hierarchy的变化物体新增、删除、改名在变化发生时重新应用我们的排序逻辑并强制Hierarchy窗口重绘以更新视觉样式。EditorGUI / Handles 绘制在Editor类中我们可以通过OnPreviewGUI或OnInteractivePreviewGUI来绘制自定义内容但对于Hierarchy中每个Item的绘制我们需要用到更底层的EditorGUIUtility相关API或者利用IMGUI在正确的时机进行覆盖绘制。更现代、更高效的做法是使用UIElements来构建自定义的Hierarchy项但这涉及到对UIElements的TreeView进行扩展复杂度较高。作为初版我们选择在OnGUI循环中通过计算每个Item的屏幕位置进行绘制这种方式更直接但需要注意性能。为什么不使用现有的Asset Store插件当然有很多优秀的插件如Hierarchy 2,Enhanced Hierarchy等。自己实现的好处在于绝对的控制权和深度定制能力。你可以让工具完全贴合自己项目的特殊规范比如与项目内部的任务管理系统、资源命名规则挂钩。其次这是一个绝佳的深入学习Unity Editor扩展开发的机会你会透彻理解Unity编辑器的渲染流程、事件系统和性能优化点。3. 核心模块实现详解3.1 模块一配置数据的持久化ScriptableObject任何编辑器工具都需要一个地方来保存用户的设置。ScriptableObject是Unity为此设计的完美容器。using UnityEngine; using System.Collections.Generic; // 排序规则定义 [System.Serializable] public class HierarchySortRule { public string ruleName “New Rule”; public bool enabled true; // 匹配条件可以是类型、名称包含、标签、层等 public MatchCondition matchCondition; public SortKey sortKey; public int priority; // 数字越小优先级越高 } // 视觉区分规则定义 [System.Serializable] public class VisualDiffRule { public string ruleName “New Visual Rule”; public bool enabled true; public MatchCondition matchCondition; public Color textColor Color.white; public Texture2D customIcon; public bool changeBackground false; public Color backgroundColor Color.gray; } // 主配置 [CreateAssetMenu(fileName “HierarchyOrganizerConfig”, menuName “Tools/Hierarchy Organizer Config”)] public class HierarchyOrganizerConfig : ScriptableObject { public ListHierarchySortRule sortRules new ListHierarchySortRule(); public ListVisualDiffRule visualRules new ListVisualDiffRule(); public bool enableAutoSort true; public bool enableVisualDiff true; }创建一个该配置的实例后它会作为一个.asset文件保存在项目中。我们的编辑器窗口将读取和修改这个文件。3.2 模块二编辑器配置窗口EditorWindow这个窗口是我们的控制中心。我们将使用IMGUI来快速构建界面因为它对于工具类窗口足够灵活和简单。using UnityEditor; using UnityEngine; public class HierarchyOrganizerWindow : EditorWindow { private HierarchyOrganizerConfig config; private Vector2 scrollPos; [MenuItem(“Tools/Window/Hierarchy Organizer”)] public static void ShowWindow() { GetWindowHierarchyOrganizerWindow(“Hierarchy Organizer”); } private void OnEnable() { // 尝试加载现有的配置如果没有则创建一个默认的 string[] guids AssetDatabase.FindAssets(“t:HierarchyOrganizerConfig”); if (guids.Length 0) { string path AssetDatabase.GUIDToAssetPath(guids[0]); config AssetDatabase.LoadAssetAtPathHierarchyOrganizerConfig(path); } if (config null) { // 提示用户创建或默认配置这里简化处理 } } private void OnGUI() { if (config null) { EditorGUILayout.HelpBox(“Please create a HierarchyOrganizerConfig asset first.”, MessageType.Warning); if (GUILayout.Button(“Create Default Config”)) { // 创建默认配置的代码... } return; } scrollPos EditorGUILayout.BeginScrollView(scrollPos); // 全局开关 config.enableAutoSort EditorGUILayout.Toggle(“Enable Auto Sort”, config.enableAutoSort); config.enableVisualDiff EditorGUILayout.Toggle(“Enable Visual Differentiation”, config.enableVisualDiff); EditorGUILayout.Space(10); EditorGUILayout.LabelField(“Sorting Rules”, EditorStyles.boldLabel); // 这里绘制排序规则的列表允许增删改查 DrawRulesList(config.sortRules); EditorGUILayout.Space(10); EditorGUILayout.LabelField(“Visual Rules”, EditorStyles.boldLabel); // 这里绘制视觉规则的列表 DrawRulesList(config.visualRules); EditorGUILayout.EndScrollView(); if (GUI.changed) { EditorUtility.SetDirty(config); // 标记配置为已修改确保保存 } } // 绘制规则列表的通用方法简化版 private void DrawRulesListT(ListT rules) where T : new() { for (int i 0; i rules.Count; i) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 绘制每个规则的字段... EditorGUILayout.EndVertical(); } if (GUILayout.Button(“ Add Rule”)) { rules.Add(new T()); } } }3.3 模块三Hierarchy的重绘与排序逻辑这是最核心也是最复杂的部分。我们需要在正确的时机介入Hierarchy的绘制和排序。步骤1监听与触发我们不能每帧都去排序那样性能开销太大。正确的做法是监听Hierarchy发生变化的时刻。[InitializeOnLoad] public static class HierarchyOrganizerCore { private static HierarchyOrganizerConfig s_config; private static ListGameObject s_sortedRootObjects new ListGameObject(); static HierarchyOrganizerCore() { EditorApplication.hierarchyChanged OnHierarchyChanged; EditorApplication.update OnEditorUpdate; // 用于延迟处理避免同一帧内多次排序 LoadConfig(); } private static void LoadConfig() { // ... 加载config的代码 } private static void OnHierarchyChanged() { if (s_config ! null s_config.enableAutoSort) { // 标记需要排序而不是立即执行 m_needSort true; } // 无论排序是否开启视觉区分都需要重绘 EditorApplication.RepaintHierarchyWindow(); } private static bool m_needSort false; private static void OnEditorUpdate() { if (m_needSort) { m_needSort false; PerformSorting(); } } }步骤2实现排序算法PerformSorting函数是核心。Unity的Hierarchy顺序实际上是由场景根节点下所有GameObject的transform.SetSiblingIndex决定的。但直接修改它会打乱编辑器状态。更安全的方法是先收集需要排序的根物体没有父物体或父物体也是根的场景物体然后根据我们的规则计算出一个新的顺序列表最后应用这个顺序。private static void PerformSorting() { // 1. 获取当前场景的所有根物体 GameObject[] rootObjs UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects(); // 2. 应用我们的排序规则 // 这里需要一个复杂的排序比较器它会遍历所有规则为每个物体计算出一个“排序权重” System.Array.Sort(rootObjs, new GameObjectHierarchyComparer(s_config)); // 3. 重新设置同级索引Sibling Index for (int i 0; i rootObjs.Length; i) { rootObjs[i].transform.SetSiblingIndex(i); } // 注意这会触发新的hierarchyChanged事件但由于m_needSort已被重置不会死循环。 }GameObjectHierarchyComparer的实现是关键它需要解析config.sortRules为两个GameObject计算出一个可比较的权重值。规则可能很复杂比如“先按是否是预制体分组组内按名称排序如果不是预制体则按类型分组组内按创建时间排序”。步骤3实现视觉区分绘制我们无法直接替换Hierarchy的绘制但可以在它绘制完成后再在上面“画”一层。这需要用到EditorWindow的OnGUI事件但目标窗口是Hierarchy窗口。一个常见的方法是使用EditorApplication.update来获取SceneHierarchyWindow的实例然后订阅其GUI事件。更实用和稳定的方法是利用Editor类的OnHeaderGUI或自定义Editor的OnPreviewGUI但这只对选中的单个对象有效。对于整个Hierarchy一个经典的“Hack”方法是获取当前GUI或Event的mousePosition和当前绘制的区域。在EditorApplication.update中尝试判断当前是否正在绘制Hierarchy窗口通过窗口标题或类型。如果是在绘制Hierarchy我们就遍历当前可见的Hierarchy项这需要计算滚动位置和项的高度然后根据config.visualRules为每一项绘制额外的图标或改变其颜色。由于这部分代码非常底层且依赖于未公开的GUI布局实现起来很繁琐且容易因Unity版本更新而失效。一个更现代、更推荐的方向是研究Unity 2019.3的UIElements和TreeViewAPI通过继承TreeView创建自定义的Hierarchy视图。这将是未来更稳定和强大的解决方案但学习曲线较陡。实操心得在项目初期我们可以采用一个折中且效果不错的方案——只修改文本颜色和添加前缀。我们可以在OnHierarchyChanged中遍历所有GameObject通过EditorUtility.SetDirty强制其刷新并在自定义的Editor脚本通过[CustomEditor(typeof(GameObject))]并重写OnHeaderGUI中根据规则修改显示的名称。虽然这主要影响Inspector标题但通过一些技巧也能部分影响Hierarchy列表的显示。这是一种“曲线救国”但快速见效的方法。4. 高级功能与性能优化4.1 实现“虚拟文件夹/分组”功能这是提升大型场景可读性的杀手锏。我们不改变实际的Transform父子关系而是在显示上“分组”。实现思路在配置中定义分组规则如“所有灯光”、“所有音频源”、“环境装饰”。在排序阶段我们不仅排序还会在排序后的列表中在特定组的第一个物体前插入一个特殊的、隐藏的GameObject作为“分组头”。这个GameObject可以命名为“LightGroup”并附加一个脚本将其在Hierarchy中渲染为一个不可点击、有背景色的横条。在视觉绘制阶段我们识别这些“分组头”对象将其绘制成背景色块分组名称的样式并将其后的同组物体在视觉上缩进。需要小心处理这些“分组头”对象的保存和加载它们不应该被打包到游戏运行时。4.2 性能优化要点在Hierarchy中频繁操作和绘制是性能敏感区。避免每帧遍历所有逻辑必须由事件驱动hierarchyChanged,selectionChanged而不是在Update中。缓存缓存再缓存PrefabUtility.GetPrefabInstanceStatus、GetComponent等调用是有开销的。对于视觉规则匹配可以给GameObject添加一个自定义的MonoBehaviour脚本来缓存其“分类状态”如CachedHierarchyTag。这个脚本只在编辑器下运行通过ExecuteInEditMode属性启用。限制重绘范围EditorApplication.RepaintHierarchyWindow()会重绘整个窗口。我们可以尝试只标记和重绘受影响的区域但这需要更精细的UIElements控制。异步与延迟执行当一次性拖入上百个预制体时排序和重绘可能会造成编辑器卡顿。可以将排序操作放到EditorApplication.delayCall中或者使用协程在编辑器脚本中需通过EditorCoroutine来分帧处理。4.3 与项目工作流集成工具的强大之处在于与项目规范结合。命名约定自动分类可以配置规则自动将名称以“Env”开头的物体识别为环境物件并归入“环境”虚拟文件夹赋予绿色文本。与Prefab Variant联动识别预制体变体Prefab Variant并用不同的图标或颜色区分变体和原始预制体。导出场景结构报告扩展工具使其能根据当前的分类和排序规则导出一份场景结构的文本或JSON报告用于团队审查或自动化测试。5. 常见问题与调试技巧实录问题1排序后物体的Transform父子关系错乱了原因你很可能直接对所有GameObject包括子物体进行了排序并调用了SetSiblingIndex。这会导致子物体被移到与父物体不同的层级。解决排序操作必须只针对根物体transform.parent null。子物体的顺序应由其父物体下的顺序决定。我们的排序算法应该是一个递归或层级处理的过程先排序所有根物体然后对每个根物体下的子物体递归应用同样的排序规则。问题2自定义图标或颜色在Hierarchy中闪烁或不显示原因绘制时机不对或绘制区域计算错误。Unity的GUI系统是立即模式的如果你在错误的事件阶段比如在Layout事件中做绘制或为已经被裁剪的区域进行绘制就会出现问题。解决确保绘制代码在Repaint事件中执行。使用GUILayoutUtility.GetRect或精确计算Rect来确保绘制位置在目标Item的可见区域内。一个调试技巧先用一个非常显眼的颜色如GUI.DrawTexture一个红色方块在计算出的位置绘制看看它是否出现在你期望的GameObject旁边。如果没有逐步调整你的位置计算逻辑。问题3启用工具后编辑器性能明显下降特别是打开大型场景时。原因未经优化的OnHierarchyChanged回调中进行了全场景遍历和复杂计算。解决节流在OnHierarchyChanged中设置一个标志在EditorApplication.update中延迟几帧再执行实际逻辑避免单帧内多次触发。增量处理只处理新增、删除或修改的物体而不是每次都处理全场景。可以通过记录上一次的物体列表快照进行对比。简化匹配规则检查视觉匹配规则中是否使用了昂贵的API调用如Physics.Raycast,AssetDatabase.LoadAssetAtPath。将这些结果缓存起来。问题4配置ScriptableObject在团队协作时每个人都要手动设置一遍吗解决不需要。将创建好的HierarchyOrganizerConfig.asset文件放入项目的Editor Resources或Settings目录下并提交到版本控制系统如Git。工具在启动时应自动寻找该路径下的配置文件。这样团队所有成员共享同一套配置。可以在配置窗口中加入“导入/导出为JSON”的功能方便在不同项目间迁移配置。问题5如何让这个工具对美术和策划同学更友好技巧提供预设在配置窗口中提供几个一键切换的预设如“纯排序模式”、“美术审核模式高亮特效和灯光”、“策划配置模式高亮交互物件”。简化界面将复杂的规则配置折叠起来主界面只显示最重要的开关和预设下拉框。添加搜索过滤在工具窗口内增加一个搜索框可以根据我们自定义的分类标签快速过滤Hierarchy中的物体这比Unity自带的搜索更符合项目语义。开发这样一个深度编辑器扩展就像为你和你的团队量身打造一把顺手的“手术刀”。它开始可能只是一个简单的颜色区分脚本但随着你对项目需求和Unity编辑器理解的加深它会逐渐演化成一个强大的场景管理中枢。这个过程本身就是对Unity引擎一次极为深刻的学习。