news 2026/9/12 6:17:54

场景资产引用查找器 - EditorWindow

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
场景资产引用查找器 - EditorWindow

目录

    • 一、背景
    • 二、功能介绍
    • 三、实现思路
    • 四、脚本继承检测逻辑
    • 五、完整核心代码
    • 六、使用场景
    • 七、目前工具限制
    • 八、后续优化方向
    • 九、总结

Unity‑Editor 工具:场景资产引用查找器完整教程
一款编辑器工具,一键检索当前场景内所有游戏物体对目标资源、脚本组件的引用关系,快速定位资源被哪些对象挂载使用。

一、背景

在日常 Unity 项目开发、资源整理、废弃资源清理、脚本重构工作中,经常遇到下面痛点:
不清楚贴图、预制体、AudioClip、Sprite 等资源被场景内哪些物体引用。
更换脚本、删除旧脚本时,很难找到场景中挂载该脚本以及其子类脚本的物体。
项目资源冗余,大量废弃资源残留在场景组件字段内,肉眼排查效率极低。
因此编写这款资产引用查找器编辑器窗口工具,遍历当前激活场景全部物体,通过序列化属性检测对象引用、脚本继承关系匹配,一键收集全部引用点位,可视化展示引用详情并支持快速选中物体。

二、功能介绍

  1. 基础功能
    支持拖拽任意 UnityEngine.Object 类型资源作为检索目标:贴图、音频、预制体、组件、Sprite、材质等。
    特殊适配 MonoScript 脚本资源:检索场景挂载该脚本以及所有子类脚本的游戏对象。
    递归遍历当前激活场景根物体以及全部深层子物体。
    检测组件序列化字段内的对象引用,记录组件名 + 属性路径。
    识别 GameObject 自身等于目标物体的情况。
    检索结果列表展示物体名称、引用详情、一键选中并高亮场景对象。
  2. 交互面板
    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,遍历场景所有组件字段检测对象引用,同时拓展了脚本子类挂载检测功能。
作为轻量级编辑器工具,可以极大优化资源排查、脚本重构、场景整理的工作效率;后续可以基于现有框架拓展批量场景扫描、预制体检索、批量资源替换等功能,打造一套完整的项目资源检查工具链。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 14:05:56

机械键盘连击拦截指南:让每个按键只敲出一个字符

机械键盘连击拦截指南&#xff1a;让每个按键只敲出一个字符 【免费下载链接】KeyboardChatterBlocker A handy quick tool for blocking mechanical keyboard chatter. 项目地址: https://gitcode.com/gh_mirrors/ke/KeyboardChatterBlocker 想打 "the"&…

作者头像 李华
网站建设 2026/8/29 21:49:42

AI开放训练如何实现可复现?从代码公开到完整实验记录

Marin 项目最近被不少 AI 开发者当作“开放训练”的标杆来讨论。原因不是它用了多么前沿的模型结构&#xff0c;也不是它刷了多么惊人的榜单分数&#xff0c;而是它把一件在 AI 领域里最难、最容易被忽略的事做到了位&#xff1a;让训练过程可以被完整复现。 很多 AI 项目的 G…

作者头像 李华
网站建设 2026/8/31 2:45:45

裸辞!一个好的开始?

裸辞&#xff01;想起来已经是好几年之前的事情&#xff0c;要不是这两天看到好多文章写到了。而且千遍一律的以职场人的身份说裸辞的危害。 裸辞一时爽&#xff0c;生活火葬场 裸辞一时爽&#xff0c;事后悔断肠 过来人经验:再难也别裸辞&#xff0c;三个后果早看明白 我只是…

作者头像 李华
网站建设 2026/9/2 2:56:54

【单片机课程设计/毕业设计】基于 STM32 的多环境因子采集与参数阈值控制系统设计 基于 STM32 的农田气象环境监测报警终端设计(010605)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/8/29 15:27:55

宣誓翻译是什么意思?去哪办快?一篇给你讲透,少花冤枉钱!

&#x1f4cc; 摘要&#xff1a;宣誓翻译是由目标国法院或政府认可的宣誓译员出具的译文&#xff0c;带译员签名、宣誓章和注册编号&#xff0c;具有当地法律效力。去哪办&#xff1f;线上小程序&#xff08;如慧办好&#xff09;、当地法院、线下涉外翻译机构都行&#xff0c;…

作者头像 李华
网站建设 2026/9/9 20:01:38

LTE-M智能调制解调器开发套件实测:从选型到上云避坑指南

去年秋天我在做一个室外资产追踪项目&#xff0c;前期调研时最头疼的环节就是通信选型。WiFi 覆盖太局限&#xff0c;蓝牙网关需要自己布&#xff0c;LoRa 得自己搭基站&#xff0c;项目周期根本不允许。后来我把目光转向了蜂窝物联网&#xff0c;最终锁定了 LTE-M 方案&#x…

作者头像 李华