当前位置: 首页 > news >正文

【Unity笔记】Unity 编辑器扩展:一键查找场景中组件引用关系(含完整源码)(组件引用查找工具实现笔记)

摘要:
本文介绍了如何在 Unity 编辑器中开发一款实用的编辑器扩展工具 —— ComponentReferenceFinder,用于查找场景中对某个自定义组件的引用关系。该工具特别适用于大型项目、多人协作或引入外部插件后,快速定位组件间的耦合关系。
本文从需求出发,逐步拆解功能目标:如何获取选中 GameObject、自定义组件的过滤逻辑、如何遍历场景中的 MonoBehaviour、反射字段和集合引用、解析 UnityEvent 中的持久化调用,以及如何在 EditorWindow 中呈现可视化查找结果。

在这里插入图片描述

前言


在 Unity 项目开发过程中,尤其当项目规模增大、多人协作或外部 SDK、DLL 插件引入后,我们常常遇到这样的需求:“我想知道某个 GameObject 上挂载的某个自定义组件,到底被场景中哪些脚本引用了?”。手工一个一个场景搜索、检查 Inspector,既容易漏掉隐式引用(数组、列表、UnityEvent),又耗时耗力。本文将从出发,以设计思路+关键代码片段的形式,记录如何一步步实现一款ComponentReferenceFinder 编辑器扩展工具,满足以下核心需求:

  1. Hierarchy 中选中一个 GameObject;
  2. 自动列出它挂载的所有“用户脚本”组件(包括自定义 DLL 插件中的 MonoBehaviour);
  3. 选择其中一个组件实例,点击“查找引用”按钮;
  4. 遍历场景(包含隐藏和 inactive)的所有 MonoBehaviour,反射查找字段引用、集合引用以及 UnityEvent 持久化引用;
  5. 将检索结果以可点击的列表形式呈现,支持点击“Ping”快速定位引用者;
  6. 在查找过程中显示可取消进度条,跳过 Unity 内部或未赋值字段,保证运行稳定。

一、需求拆解与功能规划

在动手编写工具之前,先把需求分解成几个清晰的子问题:

  1. 如何获取选中 GameObject
    利用 Selection.activeGameObject。当用户在层级面板切换选中对象后,插件要能自动响应并更新状态。

  2. 如何过滤“自定义组件”
    不是所有挂载在 GameObject 上的 MonoBehaviour 都要列出。我们只要项目中自己写的脚本,或通过第三方 DLL 引入的脚本组件,需要排除 Unity 自带的 TransformCameraLightParticleSystem,以及编辑器脚本等。

  3. 如何遍历场景中所有 MonoBehaviour
    Unity API 提供 Object.FindObjectsOfType<T>(true),可以同时检索包括 inactive 的全部场景物体。对每个物体,用 GetComponents<MonoBehaviour>() 枚举挂载的所有脚本。

  4. 如何检测字段引用
    利用反射(FieldInfo.GetValue)读取每个 MonoBehaviour 的所有实例字段(包括 public 和 private + [SerializeField])。如果字段值等于目标组件实例,则记录“单引用”。

  5. 如何检测集合引用
    当字段类型实现了 IEnumerable(排除 string),则将其强转为 IEnumerable,对其中每个元素判断是否引用目标组件;若命中则记录“集合引用”。

  6. 如何检测 UnityEvent 引用
    UnityEvent 在 Inspector 上配置的回调保存于序列化属性 m_PersistentCalls.m_Calls。借助 SerializedObjectSerializedProperty,可以读取这些持久化数据,从中找到 m_Target 字段等于目标组件的调用列表。

  7. 如何呈现结果并支持定位
    EditorWindow.OnGUI 方法中,使用滚动视图 (EditorGUILayout.BeginScrollView) 列出每一条引用记录,并为每条记录生成一个“Ping”按钮,点击后调用 EditorGUIUtility.PingObject(GameObject),在层级面板中高亮对应引用对象。


二、核心组件设计

2.1 主窗口类:ComponentReferenceFinder

  • 继承自 EditorWindow
  • 挂载菜单项 MenuItem("Tools/Component Ref Finder")
  • 关键字段:
    • GameObject selectedObject:当前选中物体;
    • MonoBehaviour[] customScripts:选中物体上的自定义脚本组件列表;
    • int selectedScriptIndex:下拉选择索引;
    • List<ReferenceResult> results:存放所有查找到的引用记录;
    • Vector2 scrollPos:滚动视图位置。

2.2 引用记录结构:ReferenceResult

用来封装一条引用信息,包括:

  • GameObject referencingObject:引用目标组件的 GameObject;
  • MonoBehaviour hostComponent:哪条脚本组件里的字段或 UnityEvent 产生了引用;
  • string fieldName:字段名;
  • bool isCollection:是否集合类型引用;
  • bool isUnityEvent:是否 UnityEvent 引用。

这个结构便于在 GUI 中统一渲染和处理点击事件。


三、实现步骤

下面从高到低,逐步拆解每一步的实现要点,并给出关键代码示例。

