ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

场景资产引用查找器 - EditorWindow

场景资产引用查找器 - EditorWindow 目录一、背景二、功能介绍三、实现思路四、脚本继承检测逻辑五、完整核心代码六、使用场景七、目前工具限制八、后续优化方向九、总结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;usingObjectUnityEngine.Object;publicclassAssetReferenceFinderWindow:EditorWindow{#region字段privateObject_targetAsset;privateVector2_scrollPos;privatebool_hasSearched;privatestring_searchInfo;privatebool_isSearching;// 搜索结果privatereadonlyListGameObject_resultsnewListGameObject();privatereadonlyDictionaryGameObject,Liststring_resultDetailsnewDictionaryGameObject,Liststring();// 搜索上下文privateHashSetObject_targetObjects;privateType_targetScriptType;privatebool_isScriptSearch;#endregion#region菜单入口[MenuItem(Tools/资产引用查找器)]publicstaticvoidOpen(){GetWindowAssetReferenceFinderWindow(资源引用查找器);}#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();_targetAssetEditorGUILayout.ObjectField(目标资源,_targetAsset,typeof(Object),true);EditorGUILayout.EndHorizontal();EditorGUILayout.Space(4);using(newEditorGUI.DisabledScope(_targetAssetnull||_isSearching)){if(GUILayout.Button(在场景中查找引用,GUILayout.Height(32))){FindReferences();}}if(_isSearching){EditorGUILayout.HelpBox(正在搜索中...,MessageType.Info);}}privatevoidDrawResults(){EditorGUILayout.LabelField(_results.Count0?$找到{_results.Count}个引用对象:当前场景中未找到对象。,EditorStyles.boldLabel);EditorGUILayout.Space(4);if(_results.Count0){_scrollPosEditorGUILayout.BeginScrollView(_scrollPos,GUILayout.ExpandHeight(true));foreach(GameObjectgoin_results){if(gonull)continue;DrawResultItem(go);}EditorGUILayout.EndScrollView();}}privatevoidDrawResultItem(GameObjectgo){EditorGUILayout.BeginHorizontal(GUI.skin.box);EditorGUILayout.LabelField(go.name,EditorStyles.boldLabel,GUILayout.Width(150));RectdetailRectGUILayoutUtility.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.activeGameObjectgo;EditorGUIUtility.PingObject(go);}EditorGUILayout.EndHorizontal();}#endregion#region核心搜索逻辑privatevoidFindReferences(){// 清理之前的搜索结果_results.Clear();_resultDetails.Clear();_hasSearchedtrue;_isSearchingtrue;_searchInfo;// 验证目标资源if(!ValidateTargetAsset()){_isSearchingfalse;return;}// 初始化搜索上下文InitializeSearchContext();// 执行搜索try{GameObject[]rootsUnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().GetRootGameObjects();foreach(GameObjectrootinroots){SearchGameObject(root.transform);}_searchInfo_results.Count0?$搜索完成找到{_results.Count}个引用:搜索完成未找到任何引用;}catch(Exceptione){Debug.LogError($搜索过程中发生错误:{e.Message});_searchInfo$搜索出错:{e.Message};}finally{_isSearchingfalse;Repaint();}}privateboolValidateTargetAsset(){if(_targetAssetnull){ShowNotification(newGUIContent(请先指定目标资源));_searchInfo请先选择一个目标资源;returnfalse;}// 如果是脚本验证是否可以获取类型if(_targetAssetisMonoScriptmonoScript){TypescriptTypemonoScript.GetClass();if(scriptTypenull){ShowNotification(newGUIContent(无法获取脚本类型));_searchInfo无法获取脚本类型请检查脚本是否有效;returnfalse;}}returntrue;}privatevoidInitializeSearchContext(){_targetObjectsnewHashSetObject();_targetScriptTypenull;_isScriptSearchfalse;if(_targetAssetisMonoScriptmonoScript){// 脚本搜索模式_targetScriptTypemonoScript.GetClass();_isScriptSearchtrue;Debug.Log($搜索模式脚本类型 [{_targetScriptType.Name}]);}else{// 资源引用搜索模式_targetObjects.Add(_targetAsset);// 如果是GameObject添加所有组件if(_targetAssetisGameObjecttargetGo){foreach(ComponentcompintargetGo.GetComponentsComponent()){if(comp!null)_targetObjects.Add(comp);}}// 如果是Component也添加自身elseif(_targetAssetisComponenttargetComp){_targetObjects.Add(targetComp);}Debug.Log($搜索模式资源引用 [{_targetAsset.name}]共{_targetObjects.Count}个目标对象);}}privatevoidSearchGameObject(Transformtransform){GameObjectgotransform.gameObject;ListstringdetailsnewListstring();Component[]componentsgo.GetComponentsComponent();foreach(Componentcompincomponents){if(compnull)continue;if(_isScriptSearch){// 脚本搜索模式检测是否挂载了目标脚本或其子类CheckScriptReference(comp,details);}else{// 资源引用搜索模式检测组件的序列化属性是否引用了目标资源CheckObjectReference(comp,details);}}// 检查GameObject本身是否是目标if(!_isScriptSearch_targetObjects.Contains(go)){details.Add(GameObject本身);}// 如果有匹配添加到结果if(details.Count0){_results.Add(go);_resultDetails[go]details;}// 递归搜索子对象for(inti0;itransform.childCount;i){SearchGameObject(transform.GetChild(i));}}privatevoidCheckScriptReference(Componentcomp,Liststringdetails){TypecompTypecomp.GetType();// 检查是否是目标脚本或其子类if(_targetScriptType.IsAssignableFrom(compType)){stringscriptNamecompType_targetScriptType?_targetScriptType.Name:${compType.Name}(继承自{_targetScriptType.Name});details.Add($挂载脚本:{scriptName});}}privatevoidCheckObjectReference(Componentcomp,Liststringdetails){SerializedObjectsonewSerializedObject(comp);SerializedPropertypropso.GetIterator();// 遍历所有序列化属性while(prop.NextVisible(true)){// 只检查对象引用类型if(prop.propertyTypeSerializedPropertyType.ObjectReference){ObjectreferencedObjectprop.objectReferenceValue;if(referencedObject!null_targetObjects.Contains(referencedObject)){// 格式化属性路径stringpropertyPathFormatPropertyPath(prop.propertyPath);details.Add(${comp.GetType().Name}.{propertyPath});}}}}privatestringFormatPropertyPath(stringpath){// 将数组路径格式化为更易读的形式returnpath.Replace(.Array.data[,[);}#endregion#region公共方法/// summary/// 外部调用设置目标资源并自动搜索/// /summarypublicvoidSearchForAsset(Objectasset){_targetAssetasset;FindReferences();}/// summary/// 清空搜索结果/// /summarypublicvoidClearResults(){_results.Clear();_resultDetails.Clear();_hasSearchedfalse;_searchInfo;Repaint();}#endregion}六、使用场景资源清理查找场景内废弃贴图、音效、预制体引用位置解除无用引用降低场景内存占用脚本重构父类脚本准备重构、删除时快速找到场景所有挂载子类脚本的物体预制体排查查找场景哪里引用了目标预制件组件引用检查检查物体身上脚本字段挂载了哪些资源多人协作项目接手别人工程快速摸清资源挂载点位场景拆分工作拆分多场景前梳理资源依赖关系。七、目前工具限制仅检索当前激活场景不会遍历项目全部场景、预制体资源内的引用只能识别序列化字段赋值的引用代码动态运行时通过脚本代码赋值的资源引用无法检测不检测 AnimatorController、Shader、LayerMask 等部分特殊类型隐式引用脚本检索模式默认只查找子类不会匹配脚本本身挂载对象大批量物体场景递归遍历物体数量上万时编辑器会出现卡顿不会识别数组、List 集合内部存储的资源引用NextVisible 默认展开可见属性但深层嵌套数组检测存在漏洞不支持筛选隐藏物体、禁用物体。八、后续优化方向增加批量全场景检索、项目预制体引用检索功能完整遍历数组、List、嵌套结构体里面所有对象引用增加过滤选项休眠物体、UI‑Layer 特定层级物体检索进度条大型场景展示扫描进度检索结果增加资源路径、物体层级完整路径增加一键解除引用、批量替换资源工具异步后台检索防止编辑器卡顿支持快捷键、右键菜单快速启动检索区分静态物体、动态物体增加标签、名字过滤。九、总结这款资产引用查找器依靠 Unity 序列化属性 API遍历场景所有组件字段检测对象引用同时拓展了脚本子类挂载检测功能。作为轻量级编辑器工具可以极大优化资源排查、脚本重构、场景整理的工作效率后续可以基于现有框架拓展批量场景扫描、预制体检索、批量资源替换等功能打造一套完整的项目资源检查工具链。
返回列表