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

前言
在 Unity 项目开发过程中,尤其当项目规模增大、多人协作或外部 SDK、DLL 插件引入后,我们常常遇到这样的需求:"我想知道某个 GameObject 上挂载的某个自定义组件,到底被场景中哪些脚本引用了?" 。手工一个一个场景搜索、检查 Inspector,既容易漏掉隐式引用(数组、列表、UnityEvent),又耗时耗力。本文将从零 出发,以设计思路+关键代码片段 的形式,记录如何一步步实现一款ComponentReferenceFinder 编辑器扩展工具,满足以下核心需求:
- 在 Hierarchy 中选中一个 GameObject;
- 自动列出它挂载的所有"用户脚本"组件(包括自定义 DLL 插件中的 MonoBehaviour);
- 选择其中一个组件实例,点击"查找引用"按钮;
- 遍历场景(包含隐藏和 inactive)的所有 MonoBehaviour,反射查找字段引用、集合引用以及 UnityEvent 持久化引用;
- 将检索结果以可点击的列表形式呈现,支持点击"Ping"快速定位引用者;
- 在查找过程中显示可取消进度条,跳过 Unity 内部或未赋值字段,保证运行稳定。
一、需求拆解与功能规划
在动手编写工具之前,先把需求分解成几个清晰的子问题:
-
如何获取选中 GameObject
利用
Selection.activeGameObject
。当用户在层级面板切换选中对象后,插件要能自动响应并更新状态。 -
如何过滤"自定义组件"
不是所有挂载在 GameObject 上的 MonoBehaviour 都要列出。我们只要项目中自己写的脚本,或通过第三方 DLL 引入的脚本组件,需要排除 Unity 自带的
Transform
、Camera
、Light
、ParticleSystem
,以及编辑器脚本等。 -
如何遍历场景中所有 MonoBehaviour
Unity API 提供
Object.FindObjectsOfType<T>(true)
,可以同时检索包括 inactive 的全部场景物体。对每个物体,用GetComponents<MonoBehaviour>()
枚举挂载的所有脚本。 -
如何检测字段引用
利用反射(
FieldInfo.GetValue
)读取每个 MonoBehaviour 的所有实例字段(包括 public 和 private +[SerializeField]
)。如果字段值等于目标组件实例,则记录"单引用"。 -
如何检测集合引用
当字段类型实现了
IEnumerable
(排除string
),则将其强转为IEnumerable
,对其中每个元素判断是否引用目标组件;若命中则记录"集合引用"。 -
如何检测 UnityEvent 引用
UnityEvent 在 Inspector 上配置的回调保存于序列化属性
m_PersistentCalls.m_Calls
。借助SerializedObject
与SerializedProperty
,可以读取这些持久化数据,从中找到m_Target
字段等于目标组件的调用列表。 -
如何呈现结果并支持定位
在
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 引用,需要:
- 新建
SerializedObject so = new SerializedObject(comp);
var prop = so.FindProperty(field.Name);
var calls = prop.FindPropertyRelative("m_PersistentCalls.m_Calls");
- 遍历
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 完整代码
四、使用步骤
-
安装脚本
- 将
ComponentReferenceFinder.cs
放到项目的Assets/Editor/
目录下。
- 将
-
打开窗口
-
在 Unity 顶部菜单栏依次点击:
Tools -> ExTool-> Component Ref Finder
-
-
选中目标 GameObject
- 在 Hierarchy 面板中点击你要排查的场景物体 A。
- 确保它是场景实例(而非 Prefab 资源文件)。
-
选择组件实例
- 窗口中会列出 A 上所有"用户脚本"组件(包括自定义 DLL 中的 MonoBehaviour)。
- 从下拉框中选择你关心的那个组件(例如
SoundEffectsPlayer
)。
-
执行检索
- 点击 "查找引用该组件的对象" 按钮。
- 窗口底部将出现一个进度条,显示当前正在扫描的 GameObject 名称。
- 在大场景中,你可以随时按 Esc 或点击进度条的取消按钮中断搜索。
-
查看与定位结果
- 检索完成后,底部列表会展示所有引用该组件实例的记录,包括:
- 单字段引用
- 集合引用(List/Array)
- UnityEvent 持久化引用
- 每条记录前有一个 "Ping" 按钮:
- 点击即可在 Hierarchy 中高亮并聚焦对应的引用 GameObject。
- 点击即可在 Hierarchy 中高亮并聚焦对应的引用 GameObject。
- 检索完成后,底部列表会展示所有引用该组件实例的记录,包括:
-
后续排查
- 找到引用后,双击打开对应脚本或在 Inspector 中检查字段,进一步定位用途与调用逻辑。
- 若发现误引用或不再需要的回调,将其清理或重构,以保持场景引用关系的清晰。
五、优化与扩展方向
5.1 优化内容
- 跳过 Unity 内部脚本 :在
IsUserScript(Type t)
中排除UnityEngine.*
、UnityEditor.*
、System.*
等程序集,减少无关反射开销。 - catch 屏蔽 :对所有反射、枚举、序列化操作进行
try-catch
,只跳过当前字段或组件,不影响整体搜索进程。 - 进度条:分批刷新,避免一次性满帧卡死。
5.2 扩展方向
- 链式引用:支持递归查找 A → B → C,识别多级引用关系,并以树状结构展示;
- 场景快照对比:记录不同场景版本之间的引用差异,帮助回归测试;
- 导出报告:将结果保存为 JSON、CSV 或 Markdown 文档,便于团队共享;
- 代码静态分析集成 :结合 Mono.Cecil、Roslyn,定位脚本中对组件方法(如
.Play()
)的调用; - 上下文菜单:在 Inspector 中右键某组件,当选时直接调用查找工具。
六、结语
本文以需求驱动 的方式,记录了从零构思到实现一个通用的 Unity 编辑器扩展------ComponentReferenceFinder 的全过程。它不仅能满足字段级引用查找,还兼顾集合和 UnityEvent 的场景,适配自定义 DLL 和大型多人项目。
把它集成到你的项目中,可以大幅提升定位隐式依赖、排查丢失引用、管理复杂组件关系的效率。希望这篇笔记能帮助你快速掌握 Unity 编辑器扩展与反射、序列化属性的组合使用思路,从而更好地构建属于自己团队的开发工具链。
欢迎在评论区分享你的使用体验与改进想法!