3.1 窗口初始化与选中物体监听

[MenuItem("Tools/Component Ref Finder")]
public static void ShowWindow() {GetWindow<ComponentReferenceFinder>("Component Ref Finder");
}private void OnSelectionChange() {// 每当层级中选中对象变化时,重置状态并重绘窗口selectedScriptIndex = 0;results.Clear();Repaint();
}
  • ShowWindow:将插件挂到 Tools 菜单;
  • OnSelectionChange:Unity 编辑器回调,选中改变时重置索引、清空结果,并调用 Repaint()OnGUI 重新绘制。

3.2 枚举自定义组件下拉列表

OnGUI 中:

selectedObject = Selection.activeGameObject;
if (selectedObject == null) {EditorGUILayout.HelpBox("请先在场景中选中一个对象!", MessageType.Warning);return;
}customScripts = selectedObject.GetComponents<MonoBehaviour>().Where(c => c != null && IsUserScript(c.GetType())).ToArray();if (customScripts.Length == 0) {EditorGUILayout.HelpBox("该对象没有自定义脚本组件", MessageType.Info);return;
}// 下拉选择
string[] scriptNames = customScripts.Select(c => c.GetType().FullName).ToArray();
selectedScriptIndex = EditorGUILayout.Popup("组件类型", selectedScriptIndex, scriptNames);
  • IsUserScript(Type t):根据脚本所属程序集名称过滤,只保留自定义 DLL/Assembly。
  • Popup 生成下拉框,用户可选择要查找引用的组件实例。

3.3 遍历场景并查找字段引用

核心查找方法 FindReferencesToComponent

private void FindReferencesToComponent(MonoBehaviour targetComp) {results.Clear();var allObjs = GameObject.FindObjectsOfType<GameObject>(true);int total = allObjs.Length, processed = 0;foreach (var obj in allObjs) {processed++;if ( EditorUtility.DisplayCancelableProgressBar("查找引用中...", obj.name, (float)processed/total) )break;if (!obj.scene.IsValid()) continue;foreach (var comp in obj.GetComponents<MonoBehaviour>()) {if (comp == null || !IsUserScript(comp.GetType())) continue;var fields = comp.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);foreach (var field in fields) {object value = null;try { value = field.GetValue(comp); }catch { continue; }    // 安全跳过// 单个引用if ( ReferenceEquals(value, targetComp) ) {results.Add(new ReferenceResult(obj, comp, field.Name, false, false));continue;}// 集合引用if (typeof(IEnumerable).IsAssignableFrom(field.FieldType)&& field.FieldType != typeof(string)){var ie = value as IEnumerable;if (ie != null) {try {foreach (var item in ie) {if ( ReferenceEquals(item, targetComp) ) {results.Add(new ReferenceResult(obj, comp, field.Name, true, false));break;}}}catch { }}}// UnityEvent 引用检测(下节示例)}}}EditorUtility.ClearProgressBar();
}
  • ProgressBar:调用 DisplayCancelableProgressBar,提示当前物体名,并支持用户取消搜索。
  • 跳过无效场景对象:判断 obj.scene.IsValid(),过滤 prefab 等资源。
  • 字段反射GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) 获取所有实例字段。
  • 引用判断:使用 ReferenceEquals,确保比对的是实例引用而不是值类型或重写了 Equals 的逻辑。
  • 集合处理:先判断类型可枚举,再强转并在 try-catch 中遍历,避免内部未赋值导致崩溃。

3.4 持久化 UnityEvent 调试

UnityEvent 底层结构如下(简化):

UnityEventBase└─ PersistentCalls m_PersistentCalls└─ List<PersistentCall> m_Calls└─ PersistentCall├─ Object m_Target├─ string m_MethodName└─ …...

要查找 UnityEvent 引用,需要:

  1. 新建 SerializedObject so = new SerializedObject(comp);
  2. var prop = so.FindProperty(field.Name);
  3. var calls = prop.FindPropertyRelative("m_PersistentCalls.m_Calls");
  4. 遍历 calls.arraySize,取出每个 m_Target 属性,比对是否等于目标组件:
if (typeof(UnityEventBase).IsAssignableFrom(field.FieldType)) {try {var so = new SerializedObject(comp);var prop = so.FindProperty(field.Name);var calls = prop.FindPropertyRelative("m_PersistentCalls.m_Calls");if (calls != null && calls.isArray) {for (int i = 0; i < calls.arraySize; i++) {var call = calls.GetArrayElementAtIndex(i);var targetProp = call.FindPropertyRelative("m_Target");if (targetProp != null &&targetProp.objectReferenceValue == targetComp) {results.Add(new ReferenceResult(obj, comp, field.Name, false, true));break;}}}}catch { }
}
  • 注意:序列化属性访问也要用 try-catch 包裹,以防 Unity 内部或私有字段访问异常。

3.5 完整代码

下载地址


四、使用步骤

  1. 安装脚本

    • ComponentReferenceFinder.cs 放到项目的 Assets/Editor/ 目录下。
  2. 打开窗口

    • 在 Unity 顶部菜单栏依次点击:
      Tools -> ExTool-> Component Ref Finder
      
  3. 选中目标 GameObject

    • Hierarchy 面板中点击你要排查的场景物体 A。
    • 确保它是场景实例(而非 Prefab 资源文件)。
      在这里插入图片描述
  4. 选择组件实例

    • 窗口中会列出 A 上所有“用户脚本”组件(包括自定义 DLL 中的 MonoBehaviour)。
    • 从下拉框中选择你关心的那个组件(例如 SoundEffectsPlayer)。
      在这里插入图片描述
  5. 执行检索

    • 点击 “查找引用该组件的对象” 按钮。
    • 窗口底部将出现一个进度条,显示当前正在扫描的 GameObject 名称。
    • 在大场景中,你可以随时按 Esc 或点击进度条的取消按钮中断搜索。
      在这里插入图片描述
  6. 查看与定位结果

    • 检索完成后,底部列表会展示所有引用该组件实例的记录,包括:
      • 单字段引用
      • 集合引用(List/Array)
      • UnityEvent 持久化引用
    • 每条记录前有一个 “Ping” 按钮:
      • 点击即可在 Hierarchy 中高亮并聚焦对应的引用 GameObject。
        在这里插入图片描述
  7. 后续排查

    • 找到引用后,双击打开对应脚本或在 Inspector 中检查字段,进一步定位用途与调用逻辑。
    • 若发现误引用或不再需要的回调,将其清理或重构,以保持场景引用关系的清晰。

五、优化与扩展方向

5.1 优化内容

  • 跳过 Unity 内部脚本:在 IsUserScript(Type t) 中排除 UnityEngine.*UnityEditor.*System.* 等程序集,减少无关反射开销。
  • catch 屏蔽:对所有反射、枚举、序列化操作进行 try-catch,只跳过当前字段或组件,不影响整体搜索进程。
  • 进度条:分批刷新,避免一次性满帧卡死。

5.2 扩展方向

  1. 链式引用:支持递归查找 A → B → C,识别多级引用关系,并以树状结构展示;
  2. 场景快照对比:记录不同场景版本之间的引用差异,帮助回归测试;
  3. 导出报告:将结果保存为 JSON、CSV 或 Markdown 文档,便于团队共享;
  4. 代码静态分析集成:结合 Mono.Cecil、Roslyn,定位脚本中对组件方法(如 .Play())的调用;
  5. 上下文菜单:在 Inspector 中右键某组件,当选时直接调用查找工具。

六、结语

本文以需求驱动的方式,记录了从零构思到实现一个通用的 Unity 编辑器扩展——ComponentReferenceFinder 的全过程。它不仅能满足字段级引用查找,还兼顾集合和 UnityEvent 的场景,适配自定义 DLL 和大型多人项目。

把它集成到你的项目中,可以大幅提升定位隐式依赖、排查丢失引用、管理复杂组件关系的效率。希望这篇笔记能帮助你快速掌握 Unity 编辑器扩展与反射、序列化属性的组合使用思路,从而更好地构建属于自己团队的开发工具链。

欢迎在评论区分享你的使用体验与改进想法!

相关文章:

  • Prompt 结构化提示工程
  • React组件测试完全指南:从入门到实践
  • vue3+dhtmlx 甘特图真是案例
  • 数据一致性问题剖析与实践(二)——单机事务的一致性问题
  • 数据为基:机器学习中数值与分类数据的处理艺术及泛化实践
  • MacOS中安装Python(homebrew,pyenv)
  • Stable Baselines3 结合 gym 训练 CartPole 倒立摆
  • 【教学类-102-17】蝴蝶三色图(用最大长宽作图,填入横板和竖版共16个WORD单元格模版大小,制作大小图)
  • Java 环境配置详解(Windows、macOS、Linux)
  • 【Leetcode 每日一题】1399. 统计最大组的数目
  • 第52讲:农业AI + 区块链——迈向可信、智能、透明的未来农业
  • 大模型框架技术演进与全栈实践指南
  • 1.5软考系统架构设计师:架构师的角色与能力要求 - 超简记忆要点、知识体系全解、考点深度解析、真题训练附答案及解析
  • Elasticsearch 报错 Limit of total fields [1000] has been exceeded
  • Postman忘记密码访问官网总是无响应
  • SpringCloud 微服务复习笔记
  • 第七篇:linux之基本权限、进程管理、系统服务
  • Linux[指令与权限]
  • Vm免安装直接使用虚拟机win7系统
  • 每日算法-250423
  • 中纪报刊文:新时代反腐败斗争为党赢得历史主动
  • “仅退款”将成历史?电商平台集中调整售后规则
  • 人民日报首推“大地书单”,10本好书上榜!
  • 河南濮阳南乐县官方回应“幼儿园强制订园服”:已责令整改
  • 威廉·透纳诞辰250周年|他是现代艺术之父
  • 白宫新闻秘书:美政府将在法庭上回应哈佛大学诉讼