// Asset Usage Detector - by Suleyman Yasir KULA (yasirkula@gmail.com) using UnityEngine; using UnityEditor; using UnityEngine.SceneManagement; using UnityEditor.SceneManagement; using System.Collections.Generic; using System.Reflection; using System; using System.IO; using System.Text; using Object = UnityEngine.Object; #if UNITY_2018_3_OR_NEWER && !UNITY_2021_2_OR_NEWER using PrefabStage = UnityEditor.Experimental.SceneManagement.PrefabStage; using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility; #endif namespace AssetUsageDetectorNamespace { [Flags] public enum SceneSearchMode { None = 0, OpenScenes = 1, ScenesInBuildSettingsAll = 2, ScenesInBuildSettingsTickedOnly = 4, AllScenes = 8 }; public partial class AssetUsageDetector { #region Helper Classes [Serializable] public class Parameters { public Object[] objectsToSearch = null; public SceneSearchMode searchInScenes = SceneSearchMode.AllScenes; public Object[] searchInScenesSubset = null; public Object[] excludedScenesFromSearch = null; public bool searchInSceneLightingSettings = true; public bool searchInAssetsFolder = true; public Object[] searchInAssetsSubset = null; public Object[] excludedAssetsFromSearch = null; public bool dontSearchInSourceAssets = true; public bool searchInProjectSettings = true; public int searchDepthLimit = 4; public BindingFlags fieldModifiers = BindingFlags.Public | BindingFlags.NonPublic; public BindingFlags propertyModifiers = BindingFlags.Public | BindingFlags.NonPublic; public bool searchNonSerializableVariables = true; public bool searchUnusedMaterialProperties = true; public SearchRefactoring searchRefactoring = null; public bool lazySceneSearch = true; #if ASSET_USAGE_ADDRESSABLES public bool addressablesSupport = false; #endif public bool calculateUnusedObjects = false; public bool hideDuplicateRows = true; public bool hideReduntantPrefabVariantLinks = true; public bool noAssetDatabaseChanges = false; public bool showDetailedProgressBar = true; } #endregion private Parameters searchParameters; // A set that contains the searched scene object(s), asset(s) and their sub-assets (if any) private readonly HashSet objectsToSearchSet = new HashSet(); // Scenes of scene object(s) in objectsToSearchSet private readonly HashSet sceneObjectsToSearchScenesSet = new HashSet(); // Project asset(s) in objectsToSearchSet private readonly HashSet assetsToSearchSet = new HashSet(); // assetsToSearchSet's path(s) private readonly HashSet assetsToSearchPathsSet = new HashSet(); // The root prefab objects in assetsToSearchSet that will be used to search for prefab references private readonly List assetsToSearchRootPrefabs = new List( 4 ); // Path(s) of the assets that should be excluded from the search private readonly HashSet excludedAssetsPathsSet = new HashSet(); // Extension(s) of assets that will always be searched in detail private readonly HashSet alwaysSearchedExtensionsSet = new HashSet(); // Results for the currently searched scene private SearchResultGroup currentSearchResultGroup; // An optimization to search an object only once (key is a hash of the searched object) private readonly Dictionary searchedObjects = new Dictionary( 4096 ); private readonly Dictionary searchedUnityObjects = new Dictionary( 32768 ); // Unity objects use their instanceIDs as key which is more performant // Stack of SearchObject function parameters to avoid infinite loops (which happens when same object is passed as parameter to function) private readonly List callStack = new List( 64 ); private Object currentSearchedObject; private int currentDepth; private bool searchingSourceAssets; private bool isInPlayMode; #if UNITY_2018_3_OR_NEWER private PrefabStage openPrefabStage; private GameObject openPrefabStagePrefabAsset; #if UNITY_2020_1_OR_NEWER private GameObject openPrefabStageContextObject; #endif #endif private int searchedObjectsCount; // Number of searched objects private double searchStartTime; private readonly List nodesPool = new List( 32 ); // Search for references! public SearchResult Run( Parameters searchParameters ) { if( searchParameters == null ) { Debug.LogError( "'searchParameters' mustn't be null!" ); return new SearchResult( false, null, null, null, this, searchParameters ); } if( searchParameters.objectsToSearch == null ) { Debug.LogError( "'objectsToSearch' list (\"SEARCHED OBJECTS\") is empty!" ); return new SearchResult( false, null, null, null, this, searchParameters ); } #if UNITY_2018_3_OR_NEWER openPrefabStagePrefabAsset = null; string openPrefabStageAssetPath = null; openPrefabStage = PrefabStageUtility.GetCurrentPrefabStage(); if( openPrefabStage != null ) { if( !openPrefabStage.stageHandle.IsValid() ) openPrefabStage = null; else { if( openPrefabStage.scene.isDirty ) { // Don't start the search if a prefab stage is currently open and dirty (not saved) Debug.LogError( "Save open prefab first!" ); return new SearchResult( false, null, null, null, this, searchParameters ); } #if UNITY_2020_1_OR_NEWER string prefabAssetPath = openPrefabStage.assetPath; #else string prefabAssetPath = openPrefabStage.prefabAssetPath; #endif openPrefabStagePrefabAsset = AssetDatabase.LoadAssetAtPath( prefabAssetPath ); openPrefabStageAssetPath = prefabAssetPath; #if UNITY_2020_1_OR_NEWER openPrefabStageContextObject = openPrefabStage.openedFromInstanceRoot; #endif } } #endif List searchResult = null; isInPlayMode = EditorApplication.isPlaying; if( !isInPlayMode && !Utilities.AreScenesSaved() && !EditorUtility.DisplayDialog( "Asset Usage Detector", "Some scene(s) aren't saved. This may result in incorrect search results in those scene(s). Proceed?", "Yes", "Cancel" ) ) { Debug.LogError( "Save open scene(s) first!" ); return new SearchResult( false, null, null, null, this, searchParameters ); } // Get the scenes that are open right now SceneSetup[] initialSceneSetup = !isInPlayMode ? EditorSceneManager.GetSceneManagerSetup() : null; Scene activeScene = EditorSceneManager.GetActiveScene(); // Make sure that the AssetDatabase is up-to-date AssetDatabase.SaveAssets(); try { this.searchParameters = searchParameters; // Initialize commonly used variables searchResult = new List(); // Overall search results currentSearchedObject = null; currentDepth = 0; searchedObjectsCount = 0; searchStartTime = EditorApplication.timeSinceStartup; searchedObjects.Clear(); searchedUnityObjects.Clear(); animationClipUniqueBindings.Clear(); callStack.Clear(); objectsToSearchSet.Clear(); sceneObjectsToSearchScenesSet.Clear(); assetsToSearchSet.Clear(); assetsToSearchPathsSet.Clear(); assetsToSearchRootPrefabs.Clear(); excludedAssetsPathsSet.Clear(); alwaysSearchedExtensionsSet.Clear(); shaderIncludesToSearchSet.Clear(); #if UNITY_2017_3_OR_NEWER assemblyDefinitionFilesToSearch.Clear(); #endif if( assetDependencyCache == null ) { LoadCache(); searchStartTime = EditorApplication.timeSinceStartup; } else if( !searchParameters.noAssetDatabaseChanges ) { foreach( var cacheEntry in assetDependencyCache.Values ) cacheEntry.verified = false; } foreach( var cacheEntry in assetDependencyCache.Values ) cacheEntry.searchResult = CacheEntry.Result.Unknown; lastRefreshedCacheEntry = null; // Store the searched objects(s) in HashSets HashSet folderContentsSet = new HashSet(); foreach( Object obj in searchParameters.objectsToSearch ) { if( obj == null || obj.Equals( null ) ) continue; if( obj.IsFolder() ) folderContentsSet.UnionWith( Utilities.EnumerateFolderContents( obj ) ); else AddSearchedObjectToFilteredSets( obj, true ); } foreach( string filePath in folderContentsSet ) { // Skip scene assets if( filePath.EndsWithFast( ".unity" ) ) continue; Object[] assets = AssetDatabase.LoadAllAssetsAtPath( filePath ); if( assets == null || assets.Length == 0 ) continue; for( int i = 0; i < assets.Length; i++ ) AddSearchedObjectToFilteredSets( assets[i], true ); } // Find Project Settings to search for references. Don't search Project Settings if searched object(s) are all scene objects // as Project Settings can't hold references to scene objects string[] projectSettingsToSearch = new string[0]; if( searchParameters.searchInProjectSettings && assetsToSearchSet.Count > 0 ) { string[] projectSettingsAssets = Directory.GetFiles( "ProjectSettings" ); projectSettingsToSearch = new string[projectSettingsAssets.Length]; for( int i = 0; i < projectSettingsAssets.Length; i++ ) projectSettingsToSearch[i] = "ProjectSettings/" + Path.GetFileName( projectSettingsAssets[i] ); // AssetDatabase.GetDependencies doesn't work with Project Settings assets. By adding these assets to assetsToSearchPathsSet, // we make sure that AssetHasAnyReference returns true for these assets and they don't get excluded from the search assetsToSearchPathsSet.UnionWith( projectSettingsToSearch ); } // Find the scenes to search for references HashSet scenesToSearch = new HashSet(); if( searchParameters.searchInScenesSubset != null && searchParameters.searchInScenesSubset.Length > 0 ) { foreach( Object obj in searchParameters.searchInScenesSubset ) { if( obj == null || obj.Equals( null ) ) continue; if( !obj.IsAsset() ) continue; if( obj.IsFolder() ) { string[] folderContents = AssetDatabase.FindAssets( "t:SceneAsset", new string[] { AssetDatabase.GetAssetPath( obj ) } ); if( folderContents == null ) continue; for( int i = 0; i < folderContents.Length; i++ ) scenesToSearch.Add( AssetDatabase.GUIDToAssetPath( folderContents[i] ) ); } else if( obj is SceneAsset ) scenesToSearch.Add( AssetDatabase.GetAssetPath( obj ) ); } } else if( ( searchParameters.searchInScenes & SceneSearchMode.AllScenes ) == SceneSearchMode.AllScenes ) { // Get all scenes from the Assets folder string[] sceneGuids = AssetDatabase.FindAssets( "t:SceneAsset" ); for( int i = 0; i < sceneGuids.Length; i++ ) scenesToSearch.Add( AssetDatabase.GUIDToAssetPath( sceneGuids[i] ) ); } else { if( ( searchParameters.searchInScenes & SceneSearchMode.OpenScenes ) == SceneSearchMode.OpenScenes ) { // Get all open (and loaded) scenes for( int i = 0; i < SceneManager.sceneCount; i++ ) { Scene scene = SceneManager.GetSceneAt( i ); if( scene.IsValid() && scene.isLoaded ) scenesToSearch.Add( scene.path ); } } bool searchInScenesInBuildTickedAll = ( searchParameters.searchInScenes & SceneSearchMode.ScenesInBuildSettingsAll ) == SceneSearchMode.ScenesInBuildSettingsAll; if( searchInScenesInBuildTickedAll || ( searchParameters.searchInScenes & SceneSearchMode.ScenesInBuildSettingsTickedOnly ) == SceneSearchMode.ScenesInBuildSettingsTickedOnly ) { // Get all scenes in build settings EditorBuildSettingsScene[] scenesTemp = EditorBuildSettings.scenes; for( int i = 0; i < scenesTemp.Length; i++ ) { if( ( searchInScenesInBuildTickedAll || scenesTemp[i].enabled ) ) scenesToSearch.Add( scenesTemp[i].path ); } } } // In Play mode, only open scenes can be searched if( isInPlayMode ) { HashSet openScenes = new HashSet(); for( int i = 0; i < SceneManager.sceneCount; i++ ) { Scene scene = SceneManager.GetSceneAt( i ); if( scene.IsValid() && scene.isLoaded ) openScenes.Add( scene.path ); } List skippedScenes = new List( scenesToSearch.Count ); scenesToSearch.RemoveWhere( ( path ) => { if( !openScenes.Contains( path ) ) { skippedScenes.Add( path ); return true; } return false; } ); if( skippedScenes.Count > 0 ) { StringBuilder sb = Utilities.stringBuilder; sb.Length = 0; sb.Append( "Can't search unloaded scenes while in play mode, skipped " ).Append( skippedScenes.Count ).AppendLine( " scene(s):" ); for( int i = 0; i < skippedScenes.Count; i++ ) sb.Append( "- " ).AppendLine( skippedScenes[i] ); Debug.Log( sb.ToString() ); } } // Initialize data used by search functions InitializeSearchFunctionsData( searchParameters ); // Initialize the nodes of searched asset(s) foreach( Object obj in objectsToSearchSet ) searchedUnityObjects.Add( obj.GetInstanceID(), PopReferenceNode( obj ) ); // Progressbar values int searchProgress = 0; int searchTotalProgress = scenesToSearch.Count; if( isInPlayMode && searchParameters.searchInScenes != SceneSearchMode.None ) searchTotalProgress++; // DontDestroyOnLoad scene if( searchParameters.showDetailedProgressBar ) searchTotalProgress += projectSettingsToSearch.Length; // Don't search assets if searched object(s) are all scene objects as assets can't hold references to scene objects if( searchParameters.searchInAssetsFolder && assetsToSearchSet.Count > 0 ) { currentSearchResultGroup = new SearchResultGroup( "Project Window (Assets)", SearchResultGroup.GroupType.Assets ); // Get the paths of all assets that are to be searched IEnumerable assetPaths; if( searchParameters.searchInAssetsSubset == null || searchParameters.searchInAssetsSubset.Length == 0 ) { string[] allAssetPaths = AssetDatabase.GetAllAssetPaths(); assetPaths = allAssetPaths; if( searchParameters.showDetailedProgressBar ) searchTotalProgress += allAssetPaths.Length; } else { folderContentsSet.Clear(); foreach( Object obj in searchParameters.searchInAssetsSubset ) { if( obj == null || obj.Equals( null ) ) continue; if( !obj.IsAsset() ) continue; if( obj.IsFolder() ) folderContentsSet.UnionWith( Utilities.EnumerateFolderContents( obj ) ); else folderContentsSet.Add( AssetDatabase.GetAssetPath( obj ) ); } assetPaths = folderContentsSet; if( searchParameters.showDetailedProgressBar ) searchTotalProgress += folderContentsSet.Count; } // Calculate the path(s) of the assets that won't be searched for references if( searchParameters.excludedAssetsFromSearch != null ) { foreach( Object obj in searchParameters.excludedAssetsFromSearch ) { if( obj == null || obj.Equals( null ) ) continue; if( !obj.IsAsset() ) continue; if( obj.IsFolder() ) excludedAssetsPathsSet.UnionWith( Utilities.EnumerateFolderContents( obj ) ); else excludedAssetsPathsSet.Add( AssetDatabase.GetAssetPath( obj ) ); } } if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching assets", 0f ) ) throw new Exception( "Search aborted" ); foreach( string path in assetPaths ) { if( searchParameters.showDetailedProgressBar && ++searchProgress % 30 == 1 && EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching assets", (float) searchProgress / searchTotalProgress ) ) throw new Exception( "Search aborted" ); if( excludedAssetsPathsSet.Contains( path ) ) continue; // If asset resides inside the Assets directory and is not a scene asset if( path.StartsWithFast( "Assets/" ) && !path.EndsWithFast( ".unity" ) ) { if( !AssetHasAnyReference( path ) ) continue; Object[] assets = AssetDatabase.LoadAllAssetsAtPath( path ); if( assets == null || assets.Length == 0 ) continue; for( int i = 0; i < assets.Length; i++ ) { // Components are already searched while searching the GameObject if( assets[i] is Component ) continue; BeginSearchObject( assets[i] ); } } } // If a reference is found in the Project view, save the results if( currentSearchResultGroup.NumberOfReferences > 0 ) searchResult.Add( currentSearchResultGroup ); } // Search all assets inside the ProjectSettings folder if( projectSettingsToSearch.Length > 0 ) { currentSearchResultGroup = new SearchResultGroup( "Project Settings", SearchResultGroup.GroupType.ProjectSettings ); if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching Project Settings", (float) searchProgress / searchTotalProgress ) ) throw new Exception( "Search aborted" ); for( int i = 0; i < projectSettingsToSearch.Length; i++ ) { if( searchParameters.showDetailedProgressBar && ++searchProgress % 30 == 1 && EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching Project Settings", (float) searchProgress / searchTotalProgress ) ) throw new Exception( "Search aborted" ); Object[] assets = AssetDatabase.LoadAllAssetsAtPath( projectSettingsToSearch[i] ); if( assets != null && assets.Length > 0 ) { for( int j = 0; j < assets.Length; j++ ) BeginSearchObject( assets[j] ); } } if( currentSearchResultGroup.NumberOfReferences > 0 ) searchResult.Add( currentSearchResultGroup ); } // Search non-serializable variables for references while searching a scene in play mode if( isInPlayMode ) searchSerializableVariablesOnly = false; if( scenesToSearch.Count > 0 ) { // Calculate the path(s) of the scenes that won't be searched for references HashSet excludedScenesPathsSet = new HashSet(); if( searchParameters.excludedScenesFromSearch != null ) { foreach( Object obj in searchParameters.excludedScenesFromSearch ) { if( obj == null || obj.Equals( null ) ) continue; if( !obj.IsAsset() ) continue; if( obj.IsFolder() ) { string[] folderContents = AssetDatabase.FindAssets( "t:SceneAsset", new string[] { AssetDatabase.GetAssetPath( obj ) } ); if( folderContents == null ) continue; for( int i = 0; i < folderContents.Length; i++ ) excludedScenesPathsSet.Add( AssetDatabase.GUIDToAssetPath( folderContents[i] ) ); } else if( obj is SceneAsset ) excludedScenesPathsSet.Add( AssetDatabase.GetAssetPath( obj ) ); } } // Search scenes for references foreach( string scenePath in scenesToSearch ) { if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching scene: " + scenePath, (float) ++searchProgress / searchTotalProgress ) ) throw new Exception( "Search aborted" ); if( string.IsNullOrEmpty( scenePath ) ) continue; if( excludedScenesPathsSet.Contains( scenePath ) ) continue; #if UNITY_2019_2_OR_NEWER // Skip scenes in read-only packages (Issue #36) // Credit: https://forum.unity.com/threads/check-if-asset-inside-package-is-readonly.900902/#post-5990822 if( !scenePath.StartsWithFast( "Assets/" ) ) { var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath( scenePath ); if( packageInfo != null && packageInfo.source != UnityEditor.PackageManager.PackageSource.Embedded && packageInfo.source != UnityEditor.PackageManager.PackageSource.Local ) continue; } #endif SearchScene( scenePath, searchResult, searchParameters, initialSceneSetup ); } } // Search through all the GameObjects under the DontDestroyOnLoad scene (if exists) if( isInPlayMode && searchParameters.searchInScenes != SceneSearchMode.None ) { if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching scene: DontDestroyOnLoad", 1f ) ) throw new Exception( "Search aborted" ); currentSearchResultGroup = new SearchResultGroup( "DontDestroyOnLoad", SearchResultGroup.GroupType.DontDestroyOnLoad ); GameObject[] rootGameObjects = GetDontDestroyOnLoadObjects(); for( int i = 0; i < rootGameObjects.Length; i++ ) SearchGameObjectRecursively( rootGameObjects[i] ); if( currentSearchResultGroup.NumberOfReferences > 0 ) searchResult.Add( currentSearchResultGroup ); } // Searching source assets last prevents some references from being excluded due to callStack.ContainsFast if( !searchParameters.dontSearchInSourceAssets ) { searchingSourceAssets = true; foreach( Object obj in objectsToSearchSet ) { currentSearchedObject = obj; SearchObject( obj ); } searchingSourceAssets = false; } EditorUtility.DisplayProgressBar( "Please wait...", "Post-processing search results", 1f ); InitializeSearchResultNodes( searchResult ); HashSet usedObjects = null; if( searchResult.Count > 0 && searchParameters.calculateUnusedObjects ) CalculateUnusedObjects( searchResult, out usedObjects ); // Log some c00l stuff to console Debug.Log( "Searched " + searchedObjectsCount + " objects in " + ( EditorApplication.timeSinceStartup - searchStartTime ).ToString( "F2" ) + " seconds" ); return new SearchResult( true, searchResult, usedObjects, initialSceneSetup, this, searchParameters ); } catch( Exception e ) { StringBuilder sb = Utilities.stringBuilder; sb.Length = 0; sb.EnsureCapacity( objectsToSearchSet.Count * 50 + callStack.Count * 50 + 500 ); sb.AppendLine( "AssetUsageDetector Error: The following Exception is thrown during the search. Details:" ); Object latestUnityObjectInCallStack = AppendCallStackToStringBuilder( sb ); sb.AppendLine( "Searching references of: " ); foreach( Object obj in objectsToSearchSet ) { if( obj ) sb.Append( obj.name ).Append( " (" ).Append( obj.GetType() ).AppendLine( ")" ); } Debug.LogError( sb.ToString(), latestUnityObjectInCallStack ); Debug.LogException( e, latestUnityObjectInCallStack ); try { InitializeSearchResultNodes( searchResult ); } catch { } return new SearchResult( false, searchResult, null, initialSceneSetup, this, searchParameters ); } finally { currentSearchResultGroup = null; currentSearchedObject = null; EditorUtility.ClearProgressBar(); // If the active scene was changed during search, reset it if( EditorSceneManager.GetActiveScene() != activeScene ) EditorSceneManager.SetActiveScene( activeScene ); #if UNITY_2018_3_OR_NEWER // If a prefab stage was open when the search was triggered, try reopening the prefab stage after the search is completed if( !string.IsNullOrEmpty( openPrefabStageAssetPath ) ) { #if UNITY_2020_1_OR_NEWER bool shouldOpenPrefabStageWithoutContext = true; if( openPrefabStageContextObject != null && !openPrefabStageContextObject.Equals( null ) ) { try { // Try to access this method: https://github.com/Unity-Technologies/UnityCsReference/blob/73925b1711847c067e607ec8371f8e9ffe7ab65d/Editor/Mono/SceneManagement/StageManager/PrefabStage/PrefabStageUtility.cs#L61-L65 MethodInfo prefabStageOpenerWithContext = typeof( PrefabStageUtility ).GetMethod( "OpenPrefab", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new Type[2] { typeof( string ), typeof( GameObject ) }, null ); if( prefabStageOpenerWithContext != null ) { prefabStageOpenerWithContext.Invoke( null, new object[2] { openPrefabStageAssetPath, openPrefabStageContextObject } ); shouldOpenPrefabStageWithoutContext = false; } } catch { } } if( shouldOpenPrefabStageWithoutContext ) #endif { AssetDatabase.OpenAsset( AssetDatabase.LoadAssetAtPath( openPrefabStageAssetPath ) ); } } #endif } } private void InitializeSearchResultNodes( List searchResult ) { for( int i = 0; i < searchResult.Count; i++ ) { searchResult[i].InitializeNodes( objectsToSearchSet ); // Remove empty search result groups if( !searchResult[i].PendingSearch && searchResult[i].NumberOfReferences == 0 ) searchResult.RemoveAt( i-- ); } } private void CalculateUnusedObjects( List searchResult, out HashSet usedObjectsSet ) { currentSearchResultGroup = new SearchResultGroup( "Unused Objects", SearchResultGroup.GroupType.UnusedObjects, false, false ); usedObjectsSet = new HashSet(); HashSet usedObjectPathsSet = new HashSet(); // For assets: stores the filepaths, For scene objects: stores the topmost GameObject's instanceID foreach( SearchResultGroup searchResultGroup in searchResult ) { for( int j = 0; j < searchResultGroup.NumberOfReferences; j++ ) { Object obj = searchResultGroup[j].UnityObject; if( obj is Component ) obj = ( (Component) obj ).gameObject; if( usedObjectsSet.Add( obj ) ) { string assetPath = AssetDatabase.GetAssetPath( obj ); if( !string.IsNullOrEmpty( assetPath ) ) usedObjectPathsSet.Add( assetPath ); else { for( Transform parent = ( (GameObject) obj ).transform.parent; parent != null; parent = parent.parent ) usedObjectPathsSet.Add( parent.gameObject.GetInstanceID().ToString() ); } } } } Dictionary unusedMainObjectNodes = new Dictionary( objectsToSearchSet.Count - usedObjectsSet.Count ); Dictionary> unusedSubObjectNodes = new Dictionary>( objectsToSearchSet.Count - usedObjectsSet.Count ); foreach( Object obj in objectsToSearchSet ) { // Omit components, their GameObjects are already included in search if( obj is Component ) continue; // Omit assets that are invisible in Hierarchy/Inspector if( ( obj.hideFlags & ( HideFlags.HideInInspector | HideFlags.HideInHierarchy ) ) != HideFlags.None ) continue; if( usedObjectsSet.Contains( obj ) ) continue; string assetPath = AssetDatabase.GetAssetPath( obj ); // Omit unused sub-assets whose parent assets are used (configurable via Settings) if( AssetUsageDetectorSettings.MarkUsedAssetsSubAssetsAsUsed && AssetDatabase.IsSubAsset( obj ) && usedObjectsSet.Contains( AssetDatabase.LoadMainAssetAtPath( assetPath ) ) ) continue; // Omit meshes of an imported model asset if( obj is Mesh && !string.IsNullOrEmpty( assetPath ) && AssetDatabase.GetMainAssetTypeAtPath( assetPath ) == typeof( GameObject ) && objectsToSearchSet.Contains( AssetDatabase.LoadMainAssetAtPath( assetPath ) ) ) continue; // Omit MonoScripts whose types can't be determined if( obj is MonoScript && ( (MonoScript) obj ).GetClass() == null ) continue; GameObject searchedTopmostGameObject = null; if( obj is GameObject ) { if( string.IsNullOrEmpty( assetPath ) ) { for( Transform parent = ( (GameObject) obj ).transform.parent; parent != null; parent = parent.parent ) { if( objectsToSearchSet.Contains( parent ) && !usedObjectsSet.Contains( parent.gameObject ) ) searchedTopmostGameObject = parent.gameObject; } } else { for( Transform parent = ( (GameObject) obj ).transform.parent; parent != null; parent = parent.parent ) { if( objectsToSearchSet.Contains( parent ) ) { searchedTopmostGameObject = parent.gameObject; break; } } } if( searchedTopmostGameObject && !string.IsNullOrEmpty( assetPath ) ) // Omit GameObject assets if their parent objects are already included in search continue; } // Use new ReferenceNodes in UnusedObjects search result group because we don't want these nodes to be linked to the actual ReferenceNodes in any way // (i.e. we don't use actual ReferenceNodes of these objects (GetReferenceNode) because these may have links to other nodes in unknown circumstances) ReferenceNode node = PopReferenceNode( obj ); node.usedState = ReferenceNode.UsedState.Unused; if( string.IsNullOrEmpty( assetPath ) ) { if( !searchedTopmostGameObject ) { if( obj is GameObject ) unusedMainObjectNodes[obj.GetInstanceID().ToString()] = node; else currentSearchResultGroup.AddReference( node ); } else // List child GameObject scene objects under their parent GameObject { string dictionaryKey = searchedTopmostGameObject.GetInstanceID().ToString(); List unusedSubObjectNodesAtPath; if( !unusedSubObjectNodes.TryGetValue( dictionaryKey, out unusedSubObjectNodesAtPath ) ) unusedSubObjectNodes[dictionaryKey] = unusedSubObjectNodesAtPath = new List( 2 ); unusedSubObjectNodesAtPath.Add( node ); } } else { if( AssetDatabase.IsMainAsset( obj ) ) unusedMainObjectNodes[assetPath] = node; else { List unusedSubObjectNodesAtPath; if( !unusedSubObjectNodes.TryGetValue( assetPath, out unusedSubObjectNodesAtPath ) ) unusedSubObjectNodes[assetPath] = unusedSubObjectNodesAtPath = new List( 2 ); unusedSubObjectNodesAtPath.Add( node ); } } } foreach( KeyValuePair kvPair in unusedMainObjectNodes ) { List unusedSubAssetNodesAtPath; if( unusedSubObjectNodes.TryGetValue( kvPair.Key, out unusedSubAssetNodesAtPath ) ) { currentSearchResultGroup.AddReference( kvPair.Value ); for( int i = 0; i < unusedSubAssetNodesAtPath.Count; i++ ) kvPair.Value.AddLinkTo( unusedSubAssetNodesAtPath[i] ); if( usedObjectPathsSet.Contains( kvPair.Key ) ) kvPair.Value.usedState = ReferenceNode.UsedState.MixedCollapsed; unusedSubObjectNodes.Remove( kvPair.Key ); } else if( !usedObjectPathsSet.Contains( kvPair.Key ) ) // If a main asset has sub-assets and all of them are used, consider the main asset as used, as well (especially useful for Sprite assets) currentSearchResultGroup.AddReference( kvPair.Value ); else if( !AssetDatabase.Contains( (Object) kvPair.Value.nodeObject ) ) { currentSearchResultGroup.AddReference( kvPair.Value ); kvPair.Value.usedState = ReferenceNode.UsedState.MixedCollapsed; } } foreach( KeyValuePair> kvPair in unusedSubObjectNodes ) // These aren't linked to any unusedMainObjectNodes, add them as root nodes to the search result group { foreach( ReferenceNode node in kvPair.Value ) currentSearchResultGroup.AddReference( node ); } if( currentSearchResultGroup.NumberOfReferences > 0 ) { for( int i = 0; i < currentSearchResultGroup.NumberOfReferences; i++ ) currentSearchResultGroup[i].InitializeRecursively(); searchResult.Insert( 0, currentSearchResultGroup ); } } // Checks if object is asset or scene object and adds it to the corresponding HashSet(s) private void AddSearchedObjectToFilteredSets( Object obj, bool expandGameObjects ) { if( obj == null || obj.Equals( null ) ) return; objectsToSearchSet.Add( obj ); #if UNITY_2018_3_OR_NEWER // When searching for references of a prefab stage object, try adding its corresponding prefab asset to the searched assets, as well if( openPrefabStage != null && openPrefabStagePrefabAsset != null && obj is GameObject && openPrefabStage.IsPartOfPrefabContents( (GameObject) obj ) ) { GameObject prefabStageObjectSource = ( (GameObject) obj ).FollowSymmetricHierarchy( openPrefabStage.prefabContentsRoot, openPrefabStagePrefabAsset ); if( prefabStageObjectSource != null ) AddSearchedObjectToFilteredSets( prefabStageObjectSource, expandGameObjects ); } #endif bool isAsset = obj.IsAsset(); if( isAsset ) { assetsToSearchSet.Add( obj ); string assetPath = AssetDatabase.GetAssetPath( obj ); if( !string.IsNullOrEmpty( assetPath ) ) { assetsToSearchPathsSet.Add( assetPath ); if( searchParameters.dontSearchInSourceAssets && AssetDatabase.IsMainAsset( obj ) ) excludedAssetsPathsSet.Add( assetPath ); } GameObject go = null; if( obj is GameObject ) go = (GameObject) obj; else if( obj is Component ) go = ( (Component) obj ).gameObject; if( go != null ) { Transform transform = go.transform; bool shouldAddRootPrefabEntry = true; for( int i = assetsToSearchRootPrefabs.Count - 1; i >= 0; i-- ) { Transform rootTransform = assetsToSearchRootPrefabs[i].transform; if( transform.IsChildOf( rootTransform ) ) { shouldAddRootPrefabEntry = false; break; } if( rootTransform.IsChildOf( transform ) ) assetsToSearchRootPrefabs.RemoveAt( i ); } if( shouldAddRootPrefabEntry ) assetsToSearchRootPrefabs.Add( go ); } } else { if( obj is GameObject ) sceneObjectsToSearchScenesSet.Add( ( (GameObject) obj ).scene.path ); else if( obj is Component ) sceneObjectsToSearchScenesSet.Add( ( (Component) obj ).gameObject.scene.path ); } if( expandGameObjects && obj is GameObject ) { // If searched asset is a GameObject, include its components in the search Component[] components = ( (GameObject) obj ).GetComponents(); for( int i = 0; i < components.Length; i++ ) { if( components[i] == null || components[i].Equals( null ) ) continue; objectsToSearchSet.Add( components[i] ); if( isAsset ) assetsToSearchSet.Add( components[i] ); } } else if( obj is Component ) { // Include searched components' GameObjects in the search, as well AddSearchedObjectToFilteredSets( ( (Component) obj ).gameObject, false ); } } // Search a scene for references private void SearchScene( string scenePath, List searchResult, Parameters searchParameters, SceneSetup[] initialSceneSetup ) { Scene scene = EditorSceneManager.GetSceneByPath( scenePath ); if( isInPlayMode && !scene.isLoaded ) return; bool canContainSceneObjectReference = scene.isLoaded && ( !EditorSceneManager.preventCrossSceneReferences || sceneObjectsToSearchScenesSet.Contains( scenePath ) ); if( !canContainSceneObjectReference ) { bool canContainAssetReference = assetsToSearchSet.Count > 0 && ( isInPlayMode || AssetHasAnyReference( scenePath ) ); if( !canContainAssetReference ) return; } if( !scene.isLoaded ) { if( searchParameters.lazySceneSearch ) { searchResult.Add( new SearchResultGroup( scenePath, SearchResultGroup.GroupType.Scene, true, true ) ); return; } scene = EditorSceneManager.OpenScene( scenePath, OpenSceneMode.Additive ); } currentSearchResultGroup = new SearchResultGroup( scenePath, SearchResultGroup.GroupType.Scene ); // Search through all the GameObjects in the scene GameObject[] rootGameObjects = scene.GetRootGameObjects(); for( int i = 0; i < rootGameObjects.Length; i++ ) SearchGameObjectRecursively( rootGameObjects[i] ); // Search through Lighting Settings (it requires changing the active scene but don't do that in play mode) if( searchParameters.searchInSceneLightingSettings && ( !isInPlayMode || SceneManager.GetActiveScene() == scene ) ) { if( !isInPlayMode && EditorSceneManager.GetActiveScene() != scene ) EditorSceneManager.SetActiveScene( scene ); BeginSearchObject( lightmapSettingsGetter() ); BeginSearchObject( renderSettingsGetter() ); } // If no references are found in the scene and if the scene is not part of the initial scene setup, close it if( currentSearchResultGroup.NumberOfReferences == 0 ) { if( !isInPlayMode ) { bool sceneIsOneOfInitials = false; for( int i = 0; i < initialSceneSetup.Length; i++ ) { if( initialSceneSetup[i].path == scenePath ) { if( !initialSceneSetup[i].isLoaded ) EditorSceneManager.CloseScene( scene, false ); sceneIsOneOfInitials = true; break; } } if( !sceneIsOneOfInitials ) EditorSceneManager.CloseScene( scene, true ); } } else { // Some references are found in this scene, save the results searchResult.Add( currentSearchResultGroup ); } } // Search a GameObject and its children for references recursively private void SearchGameObjectRecursively( GameObject go ) { BeginSearchObject( go ); Transform tr = go.transform; for( int i = 0; i < tr.childCount; i++ ) SearchGameObjectRecursively( tr.GetChild( i ).gameObject ); } // Begin searching a root object (like a GameObject or an asset) private void BeginSearchObject( Object obj ) { if( obj is SceneAsset ) return; currentSearchedObject = obj; ReferenceNode searchResult = SearchObject( obj ); if( searchResult != null ) currentSearchResultGroup.AddReference( searchResult ); } // Search an object for references private ReferenceNode SearchObject( object obj ) { if( obj == null || obj.Equals( null ) ) return null; // Avoid recursion (which leads to stackoverflow exception) using a stack (initially, I was using callStack.ContainsFast // here but it returned false for objects that do exist in the call stack if VFX Graph window was open) for( int i = callStack.Count - 1; i >= 0; i-- ) { if( callStack[i].Equals( obj ) ) return null; } bool searchingSourceAsset = searchingSourceAssets && ReferenceEquals( currentSearchedObject, obj ); // Hashing does not work well with structs all the time, don't cache search results for structs if( !( obj is ValueType ) && !searchingSourceAsset ) { // If object was searched before, return the cached result ReferenceNode cachedResult; if( TryGetReferenceNode( obj, out cachedResult ) ) return cachedResult; } searchedObjectsCount++; ReferenceNode result; Object unityObject = obj as Object; if( unityObject != null ) { // If the Object is an asset, search it in detail only if its dependencies contain at least one of the searched asset(s) string assetPath = null; if( unityObject.IsAsset() ) { if( assetsToSearchSet.Count == 0 ) { searchedUnityObjects.Add( unityObject.GetInstanceID(), null ); return null; } assetPath = AssetDatabase.GetAssetPath( unityObject ); if( excludedAssetsPathsSet.Contains( assetPath ) || !AssetHasAnyReference( assetPath ) ) { searchedUnityObjects.Add( unityObject.GetInstanceID(), null ); return null; } } callStack.Add( unityObject ); // Search the Object in detail Func searchFunction; if( assetPath != null && extensionToSearchFunction.TryGetValue( Utilities.GetFileExtension( assetPath ), out searchFunction ) && AssetDatabase.IsMainAsset( unityObject ) ) result = searchFunction( unityObject ); else if( typeToSearchFunction.TryGetValue( unityObject.GetType(), out searchFunction ) ) result = searchFunction( unityObject ); else if( unityObject is Component ) result = SearchComponent( unityObject ); else { result = PopReferenceNode( unityObject ); SearchVariablesWithSerializedObject( result ); } // A prefab asset should have a link to its children because when a scene object uses a prefab and a child of that prefab uses // a searched object, the scene object needs to appear in the search results. Since prefab assets aren't automatically linked to // their children, we need to create that link manually if( assetPath != null && unityObject is GameObject && AssetDatabase.IsMainAsset( unityObject ) ) { if( result == null ) result = PopReferenceNode( unityObject ); GameObject prefabGameObject = (GameObject) unityObject; Transform[] prefabChildren = prefabGameObject.GetComponentsInChildren( true ); for( int i = 0; i < prefabChildren.Length; i++ ) { if( prefabChildren[i].gameObject != prefabGameObject ) result.AddLinkTo( SearchObject( prefabChildren[i].gameObject ), isWeakLink: true ); } } callStack.RemoveAt( callStack.Count - 1 ); } else { // Comply with the recursive search limit if( currentDepth >= searchParameters.searchDepthLimit ) return null; callStack.Add( obj ); currentDepth++; result = PopReferenceNode( obj ); SearchVariablesWithReflection( result ); currentDepth--; callStack.RemoveAt( callStack.Count - 1 ); } if( result != null && result.NumberOfOutgoingLinks == 0 ) { PoolReferenceNode( result ); result = null; } // Cache the search result if we are skimming through a class (not a struct; i.e. objHash != null) // and if the object is a UnityEngine.Object (if not, cache the result only if we have actually found something // or we are at the root of the search; i.e. currentDepth == 0) if( !( obj is ValueType ) && ( result != null || unityObject != null || currentDepth == 0 ) ) { if( !searchingSourceAsset ) { if( obj is Object ) searchedUnityObjects.Add( unityObject.GetInstanceID(), result ); else searchedObjects.Add( GetNodeObjectHash( obj ), result ); } else if( result != null ) { result.CopyReferencesTo( searchedUnityObjects[unityObject.GetInstanceID()] ); PoolReferenceNode( result ); } } return result; } // Check if the asset at specified path depends on any of the references private bool AssetHasAnyReference( string assetPath ) { #if ASSET_USAGE_ADDRESSABLES if( searchParameters.addressablesSupport ) return true; #endif if( assetsToSearchPathsSet.Contains( assetPath ) ) return true; if( alwaysSearchedExtensionsSet.Count > 0 && alwaysSearchedExtensionsSet.Contains( Utilities.GetFileExtension( assetPath ) ) ) return true; return AssetHasAnyReferenceInternal( assetPath ); } // Recursively check if the asset at specified path depends on any of the references private bool AssetHasAnyReferenceInternal( string assetPath ) { CacheEntry cacheEntry; if( !assetDependencyCache.TryGetValue( assetPath, out cacheEntry ) ) { cacheEntry = new CacheEntry( assetPath ); assetDependencyCache[assetPath] = cacheEntry; } else if( !cacheEntry.verified ) cacheEntry.Verify( assetPath ); if( cacheEntry.searchResult != CacheEntry.Result.Unknown ) return cacheEntry.searchResult == CacheEntry.Result.Yes; cacheEntry.searchResult = CacheEntry.Result.No; string[] dependencies = cacheEntry.dependencies; long[] fileSizes = cacheEntry.fileSizes; for( int i = 0; i < dependencies.Length; i++ ) { // If a dependency was renamed (which doesn't affect the verified hash, unfortunately), // force refresh the asset's dependencies and search it again if( !Directory.Exists( dependencies[i] ) ) // Calling FileInfo.Length on a directory throws FileNotFoundException { FileInfo assetFile = new FileInfo( dependencies[i] ); if( !assetFile.Exists || assetFile.Length != fileSizes[i] ) { // Although not reproduced, it is reported that this section caused StackOverflowException due to infinite loop, // if that happens, log useful information to help reproduce the issue if( lastRefreshedCacheEntry == cacheEntry ) { StringBuilder sb = Utilities.stringBuilder; sb.Length = 0; sb.EnsureCapacity( 1000 ); sb.AppendLine( "Infinite loop while refreshing a cache entry, please report it to the author." ).AppendLine(); sb.Append( "Asset path: " ).AppendLine( assetPath ); for( int j = 0; j < 2; j++ ) { if( j == 1 ) { cacheEntry.Refresh( assetPath ); dependencies = cacheEntry.dependencies; fileSizes = cacheEntry.fileSizes; } sb.AppendLine().AppendLine( j == 0 ? "Old Dependencies:" : "New Dependencies" ); for( int k = 0; k < dependencies.Length; k++ ) { sb.Append( "- " ).Append( dependencies[k] ); if( Directory.Exists( dependencies[k] ) ) { sb.Append( " (Dir)" ); if( fileSizes[k] != 0L ) sb.Append( " WasCachedAsFile: " ).Append( fileSizes[k] ); } else { assetFile = new FileInfo( dependencies[k] ); sb.Append( " (File) " ).Append( "CachedSize: " ).Append( fileSizes[k] ); if( assetFile.Exists ) sb.Append( " RealSize: " ).Append( assetFile.Length ); else sb.Append( " NoLongerExists" ); } sb.AppendLine(); } } Debug.LogError( sb.ToString() ); return false; } cacheEntry.Refresh( assetPath ); cacheEntry.searchResult = CacheEntry.Result.Unknown; lastRefreshedCacheEntry = cacheEntry; return AssetHasAnyReferenceInternal( assetPath ); } } if( assetsToSearchPathsSet.Contains( dependencies[i] ) ) { cacheEntry.searchResult = CacheEntry.Result.Yes; return true; } } for( int i = 0; i < dependencies.Length; i++ ) { if( AssetHasAnyReferenceInternal( dependencies[i] ) ) { cacheEntry.searchResult = CacheEntry.Result.Yes; return true; } } return false; } // If object was already searched, return its ReferenceNode private bool TryGetReferenceNode( object nodeObject, out ReferenceNode referenceNode ) { if( nodeObject is Object ) { if( searchedUnityObjects.TryGetValue( ( (Object) nodeObject ).GetInstanceID(), out referenceNode ) ) return true; } else if( searchedObjects.TryGetValue( GetNodeObjectHash( nodeObject ), out referenceNode ) ) return true; referenceNode = null; return false; } // Get reference node for object private ReferenceNode GetReferenceNode( object nodeObject ) { ReferenceNode result; if( nodeObject is Object ) { int hash = ( (Object) nodeObject ).GetInstanceID(); if( !searchedUnityObjects.TryGetValue( hash, out result ) || result == null ) { result = PopReferenceNode( nodeObject ); searchedUnityObjects[hash] = result; } } else { string hash = GetNodeObjectHash( nodeObject ); if( !searchedObjects.TryGetValue( hash, out result ) || result == null ) { result = PopReferenceNode( nodeObject ); searchedObjects[hash] = result; } } return result; } // Fetch a reference node from pool private ReferenceNode PopReferenceNode( object nodeObject ) { ReferenceNode node; if( nodesPool.Count == 0 ) node = new ReferenceNode(); else { int index = nodesPool.Count - 1; node = nodesPool[index]; nodesPool.RemoveAt( index ); } node.nodeObject = nodeObject; return node; } // Pool a reference node private void PoolReferenceNode( ReferenceNode node ) { node.Clear(); nodesPool.Add( node ); } // Get a unique-ish string hash code for a plain C# object (i.e. non-UnityEngine.Object object) private string GetNodeObjectHash( object nodeObject ) { return nodeObject.GetHashCode() + nodeObject.GetType().Name; } // Retrieve the game objects listed under the DontDestroyOnLoad scene private GameObject[] GetDontDestroyOnLoadObjects() { GameObject temp = null; try { temp = new GameObject(); Object.DontDestroyOnLoad( temp ); Scene dontDestroyOnLoad = temp.scene; Object.DestroyImmediate( temp ); temp = null; return dontDestroyOnLoad.GetRootGameObjects(); } finally { if( temp != null ) Object.DestroyImmediate( temp ); } } // Appends contents of callStack to StringBuilder and returns the most recent Unity object in callStack private Object AppendCallStackToStringBuilder( StringBuilder sb ) { Object latestUnityObjectInCallStack = null; if( callStack.Count > 0 ) { sb.AppendLine().AppendLine( "Stack contents: " ); for( int i = callStack.Count - 1; i >= 0; i-- ) { latestUnityObjectInCallStack = callStack[i] as Object; if( latestUnityObjectInCallStack ) { if( !AssetDatabase.Contains( latestUnityObjectInCallStack ) ) { string scenePath = AssetDatabase.GetAssetOrScenePath( latestUnityObjectInCallStack ); if( !string.IsNullOrEmpty( scenePath ) && SceneManager.GetSceneByPath( scenePath ).IsValid() ) sb.Append( "Scene: " ).AppendLine( scenePath ); } break; } } for( int i = callStack.Count - 1; i >= 0; i-- ) { sb.Append( i ).Append( ": " ); Object unityObject = callStack[i] as Object; if( unityObject ) sb.Append( unityObject.name ).Append( " (" ).Append( unityObject.GetType() ).AppendLine( ")" ); else if( callStack[i] != null ) sb.Append( callStack[i].GetType() ).AppendLine( " object" ); else sb.AppendLine( "<>" ); } sb.AppendLine(); } return latestUnityObjectInCallStack; } } }