【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 编辑器扩展与反射、序列化属性的组合使用思路,从而更好地构建属于自己团队的开发工具链。

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

相关推荐
切韵9 天前
Unity编辑器扩展:UI绑定复制工具
ui·unity·编辑器
懒惰的bit9 天前
STM32F103C8T6 学习笔记摘要(四)
笔记·stm32·学习
zkyqss9 天前
OVS Faucet练习(下)
linux·笔记·openstack
浦东新村轱天乐10 天前
【麻省理工】《how to speaking》笔记
笔记
奔跑的蜗牛AZ10 天前
TiDB 字符串行转列与 JSON 数据查询优化知识笔记
笔记·json·tidb
cwtlw10 天前
Excel学习03
笔记·学习·其他·excel
杭州杭州杭州10 天前
计算机网络笔记
笔记·计算机网络
cyborg10 天前
终于再也不用在notion中写公式了
笔记
循环过三天10 天前
1.2、CAN总线帧格式
笔记·stm32·单片机·嵌入式硬件·学习
循环过三天10 天前
1.1、CAN总线简介
笔记·stm32·单片机·嵌入式硬件·学习