Skip to content

Instantly share code, notes, and snippets.

@RevenantX
Last active June 5, 2024 08:14
Show Gist options
  • Save RevenantX/41193128d413fc7f7ba9e7e06bfd43ca to your computer and use it in GitHub Desktop.
Save RevenantX/41193128d413fc7f7ba9e7e06bfd43ca to your computer and use it in GitHub Desktop.
Find asset usages in prefabs and scenes (Unity3d)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace Code.Editor
{
public class FindUsages : EditorWindow
{
private struct SearchResult
{
public string Name;
public string Path;
}
private class DatabaseRefreshListener : AssetPostprocessor
{
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths)
{
NeedCacheRefresh = true;
}
}
private static readonly List<SearchResult> PrefabNames = new List<SearchResult>();
private static readonly List<SearchResult> SceneNames = new List<SearchResult>();
private static readonly List<string> FilesWithAsset = new List<string>();
private const string MenuItemName = "Assets/- Find usages";
private const string YAMLHeader = "%YAML";
private static readonly string[] SearchTypes = { ".prefab", ".asset", ".unity" };
private static readonly Encoding AsciiEncoding = Encoding.ASCII;
private static bool NeedCacheRefresh = true;
private static string[] FilesCache;
private static long ProcessedFiles;
private static int ThreadsActive;
private static byte[] TargetAsciiGuid;
private static byte[] TargetHexGuid;
private Vector2 _scrollPosition;
private GUIStyle _buttonStyle;
[MenuItem(MenuItemName, true, 10)]
private static bool MenuItemCheck()
{
if (Selection.activeObject == null)
return false;
//skip directories
return !Directory.Exists(AssetDatabase.GetAssetPath(Selection.activeObject));
}
private static void SearchThread(object _)
{
byte[] binaryData = new byte[1024];
while (true)
{
long i = Interlocked.Increment(ref ProcessedFiles) - 1;
if (i >= FilesCache.Length)
break;
using (var fileStream = new FileStream(FilesCache[i], FileMode.Open))
{
int fileLength = (int)fileStream.Length;
if (binaryData.Length < fileLength)
binaryData = new byte[fileLength];
int readCount = fileStream.Read(binaryData, 0, fileLength);
if (readCount < YAMLHeader.Length)
continue;
var byteSequence = AsciiEncoding.GetString(binaryData, 0, YAMLHeader.Length) == YAMLHeader
? TargetAsciiGuid
: TargetHexGuid;
for (int j = YAMLHeader.Length; j < readCount - byteSequence.Length; j++)
{
int k;
for (k = 0; k < byteSequence.Length; k++)
{
if (binaryData[j + k] != byteSequence[k])
break;
}
if (k == byteSequence.Length)
{
lock (FilesWithAsset)
FilesWithAsset.Add(FilesCache[i]);
break;
}
}
}
}
Interlocked.Decrement(ref ThreadsActive);
}
[MenuItem(MenuItemName, false, 30)]
static void MenuItem()
{
string guid = Selection.assetGUIDs[0];
TargetAsciiGuid = AsciiEncoding.GetBytes($"guid: {guid}");
TargetHexGuid = new byte[guid.Length / 2];
for (int i = 0; i < guid.Length; i += 2)
TargetHexGuid[i/2] = byte.Parse(new string(new [] { guid[i+1], guid[i] }), NumberStyles.HexNumber);
if (NeedCacheRefresh || FilesCache == null)
{
var filesList = new List<string>();
foreach (string file in Directory.EnumerateFiles("Assets", "*.*", SearchOption.AllDirectories))
{
for (int i = 0; i < SearchTypes.Length; i++)
{
if (file.EndsWith(SearchTypes[i]))
{
filesList.Add(file);
break;
}
}
}
FilesCache = filesList.ToArray();
}
ProcessedFiles = 0;
FilesWithAsset.Clear();
PrefabNames.Clear();
SceneNames.Clear();
int processorCount = Environment.ProcessorCount;
ThreadsActive = processorCount;
Debug.Log($"FindUsages. GUID: {guid} Cores: {processorCount}, Files: {FilesCache.Length}");
for (int i = 0; i < processorCount; i++)
ThreadPool.QueueUserWorkItem(SearchThread);
var sw = new Stopwatch();
sw.Start();
while (ThreadsActive > 0)
{
EditorUtility.DisplayProgressBar("Find usages", $"File: {Selection.activeObject.name}", (float)ProcessedFiles / FilesCache.Length);
Thread.Sleep(1);
}
EditorUtility.ClearProgressBar();
sw.Stop();
Debug.Log($"Search time: {sw.ElapsedMilliseconds} ms");
for (int i = 0; i < FilesWithAsset.Count; i++)
{
var prefabPath = FilesWithAsset[i];
var strWihtouPath = prefabPath.Substring(prefabPath.LastIndexOf(Path.DirectorySeparatorChar) + 1);
var targetList = prefabPath.EndsWith(".unity") ? SceneNames : PrefabNames;
targetList.Add(new SearchResult
{
Name = strWihtouPath.Substring(0, strWihtouPath.IndexOf(".", StringComparison.InvariantCulture)) + " ",
Path = prefabPath
});
}
if (PrefabNames.Count > 0 || SceneNames.Count > 0)
GetWindow<FindUsages>().Show();
else
EditorUtility.DisplayDialog("Find usages", "Nothing found", "OK");
}
private void OnGUI()
{
_buttonStyle ??= new(GUI.skin.button) { alignment = TextAnchor.MiddleLeft };
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
GUILayout.Label("Prefabs");
DrawSearchResults(PrefabNames);
GUILayout.Label("Scenes");
DrawSearchResults(SceneNames);
EditorGUILayout.EndScrollView();
return;
void DrawSearchResults(List<SearchResult> searchResults)
{
for (int i = 0; i < searchResults.Count; i++)
{
if (GUILayout.Button($"{searchResults[i].Name} ({searchResults[i].Path})", _buttonStyle, GUILayout.Height(20), GUILayout.ExpandWidth(true)))
{
Selection.activeObject = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(searchResults[i].Path);
EditorGUIUtility.PingObject(Selection.activeObject);
}
}
}
}
}
}
@muellerat91interactive
Copy link

Very cool script!
Little addition: you could add EditorGUIUtility.PingObject(Selection.activeObject); after line 171 for even more convenience :)

@RevenantX
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment