【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 窗口初始化与选中物体监听

csharp 复制代码
[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 中:

csharp 复制代码
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

csharp 复制代码
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 属性,比对是否等于目标组件:
csharp 复制代码
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 编辑器扩展与反射、序列化属性的组合使用思路,从而更好地构建属于自己团队的开发工具链。

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

相关推荐
世事如云有卷舒10 分钟前
《C++ Primer》学习笔记(四)
c++·笔记·学习
寂空_44 分钟前
【算法笔记】动态规划基础(一):dp思想、基础线性dp
c++·笔记·算法·动态规划
nenchoumi31191 小时前
LLM 论文精读(二)Training Compute-Optimal Large Language Models
论文阅读·人工智能·笔记·学习·语言模型·自然语言处理
xiao--xin1 小时前
计算机网络笔记(六)——1.6计算机网络的性能
笔记·计算机网络·带宽·计算机基础·性能指标·吞吐量·时延
卡皮巴拉爱吃小蛋糕2 小时前
MySQL的事务(Transaction)【学习笔记】
数据库·笔记·学习·mysql
qq_431331352 小时前
Unity ML-Agents + VScode 环境搭建 Windows
windows·vscode·unity·强化学习
jackson凌2 小时前
【Java学习方法】终止循环的关键字
java·笔记·学习方法
麻溜学习2 小时前
IP地址与子网掩码
笔记
Java与Android技术栈2 小时前
图像编辑器 Monica 之生成漫画风格的图像、以及使用 GPU 实现推理
编辑器
浔川python社3 小时前
《浔川代码编辑器v2.1.0预告》
python·编辑器