目录
- 一、背景
- 二、功能介绍
- 三、实现思路
- 四、脚本继承检测逻辑
- 五、完整核心代码
- 六、使用场景
- 七、目前工具限制
- 八、后续优化方向
- 九、总结
Unity‑Editor 工具:场景资产引用查找器完整教程
一款编辑器工具,一键检索当前场景内所有游戏物体对目标资源、脚本组件的引用关系,快速定位资源被哪些对象挂载使用。
一、背景
在日常 Unity 项目开发、资源整理、废弃资源清理、脚本重构工作中,经常遇到下面痛点:
不清楚贴图、预制体、AudioClip、Sprite 等资源被场景内哪些物体引用。
更换脚本、删除旧脚本时,很难找到场景中挂载该脚本以及其子类脚本的物体。
项目资源冗余,大量废弃资源残留在场景组件字段内,肉眼排查效率极低。
因此编写这款资产引用查找器编辑器窗口工具,遍历当前激活场景全部物体,通过序列化属性检测对象引用、脚本继承关系匹配,一键收集全部引用点位,可视化展示引用详情并支持快速选中物体。
二、功能介绍
- 基础功能
支持拖拽任意 UnityEngine.Object 类型资源作为检索目标:贴图、音频、预制体、组件、Sprite、材质等。
特殊适配 MonoScript 脚本资源:检索场景挂载该脚本以及所有子类脚本的游戏对象。
递归遍历当前激活场景根物体以及全部深层子物体。
检测组件序列化字段内的对象引用,记录组件名 + 属性路径。
识别 GameObject 自身等于目标物体的情况。
检索结果列表展示物体名称、引用详情、一键选中并高亮场景对象。 - 交互面板
ObjectField 拖拽传入需要查找的目标资源。
点击按钮启动场景检索。
滚动视图展示所有命中物体、引用来源。
「选中」按钮快速定位 Hierarchy 层级对应的游戏对象。
三、实现思路
整体流程分为 编辑器窗口初始化 → 目标资源类型判断 → 场景全物体递归遍历 → 组件序列化字段遍历检测引用 → 脚本继承关系匹配 → 结果缓存存储 → GUI 渲染展示。
搭建 EditorWindow 编辑器窗口,提供资源拖拽入口与检索按钮。
判断传入资源是否为 C# 脚本(MonoScript):
是脚本:获取脚本运行时类型,后续用来检测组件继承。
普通资源:存入哈希集合,同时如果目标是 GameObject,则顺带缓存物体身上所有组件。
获取当前激活场景所有根层级 GameObject。
递归遍历每一个 Transform,遍历物体身上全部组件。
两条检测分支:
脚本模式:判断当前组件类型是否继承目标脚本;
普通资源模式:利用 SerializedObject 遍历组件所有序列化属性,比对对象引用;
物体自身等于目标资源时也标记命中;
将命中物体和引用详情存入容器;
在窗口 UI 滚动列表渲染检索结果,提供选中跳转功能。
四、脚本继承检测逻辑
工具专门针对脚本挂载场景做了继承适配:
if(_targetScriptType!=null){if(_targetScriptType.IsAssignableFrom(comp.GetType())&&comp.GetType()!=_targetScriptType){details.Add($"{comp.GetType().Name}-{_targetScriptType.Name}");}}IsAssignableFrom 用来判断当前组件类型是否可以赋值给目标脚本类型。
增加判断 comp.GetType() != _targetScriptType,只匹配子类,不会命中脚本本身。
命中之后记录子类名称和父类脚本名称,方便开发者查看继承挂载关系。
如果你需要同时检索脚本本身 + 子类,可以删掉 GetType() != _targetScriptType 这一段条件。
五、完整核心代码
usingSystem;usingSystem.Collections.Generic;usingSystem.Text;usingUnityEditor;usingUnityEngine;usingObject=UnityEngine.Object;publicclassAssetReferenceFinderWindow:EditorWindow{#region字段privateObject_targetAsset;privateVector2_scrollPos;privatebool_hasSearched;privatestring_searchInfo="";privatebool_isSearching;// 搜索结果privatereadonlyList<GameObject>_results=newList<GameObject>();privatereadonlyDictionary<GameObject,List<string>>_resultDetails=newDictionary<GameObject,List<string>>();// 搜索上下文privateHashSet<Object>_targetObjects;privateType_targetScriptType;privatebool_isScriptSearch;#endregion#region菜单入口[MenuItem("Tools/资产引用查找器")]publicstaticvoidOpen(){GetWindow<AssetReferenceFinderWindow>("资源引用查找器");}#endregion#regionGUI绘制privatevoidOnGUI(){EditorGUILayout.Space(4);DrawSearchArea();EditorGUILayout.Space(8);if(_hasSearched){DrawResults();}elseif(!string.IsNullOrEmpty(_searchInfo)){EditorGUILayout.HelpBox(_searchInfo,MessageType.Info);}}privatevoidDrawSearchArea(){EditorGUILayout.BeginHorizontal();_targetAsset=EditorGUILayout.ObjectField("目标资源",_targetAsset,typeof(Object),true);EditorGUILayout.EndHorizontal();EditorGUILayout.Space(4);using(newEditorGUI.DisabledScope(_targetAsset==null||_isSearching)){if(GUILayout.Button("在场景中查找引用",GUILayout.Height(32))){FindReferences();}}if(_isSearching){EditorGUILayout.HelpBox("正在搜索中...",MessageType.Info);}}privatevoidDrawResults(){EditorGUILayout.LabelField(_results.Count>0?$"找到{_results.Count}个引用对象:":"当前场景中未找到对象。",EditorStyles.boldLabel);EditorGUILayout.Space(4);if(_results.Count>0){_scrollPos=EditorGUILayout.BeginScrollView(_scrollPos,GUILayout.ExpandHeight(true));foreach(GameObjectgoin_results){if(go==null)continue;DrawResultItem(go);}EditorGUILayout.EndScrollView();}}privatevoidDrawResultItem(GameObjectgo){EditorGUILayout.BeginHorizontal(GUI.skin.box);EditorGUILayout.LabelField(go.name,EditorStyles.boldLabel,GUILayout.Width(150));RectdetailRect=GUILayoutUtility.GetRect(0,EditorGUIUtility.singleLineHeight,GUILayout.ExpandWidth(true));if(_resultDetails.TryGetValue(go,outvardetails)){GUI.Label(detailRect,string.Join(" | ",details),EditorStyles.label);}if(GUILayout.Button("选中",GUILayout.Width(60))){Selection.activeGameObject=go;EditorGUIUtility.PingObject(go);}EditorGUILayout.EndHorizontal();}#endregion#region核心搜索逻辑privatevoidFindReferences(){// 清理之前的搜索结果_results.Clear();_resultDetails.Clear();_hasSearched=true;_isSearching=true;_searchInfo="";// 验证目标资源if(!ValidateTargetAsset()){_isSearching=false;return;}// 初始化搜索上下文InitializeSearchContext();// 执行搜索try{GameObject[]roots=UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().GetRootGameObjects();foreach(GameObjectrootinroots){SearchGameObject(root.transform);}_searchInfo=_results.Count>0?$"搜索完成,找到{_results.Count}个引用":"搜索完成,未找到任何引用";}catch(Exceptione){Debug.LogError($"搜索过程中发生错误:{e.Message}");_searchInfo=$"搜索出错:{e.Message}";}finally{_isSearching=false;Repaint();}}privateboolValidateTargetAsset(){if(_targetAsset==null){ShowNotification(newGUIContent("请先指定目标资源"));_searchInfo="请先选择一个目标资源";returnfalse;}// 如果是脚本,验证是否可以获取类型if(_targetAssetisMonoScriptmonoScript){TypescriptType=monoScript.GetClass();if(scriptType==null){ShowNotification(newGUIContent("无法获取脚本类型"));_searchInfo="无法获取脚本类型,请检查脚本是否有效";returnfalse;}}returntrue;}privatevoidInitializeSearchContext(){_targetObjects=newHashSet<Object>();_targetScriptType=null;_isScriptSearch=false;if(_targetAssetisMonoScriptmonoScript){// 脚本搜索模式_targetScriptType=monoScript.GetClass();_isScriptSearch=true;Debug.Log($"搜索模式:脚本类型 [{_targetScriptType.Name}]");}else{// 资源引用搜索模式_targetObjects.Add(_targetAsset);// 如果是GameObject,添加所有组件if(_targetAssetisGameObjecttargetGo){foreach(ComponentcompintargetGo.GetComponents<Component>()){if(comp!=null)_targetObjects.Add(comp);}}// 如果是Component,也添加自身elseif(_targetAssetisComponenttargetComp){_targetObjects.Add(targetComp);}Debug.Log($"搜索模式:资源引用 [{_targetAsset.name}],共{_targetObjects.Count}个目标对象");}}privatevoidSearchGameObject(Transformtransform){GameObjectgo=transform.gameObject;List<string>details=newList<string>();Component[]components=go.GetComponents<Component>();foreach(Componentcompincomponents){if(comp==null)continue;if(_isScriptSearch){// 脚本搜索模式:检测是否挂载了目标脚本或其子类CheckScriptReference(comp,details);}else{// 资源引用搜索模式:检测组件的序列化属性是否引用了目标资源CheckObjectReference(comp,details);}}// 检查GameObject本身是否是目标if(!_isScriptSearch&&_targetObjects.Contains(go)){details.Add("GameObject(本身)");}// 如果有匹配,添加到结果if(details.Count>0){_results.Add(go);_resultDetails[go]=details;}// 递归搜索子对象for(inti=0;i<transform.childCount;i++){SearchGameObject(transform.GetChild(i));}}privatevoidCheckScriptReference(Componentcomp,List<string>details){TypecompType=comp.GetType();// 检查是否是目标脚本或其子类if(_targetScriptType.IsAssignableFrom(compType)){stringscriptName=compType==_targetScriptType?_targetScriptType.Name:$"{compType.Name}(继承自{_targetScriptType.Name})";details.Add($"挂载脚本:{scriptName}");}}privatevoidCheckObjectReference(Componentcomp,List<string>details){SerializedObjectso=newSerializedObject(comp);SerializedPropertyprop=so.GetIterator();// 遍历所有序列化属性while(prop.NextVisible(true)){// 只检查对象引用类型if(prop.propertyType==SerializedPropertyType.ObjectReference){ObjectreferencedObject=prop.objectReferenceValue;if(referencedObject!=null&&_targetObjects.Contains(referencedObject)){// 格式化属性路径stringpropertyPath=FormatPropertyPath(prop.propertyPath);details.Add($"{comp.GetType().Name}.{propertyPath}");}}}}privatestringFormatPropertyPath(stringpath){// 将数组路径格式化为更易读的形式returnpath.Replace(".Array.data[","[");}#endregion#region公共方法/// <summary>/// 外部调用:设置目标资源并自动搜索/// </summary>publicvoidSearchForAsset(Objectasset){_targetAsset=asset;FindReferences();}/// <summary>/// 清空搜索结果/// </summary>publicvoidClearResults(){_results.Clear();_resultDetails.Clear();_hasSearched=false;_searchInfo="";Repaint();}#endregion}六、使用场景
资源清理:查找场景内废弃贴图、音效、预制体引用位置,解除无用引用,降低场景内存占用;
脚本重构:父类脚本准备重构、删除时,快速找到场景所有挂载子类脚本的物体;
预制体排查:查找场景哪里引用了目标预制件;
组件引用检查:检查物体身上脚本字段挂载了哪些资源;
多人协作项目:接手别人工程,快速摸清资源挂载点位;
场景拆分工作:拆分多场景前,梳理资源依赖关系。
七、目前工具限制
仅检索当前激活场景,不会遍历项目全部场景、预制体资源内的引用;
只能识别序列化字段赋值的引用;代码动态运行时通过脚本代码赋值的资源引用无法检测;
不检测 AnimatorController、Shader、LayerMask 等部分特殊类型隐式引用;
脚本检索模式默认只查找子类,不会匹配脚本本身挂载对象;
大批量物体场景递归遍历,物体数量上万时编辑器会出现卡顿;
不会识别数组、List 集合内部存储的资源引用(NextVisible 默认展开可见属性,但深层嵌套数组检测存在漏洞);
不支持筛选隐藏物体、禁用物体。
八、后续优化方向
增加批量全场景检索、项目预制体引用检索功能;
完整遍历数组、List、嵌套结构体里面所有对象引用;
增加过滤选项:休眠物体、UI‑Layer 特定层级物体;
检索进度条,大型场景展示扫描进度;
检索结果增加资源路径、物体层级完整路径;
增加一键解除引用、批量替换资源工具;
异步后台检索,防止编辑器卡顿;
支持快捷键、右键菜单快速启动检索;
区分静态物体、动态物体,增加标签、名字过滤。
九、总结
这款资产引用查找器依靠 Unity 序列化属性 API,遍历场景所有组件字段检测对象引用,同时拓展了脚本子类挂载检测功能。
作为轻量级编辑器工具,可以极大优化资源排查、脚本重构、场景整理的工作效率;后续可以基于现有框架拓展批量场景扫描、预制体检索、批量资源替换等功能,打造一套完整的项目资源检查工具链。