diff --git a/Editor/.plan b/Editor/.plan
new file mode 100644
index 0000000..fe80a17
--- /dev/null
+++ b/Editor/.plan
@@ -0,0 +1,9 @@
+Refactor "Layer Users" to "Layer Usages"
+Upgrade Prefab Layer Users to display GameObjects the same way Scene Layer Users does
+Update scene scanning to be async in MissingReferences and Layer Usages
+Refactor scanning options to be display options so we don't require a re-scan
+Rename MissingReferenceWindow base class to AnalysisWindow and refactor to share code Prefab/Tag usages and Solid Color Textures
+ Split out Folder, GameObjectContainer, etc. types into their own files, share among all analysis windows
+Create options to allow Layer Usages windows to skip scanning for layer masks (SerializedProperty access is the bottleneck)
+Allow filtering Layer Usages with multiple layers
+Use rich text for GameObject labels (bold tag or layer, etc.)
\ No newline at end of file
diff --git a/Editor/GlobalNamespaceWatcher.cs b/Editor/GlobalNamespaceWatcher.cs
index 37526a1..b61b153 100644
--- a/Editor/GlobalNamespaceWatcher.cs
+++ b/Editor/GlobalNamespaceWatcher.cs
@@ -21,6 +21,11 @@ public class GlobalNamespaceWatcher : EditorWindow
///
class AssemblyRow
{
+ static readonly GUIContent k_OpenGUIContent = new GUIContent("Open", "Open this script in the default script editor.");
+ static readonly GUIContent k_OpenAllGUIContent = new GUIContent("Open All", "Open all scripts in this assembly's " +
+ "global namespace in the default script editor.\nWARNING: This process can lock the Editor for a long time and cannot be canceled.");
+ static readonly GUILayoutOption k_OpenButtonWidth = GUILayout.Width(100);
+
///
/// The path to the assembly, sourced from Assembly.Location.
///
@@ -46,6 +51,11 @@ class AssemblyRow
///
public int MonoScriptTypeCount => m_MonoScriptCount;
+ ///
+ /// The types in this assembly.
+ ///
+ public SortedList Types => m_Types;
+
///
/// Draw this assembly row to the GUI.
///
@@ -54,7 +64,25 @@ class AssemblyRow
public void Draw(string assemblyName, bool showOnlyMonoScriptTypes = false)
{
var count = showOnlyMonoScriptTypes ? m_MonoScriptCount : m_Types.Count;
- m_Expanded = EditorGUILayout.Foldout(m_Expanded, $"{assemblyName}: ({count})", true);
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, $"{assemblyName}: ({count})", true);
+ using (new EditorGUI.DisabledScope(m_MonoScriptCount == 0))
+ {
+ if (GUILayout.Button(k_OpenAllGUIContent, k_OpenButtonWidth))
+ {
+ foreach (var kvp in m_Types)
+ {
+ var monoScript = kvp.Value;
+ if (monoScript == null)
+ continue;
+
+ AssetDatabase.OpenAsset(monoScript);
+ }
+ }
+ }
+ }
+
if (m_Expanded)
{
using (new EditorGUI.IndentLevelScope())
@@ -64,18 +92,33 @@ public void Draw(string assemblyName, bool showOnlyMonoScriptTypes = false)
{
foreach (var kvp in m_Types)
{
+ var label = kvp.Key;
var monoScript = kvp.Value;
- if (showOnlyMonoScriptTypes && monoScript == null)
- continue;
-
- EditorGUILayout.LabelField(kvp.Key);
- EditorGUILayout.ObjectField(monoScript, typeof(MonoScript), false);
+ DrawScript(showOnlyMonoScriptTypes, monoScript, label);
}
}
}
}
}
+ static void DrawScript(bool showOnlyMonoScriptTypes, MonoScript monoScript, string label)
+ {
+ if (showOnlyMonoScriptTypes && monoScript == null)
+ return;
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ EditorGUILayout.LabelField(label);
+ using (new EditorGUI.DisabledScope(monoScript == null))
+ {
+ if (GUILayout.Button(k_OpenGUIContent, k_OpenButtonWidth))
+ AssetDatabase.OpenAsset(monoScript);
+ }
+ }
+
+ EditorGUILayout.ObjectField(monoScript, typeof(MonoScript), false);
+ }
+
///
/// Add a type to this assembly row.
/// The type will be stored in a dictionary, and if there is a MonoScript, we increment a counter to show if only MonoScript types will be shown.
@@ -84,10 +127,16 @@ public void Draw(string assemblyName, bool showOnlyMonoScriptTypes = false)
/// An associated MonoScript, if one exists.
public void AddType(string typeName, MonoScript monoScript)
{
+ var label = typeName;
if (monoScript != null)
+ {
m_MonoScriptCount++;
+ var path = AssetDatabase.GetAssetPath(monoScript);
+ if (!string.IsNullOrEmpty(path))
+ label = path;
+ }
- m_Types.Add(typeName, monoScript);
+ m_Types.Add(label, monoScript);
}
}
@@ -99,7 +148,11 @@ public void AddType(string typeName, MonoScript monoScript)
const int k_LabelWidth = 200;
const string k_CongratulationsLabel = "Congratulations! There are no types in the global namespace. :)";
+ static readonly GUIContent k_OpenEverythingGUIContent = new GUIContent("Open Everything", "Open all scripts in the " +
+ "global namespace in the default script editor.\nWARNING: This process can lock the Editor for a long time and cannot be canceled.");
+
static SortedList s_Assemblies;
+ static int s_TotalMonoScriptCount;
[SerializeField]
Vector2 m_ScrollPosition;
@@ -120,6 +173,9 @@ void OnEnable()
{
if (s_Assemblies == null)
{
+ // Reset total count, just in case it's gone out of sync with s_Assemblies.
+ s_TotalMonoScriptCount = 0;
+
// Prepare a map of MonoScript types for fast access.
var monoScripts = MonoImporter.GetAllRuntimeMonoScripts();
var monoScriptDictionary = new Dictionary(monoScripts.Length);
@@ -190,7 +246,10 @@ void OnEnable()
}
if (addedType)
+ {
s_Assemblies.Add(assemblyName, row);
+ s_TotalMonoScriptCount += row.MonoScriptTypeCount;
+ }
}
catch
{
@@ -209,6 +268,24 @@ void OnGUI()
m_ShowOnlyProjectAssemblies = EditorGUILayout.Toggle(k_ShowOnlyProjectAssembliesLabel, m_ShowOnlyProjectAssemblies);
m_ShowOnlyMonoScriptTypes = EditorGUILayout.Toggle(k_ShowOnlyMonoScriptTypesLabel, m_ShowOnlyMonoScriptTypes);
+ using (new EditorGUI.DisabledScope(s_TotalMonoScriptCount == 0))
+ {
+ if (GUILayout.Button(k_OpenEverythingGUIContent))
+ {
+ foreach (var kvp in s_Assemblies)
+ {
+ foreach (var kvp2 in kvp.Value.Types)
+ {
+ var monoScript = kvp2.Value;
+ if (monoScript != null)
+ continue;
+
+ AssetDatabase.OpenAsset(monoScript);
+ }
+ }
+ }
+ }
+
// Give users convenient buttons to expand/collapse the assembly rows
using (new EditorGUILayout.HorizontalScope())
{
diff --git a/Editor/LayerUsers.meta b/Editor/LayerUsers.meta
new file mode 100644
index 0000000..b911809
--- /dev/null
+++ b/Editor/LayerUsers.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 059244c3e45aac5438c1428abd575aaa
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/LayerUsers/PrefabLayerUsers.cs b/Editor/LayerUsers/PrefabLayerUsers.cs
new file mode 100644
index 0000000..07a3ed9
--- /dev/null
+++ b/Editor/LayerUsers/PrefabLayerUsers.cs
@@ -0,0 +1,868 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using UnityEditor;
+using UnityEditor.Experimental.SceneManagement;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+
+namespace Unity.Labs.SuperScience
+{
+ ///
+ /// Scans the project for prefabs which use non-default layers.
+ /// Use this utility to track down where a certain layer is used.
+ ///
+ public class PrefabLayerUsers : EditorWindow
+ {
+ ///
+ /// Tree structure for folder scan results.
+ /// This is the root object for the project scan, and represents the results in a hierarchy that matches the
+ /// project's folder structure for an easy to read presentation of layer users.
+ /// When the Scan method encounters a layer user, we search the parent folder for one of these using the asset path to file it where it belongs.
+ ///
+ class Folder
+ {
+ // TODO: Share code between this window and others that display a folder structure
+ const int k_IndentAmount = 15;
+ const int k_SeparatorLineHeight = 1;
+
+ readonly SortedDictionary m_Subfolders = new SortedDictionary();
+ readonly List m_Prefabs = new List();
+ readonly SortedDictionary m_TotalCountPerLayer = new SortedDictionary();
+ readonly SortedDictionary m_TotalWithoutLayerMasksPerLayer = new SortedDictionary();
+ int m_TotalCount;
+ int m_TotalWithoutLayerMasks;
+ bool m_Expanded;
+
+ ///
+ /// Clear the contents of this container.
+ ///
+ public void Clear()
+ {
+ m_Subfolders.Clear();
+ m_Prefabs.Clear();
+ m_TotalCountPerLayer.Clear();
+ m_TotalWithoutLayerMasksPerLayer.Clear();
+ m_TotalCount = 0;
+ m_TotalWithoutLayerMasks = 0;
+ }
+
+ ///
+ /// Add a prefab to this folder at a given path.
+ ///
+ public void AddPrefab(PrefabRow prefabRow)
+ {
+ var folder = GetOrCreateFolderForAssetPath(prefabRow);
+ folder.m_Prefabs.Add(prefabRow);
+ }
+
+ ///
+ /// Get the Folder object which corresponds to the path of a given .
+ /// If this is the first asset encountered for a given folder, create a chain of folder objects
+ /// rooted with this one and return the folder at the end of that chain.
+ /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one more layer user.
+ ///
+ /// A struct containing the prefab asset reference and its metadata
+ /// The folder object corresponding to the folder containing the layer users at the given path.
+ Folder GetOrCreateFolderForAssetPath(PrefabRow prefabRow)
+ {
+ var directories = prefabRow.Path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ var folder = this;
+ var gameObjectLayers = prefabRow.GameObjectLayers;
+ var layerMaskLayers = prefabRow.LayerMaskLayers;
+ folder.AggregateCount(gameObjectLayers, layerMaskLayers);
+ var length = directories.Length - 1;
+ for (var i = 0; i < length; i++)
+ {
+ var directory = directories[i];
+ var subfolders = folder.m_Subfolders;
+ if (!subfolders.TryGetValue(directory, out var subfolder))
+ {
+ subfolder = new Folder();
+ subfolders[directory] = subfolder;
+ }
+
+ folder = subfolder;
+ folder.AggregateCount(gameObjectLayers, layerMaskLayers);
+ }
+
+ return folder;
+ }
+
+ ///
+ /// Draw GUI for this Folder.
+ ///
+ /// The name of the folder.
+ /// Layer to Name dictionary for fast lookup of layer names.
+ /// (Optional) Layer used to filter results.
+ /// (Optional) Whether to include layers from LayerMask fields in the results.
+ public void Draw(string name, Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ var wasExpanded = m_Expanded;
+ var layerNameList = GetLayerNameList(m_TotalCountPerLayer.Keys, m_TotalWithoutLayerMasksPerLayer.Keys, layerToName, layerFilter, includeLayerMaskFields);
+ var label = $"{name}: {GetCount(layerFilter, includeLayerMaskFields)} {{{layerNameList}}}";
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+
+ DrawLineSeparator();
+
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ SetExpandedRecursively(m_Expanded);
+
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var kvp in m_Subfolders)
+ {
+ var folder = kvp.Value;
+ if (folder.GetCount(layerFilter, includeLayerMaskFields) == 0)
+ continue;
+
+ folder.Draw(kvp.Key, layerToName, layerFilter, includeLayerMaskFields);
+ }
+
+ var showedPrefab = false;
+ foreach (var prefabRow in m_Prefabs)
+ {
+ var gameObjectLayers = prefabRow.GameObjectLayers;
+ var layerMaskLayers = prefabRow.LayerMaskLayers;
+ if (layerFilter == k_InvalidLayer || gameObjectLayers.Contains(layerFilter) || layerMaskLayers.Contains(layerFilter))
+ {
+ prefabRow.Draw(layerToName, layerFilter, includeLayerMaskFields);
+ showedPrefab = true;
+ }
+ }
+
+ if (showedPrefab)
+ DrawLineSeparator();
+ }
+ }
+
+ ///
+ /// Draw a separator line.
+ ///
+ static void DrawLineSeparator()
+ {
+ EditorGUILayout.Separator();
+ using (new GUILayout.HorizontalScope())
+ {
+ GUILayout.Space(EditorGUI.indentLevel * k_IndentAmount);
+ GUILayout.Box(GUIContent.none, Styles.LineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true));
+ }
+
+ EditorGUILayout.Separator();
+ }
+
+ ///
+ /// Set the expanded state of this folder, its contents and their children and all of its subfolders and their contents and children.
+ ///
+ /// Whether this object should be expanded in the GUI.
+ void SetExpandedRecursively(bool expanded)
+ {
+ m_Expanded = expanded;
+ foreach (var kvp in m_Subfolders)
+ {
+ kvp.Value.SetExpandedRecursively(expanded);
+ }
+ }
+
+ ///
+ /// Sort the contents of this folder and all subfolders by name.
+ ///
+ public void SortContentsRecursively()
+ {
+ m_Prefabs.Sort((a, b) => a.PrefabAsset.name.CompareTo(b.PrefabAsset.name));
+ foreach (var kvp in m_Subfolders)
+ {
+ kvp.Value.SortContentsRecursively();
+ }
+ }
+
+ public int GetCount(int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ var unfiltered = layerFilter == k_InvalidLayer;
+ if (unfiltered && includeLayerMaskFields)
+ return m_TotalCount;
+
+ if (unfiltered)
+ return m_TotalWithoutLayerMasks;
+
+ if (includeLayerMaskFields)
+ {
+ m_TotalCountPerLayer.TryGetValue(layerFilter, out var totalCount);
+ return totalCount;
+ }
+
+ m_TotalWithoutLayerMasksPerLayer.TryGetValue(layerFilter, out var totalWithoutLayerMasks);
+ return totalWithoutLayerMasks;
+ }
+
+ void AggregateCount(SortedSet gameObjectLayers, SortedSet layerMaskLayers)
+ {
+ var hasGameObjectLayers = gameObjectLayers.Count > 0;
+ if (hasGameObjectLayers || layerMaskLayers.Count > 0)
+ m_TotalCount++;
+
+ if (hasGameObjectLayers)
+ m_TotalWithoutLayerMasks++;
+
+ k_LayerUnionHashSet.Clear();
+ k_LayerUnionHashSet.UnionWith(gameObjectLayers);
+ k_LayerUnionHashSet.UnionWith(layerMaskLayers);
+ foreach (var layer in k_LayerUnionHashSet)
+ {
+ m_TotalCountPerLayer.TryGetValue(layer, out var count);
+ count++;
+ m_TotalCountPerLayer[layer] = count;
+ }
+
+ foreach (var layer in gameObjectLayers)
+ {
+ m_TotalWithoutLayerMasksPerLayer.TryGetValue(layer, out var count);
+ count++;
+ m_TotalWithoutLayerMasksPerLayer[layer] = count;
+ }
+ }
+ }
+
+ class PrefabRow
+ {
+ public string Path;
+ public GameObject PrefabAsset;
+ public SortedSet GameObjectLayers;
+ public SortedSet LayerMaskLayers;
+ public List LayerUsers;
+ public int LayerUsersWithoutLayerMasks;
+
+ bool m_Expanded;
+
+ public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ var layerNameList = GetLayerNameList(GameObjectLayers, LayerMaskLayers, layerToName, layerFilter, includeLayerMaskFields);
+ var label = $"{PrefabAsset.name} {{{layerNameList}}}";
+ if (includeLayerMaskFields && LayerUsers.Count > 0 || LayerUsersWithoutLayerMasks > 0)
+ {
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+ EditorGUILayout.ObjectField(PrefabAsset, typeof(GameObject), false);
+ }
+ else
+ {
+ EditorGUILayout.ObjectField(label, PrefabAsset, typeof(GameObject), false);
+ }
+
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var layerUser in LayerUsers)
+ {
+ if (layerFilter != k_InvalidLayer)
+ {
+ var gameObjectLayerMatchesFilter = layerUser.GameObject.layer == layerFilter;
+ var layerMasksMatchFilter = includeLayerMaskFields && layerUser.LayerMaskLayers.Contains(layerFilter);
+ if (!(gameObjectLayerMatchesFilter || layerMasksMatchFilter))
+ continue;
+ }
+
+ layerUser.Draw(layerToName, layerFilter, includeLayerMaskFields);
+ }
+ }
+ }
+ }
+
+ class GameObjectRow
+ {
+ public string TransformPath;
+ public GameObject GameObject;
+ public List LayerMaskComponents;
+ public SortedSet LayerMaskLayers;
+
+ bool m_Expanded;
+
+ public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ var layer = GameObject.layer;
+ var layerName = GetLayerNameString(layerToName, layer);
+
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append($"{TransformPath} - Layer: {layerName}");
+ if (includeLayerMaskFields && LayerMaskLayers.Count > 0)
+ {
+ var layerNameList = GetLayerNameList(LayerMaskLayers, layerToName, layerFilter);
+ k_StringBuilder.Append($" LayerMasks:{{{layerNameList}}}");
+ }
+
+ var label = k_StringBuilder.ToString();
+
+ if (includeLayerMaskFields && GetComponentCount(layerFilter) > 0)
+ {
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+ EditorGUILayout.ObjectField(GameObject, typeof(GameObject), true);
+ }
+ else
+ {
+ EditorGUILayout.ObjectField(label, GameObject, typeof(GameObject), true);
+ }
+
+ if (!m_Expanded || !includeLayerMaskFields)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var component in LayerMaskComponents)
+ {
+ if (layerFilter != k_InvalidLayer && !component.UsedLayers.Contains(layerFilter))
+ continue;
+
+ component.Draw(layerToName, layerFilter);
+ }
+ }
+ }
+
+ int GetComponentCount(int layerFilter = k_InvalidLayer)
+ {
+ if (layerFilter == k_InvalidLayer)
+ return LayerMaskComponents.Count;
+
+ var count = 0;
+ foreach (var component in LayerMaskComponents)
+ {
+ if (component.UsedLayers.Contains(layerFilter))
+ count++;
+ }
+
+ return count;
+ }
+ }
+
+ struct ComponentRow
+ {
+ public Component Component;
+ public SortedSet UsedLayers;
+
+ public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer)
+ {
+ using (new GUILayout.HorizontalScope())
+ {
+ var layerNameList = GetLayerNameList(UsedLayers, layerToName, layerFilter);
+ EditorGUILayout.ObjectField($"{Component.name} ({Component.GetType().Name}) {{{layerNameList}}}", Component, typeof(Component), true);
+ }
+ }
+ }
+
+ class FilterRow
+ {
+ public readonly HashSet AllUsers = new HashSet();
+ public readonly HashSet UsersWithoutLayerMasks = new HashSet();
+ }
+
+ static class Styles
+ {
+ internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft,
+ fontStyle = FontStyle.Bold
+ };
+
+ internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft
+ };
+
+ internal static readonly GUIStyle LineStyle = new GUIStyle
+ {
+ normal = new GUIStyleState
+ {
+#if UNITY_2019_4_OR_NEWER
+ background = Texture2D.grayTexture
+#else
+ background = Texture2D.whiteTexture
+#endif
+ }
+ };
+ }
+
+ const string k_MenuItemName = "Window/SuperScience/Prefab Layer Users";
+ const string k_WindowTitle = "Prefab Layer Users";
+ const string k_NoLayerUsers = "No prefabs using a non-default layer";
+ const string k_ProjectFolderName = "Project";
+ const int k_FilterPanelWidth = 180;
+ const int k_ObjectFieldWidth = 150;
+ const string k_Instructions = "Click the Scan button to scan your project for users of non-default layers. WARNING: " +
+ "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor.";
+ const string k_ScanFilter = "t:Prefab";
+ const int k_ProgressBarHeight = 15;
+ const int k_MaxScanUpdateTimeMilliseconds = 50;
+ const int k_InvalidLayer = -1;
+
+ static readonly GUIContent k_IncludeLayerMaskFieldsGUIContent = new GUIContent("Include LayerMask Fields",
+ "Include layers from layer mask fields in the results. This is only possible if there is at least one layer without a name.");
+
+ static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for users of non-default layers");
+ static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan");
+ static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth);
+ static readonly Vector2 k_MinSize = new Vector2(400, 200);
+
+ static readonly Stopwatch k_StopWatch = new Stopwatch();
+
+ Vector2 m_FilterListScrollPosition;
+ Vector2 m_FolderTreeScrollPosition;
+ readonly Folder m_ParentFolder = new Folder();
+ readonly SortedDictionary m_FilterRows = new SortedDictionary();
+ static readonly string[] k_ScanFolders = { "Assets", "Packages" };
+ int m_ScanCount;
+ int m_ScanProgress;
+ IEnumerator m_ScanEnumerator;
+ readonly Dictionary m_LayerToName = new Dictionary();
+ int m_LayerWithNoName = k_InvalidLayer;
+
+ [SerializeField]
+ bool m_IncludeLayerMaskFields = true;
+
+ [SerializeField]
+ int m_LayerFilter = k_InvalidLayer;
+
+ // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use
+ static readonly StringBuilder k_StringBuilder = new StringBuilder(4096);
+ static readonly List k_Components = new List();
+ static readonly SortedSet k_LayerUnionHashSet = new SortedSet();
+ static readonly Stack k_TransformPathStack = new Stack();
+
+ ///
+ /// Initialize the window
+ ///
+ [MenuItem(k_MenuItemName)]
+ static void Init()
+ {
+ GetWindow(k_WindowTitle).Show();
+ }
+
+ void OnEnable()
+ {
+ minSize = k_MinSize;
+ m_ScanCount = 0;
+ m_ScanProgress = 0;
+ }
+
+ void OnDisable()
+ {
+ m_ScanEnumerator = null;
+ }
+
+ void OnGUI()
+ {
+ EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth;
+
+ if (m_ScanEnumerator == null)
+ {
+ if (GUILayout.Button(k_ScanGUIContent))
+ Scan();
+ }
+ else
+ {
+ if (GUILayout.Button(k_CancelGUIContent))
+ m_ScanEnumerator = null;
+ }
+
+ // If m_LayerToName hasn't been set up, we haven't scanned yet
+ // This dictionary will always at least include the built-in layer names
+ if (m_LayerToName.Count == 0)
+ {
+ EditorGUILayout.HelpBox(k_Instructions, MessageType.Info);
+ GUIUtility.ExitGUI();
+ return;
+ }
+
+
+ using (new GUILayout.HorizontalScope())
+ {
+ using (new GUILayout.VerticalScope(k_FilterPanelWidthOption))
+ {
+ DrawFilters();
+ }
+
+ using (new GUILayout.VerticalScope())
+ {
+ using (new EditorGUI.DisabledScope(m_LayerWithNoName == k_InvalidLayer))
+ {
+ m_IncludeLayerMaskFields = EditorGUILayout.Toggle(k_IncludeLayerMaskFieldsGUIContent, m_IncludeLayerMaskFields);
+ }
+
+ if (m_ParentFolder.GetCount(m_LayerFilter, m_IncludeLayerMaskFields) == 0)
+ {
+ GUILayout.Label(k_NoLayerUsers);
+ }
+ else
+ {
+ using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition))
+ {
+ m_FolderTreeScrollPosition = scrollView.scrollPosition;
+ m_ParentFolder.Draw(k_ProjectFolderName, m_LayerToName, m_LayerFilter, m_IncludeLayerMaskFields);
+ }
+ }
+ }
+
+ if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0)
+ {
+ var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight);
+ EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}");
+ }
+ }
+ }
+
+ ///
+ /// Draw a list buttons for filtering based on layer.
+ ///
+ void DrawFilters()
+ {
+ var count = m_ParentFolder.GetCount(k_InvalidLayer, m_IncludeLayerMaskFields);
+ var style = m_LayerFilter == k_InvalidLayer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"All ({count})", style))
+ m_LayerFilter = k_InvalidLayer;
+
+ using (var scrollView = new GUILayout.ScrollViewScope(m_FilterListScrollPosition))
+ {
+ m_FilterListScrollPosition = scrollView.scrollPosition;
+ foreach (var kvp in m_FilterRows)
+ {
+ var layer = kvp.Key;
+
+ // Skip the default layer
+ if (layer == 0)
+ continue;
+
+ count = 0;
+ if (m_FilterRows.TryGetValue(layer, out var filterRow))
+ count = m_IncludeLayerMaskFields ? filterRow.AllUsers.Count : filterRow.UsersWithoutLayerMasks.Count;
+
+ var layerName = GetLayerNameString(m_LayerToName, layer);
+ style = m_LayerFilter == layer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"{layer}: {layerName} ({count})", style))
+ m_LayerFilter = layer;
+ }
+ }
+ }
+
+ ///
+ /// Update the current scan coroutine.
+ ///
+ void UpdateScan()
+ {
+ if (m_ScanEnumerator == null)
+ return;
+
+ k_StopWatch.Reset();
+ k_StopWatch.Start();
+
+ // Process as many steps as possible within a given time frame
+ while (m_ScanEnumerator.MoveNext())
+ {
+ // Process for a maximum amount of time and early-out to keep the UI responsive
+ if (k_StopWatch.ElapsedMilliseconds > k_MaxScanUpdateTimeMilliseconds)
+ break;
+ }
+
+ m_ParentFolder.SortContentsRecursively();
+ Repaint();
+ }
+
+ ///
+ /// Coroutine for processing scan results.
+ ///
+ /// Prefab assets to scan.
+ /// IEnumerator used to run the coroutine.
+ IEnumerator ProcessScan(List<(string, GameObject)> prefabAssets)
+ {
+ m_ScanCount = prefabAssets.Count;
+ m_ScanProgress = 0;
+ foreach (var (path, prefabAsset) in prefabAssets)
+ {
+ FindLayerUsersInPrefab(path, prefabAsset);
+ m_ScanProgress++;
+ yield return null;
+ }
+
+ m_ScanEnumerator = null;
+ EditorApplication.update -= UpdateScan;
+ }
+
+ void FindLayerUsersInPrefab(string path, GameObject prefabAsset)
+ {
+ var gameObjectLayers = new SortedSet();
+ var layerMaskLayers = new SortedSet();
+ var layerUsers = new List();
+ var layerUsersWithoutLayerMasks = 0;
+
+ var prefabRoot = prefabAsset;
+ var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
+ if (prefabStage != null && prefabStage.assetPath == path)
+ prefabRoot = prefabStage.prefabContentsRoot;
+
+ FindLayerUsersRecursively(prefabRoot, gameObjectLayers, layerMaskLayers, layerUsers, ref layerUsersWithoutLayerMasks);
+ if (gameObjectLayers.Count > 0 || layerMaskLayers.Count > 0)
+ {
+ var prefabRow = new PrefabRow
+ {
+ Path = path,
+ PrefabAsset = prefabAsset,
+ GameObjectLayers = gameObjectLayers,
+ LayerMaskLayers = layerMaskLayers,
+ LayerUsers = layerUsers,
+ LayerUsersWithoutLayerMasks = layerUsersWithoutLayerMasks
+ };
+
+ m_ParentFolder.AddPrefab(prefabRow);
+ k_LayerUnionHashSet.Clear();
+ k_LayerUnionHashSet.UnionWith(gameObjectLayers);
+ k_LayerUnionHashSet.UnionWith(layerMaskLayers);
+ foreach (var layer in k_LayerUnionHashSet)
+ {
+ var filterRow = GetOrCreateFilterRowForLayer(layer);
+ filterRow.AllUsers.Add(prefabAsset);
+ }
+
+ foreach (var layer in gameObjectLayers)
+ {
+ var filterRow = GetOrCreateFilterRowForLayer(layer);
+ filterRow.UsersWithoutLayerMasks.Add(prefabAsset);
+ }
+ }
+ }
+
+ void FindLayerUsersRecursively(GameObject gameObject, SortedSet prefabGameObjectLayers, SortedSet prefabLayerMaskLayers, List layerUsers, ref int layerUsersWithoutLayerMasks)
+ {
+ var isLayerUser = false;
+ var layer = gameObject.layer;
+ if (layer != 0)
+ {
+ isLayerUser = true;
+ prefabGameObjectLayers.Add(layer);
+ layerUsersWithoutLayerMasks++;
+ }
+
+ var layerMaskLayers = new SortedSet();
+ var componentRows = new List();
+
+ // GetComponents will clear the list, so we don't have to
+ gameObject.GetComponents(k_Components);
+ foreach (var component in k_Components)
+ {
+ var serializedObject = new SerializedObject(component);
+ var iterator = serializedObject.GetIterator();
+ var componentUsesLayers = false;
+ var usedLayers = new SortedSet();
+ while (iterator.Next(true))
+ {
+ if (iterator.propertyType != SerializedPropertyType.LayerMask)
+ continue;
+
+ componentUsesLayers |= GetLayersFromLayerMask(usedLayers, iterator.intValue);
+ }
+
+ isLayerUser |= componentUsesLayers;
+
+ if (componentUsesLayers)
+ {
+ prefabLayerMaskLayers.UnionWith(usedLayers);
+ layerMaskLayers.UnionWith(usedLayers);
+ componentRows.Add(new ComponentRow
+ {
+ Component = component,
+ UsedLayers = usedLayers
+ });
+ }
+ }
+
+ // Clear the list after we're done to avoid lingering references
+ k_Components.Clear();
+
+ if (isLayerUser)
+ {
+ layerUsers.Add(new GameObjectRow
+ {
+ TransformPath = GetTransformPath(gameObject.transform),
+ GameObject = gameObject,
+ LayerMaskComponents = componentRows,
+ LayerMaskLayers = layerMaskLayers
+ });
+ }
+
+ foreach (Transform child in gameObject.transform)
+ {
+ FindLayerUsersRecursively(child.gameObject, prefabGameObjectLayers, prefabLayerMaskLayers, layerUsers, ref layerUsersWithoutLayerMasks);
+ }
+ }
+
+ bool GetLayersFromLayerMask(SortedSet layers, int layerMask)
+ {
+ // If all layers are named, it is not possible to infer whether layer mask fields "use" a layer
+ if (m_LayerWithNoName == k_InvalidLayer)
+ return false;
+
+ // Exclude the special cases where every layer is included or excluded
+ if (layerMask == k_InvalidLayer || layerMask == 0)
+ return false;
+
+ // Depending on whether or not the mask started out as "Everything" or "Nothing", a layer will count as "used" when the user toggles its state.
+ // We use the layer without a name to check whether or not the starting point is "Everything" or "Nothing." If this layer's bit is 0, we assume
+ // the mask started with "Nothing." Otherwise, if its bit is 1, we assume the mask started with "Everything."
+ var defaultValue = (layerMask & 1 << m_LayerWithNoName) != 0;
+
+ var isLayerMaskUser = false;
+ foreach (var kvp in m_LayerToName)
+ {
+ var layer = kvp.Key;
+
+ // Skip layer 0 since we only want non-default layers
+ if (layer == 0)
+ continue;
+
+ // We compare (using xor) this layer's bit value with the default value. If they are different, the layer counts as "used."
+ if ((layerMask & (1 << layer)) != 0 ^ defaultValue)
+ {
+ isLayerMaskUser = true;
+ layers.Add(layer);
+ }
+ }
+
+ return isLayerMaskUser;
+ }
+
+ ///
+ /// Scan the project for layer users and populate the data structures for UI.
+ ///
+ void Scan()
+ {
+ var guids = AssetDatabase.FindAssets(k_ScanFilter, k_ScanFolders);
+ if (guids == null || guids.Length == 0)
+ return;
+
+ m_FilterRows.Clear();
+ m_ParentFolder.Clear();
+
+ m_LayerWithNoName = k_InvalidLayer;
+ m_LayerToName.Clear();
+ for (var i = 0; i < 32; i++)
+ {
+ var layerName = LayerMask.LayerToName(i);
+ if (!string.IsNullOrEmpty(layerName))
+ {
+ m_LayerToName.Add(i, layerName);
+ m_FilterRows.Add(i, new FilterRow());
+ }
+ else
+ {
+ m_LayerWithNoName = i;
+ }
+ }
+
+ // LayerMask field scanning requires at least one layer without a name
+ if (m_LayerWithNoName == k_InvalidLayer)
+ m_IncludeLayerMaskFields = false;
+
+ var prefabAssets = new List<(string, GameObject)>();
+ foreach (var guid in guids)
+ {
+ var path = AssetDatabase.GUIDToAssetPath(guid);
+ if (string.IsNullOrEmpty(path))
+ {
+ Debug.Log($"Could not convert {guid} to path");
+ continue;
+ }
+
+ var prefab = AssetDatabase.LoadAssetAtPath(path);
+ if (prefab == null)
+ {
+ Debug.LogWarning($"Could not load prefab at {path}");
+ continue;
+ }
+
+ prefabAssets.Add((path, prefab));
+ }
+
+ m_ScanEnumerator = ProcessScan(prefabAssets);
+ EditorApplication.update += UpdateScan;
+ }
+
+ ///
+ /// Get or create a for a given layer value.
+ ///
+ /// The layer value to use for this row.
+ /// The row for the layer value.
+ FilterRow GetOrCreateFilterRowForLayer(int layer)
+ {
+ if (m_FilterRows.TryGetValue(layer, out var filterRow))
+ return filterRow;
+
+ filterRow = new FilterRow();
+ m_FilterRows[layer] = filterRow;
+ return filterRow;
+ }
+
+ static string GetLayerNameList(IEnumerable gameObjectLayers, IEnumerable layerMaskLayers,
+ Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ k_LayerUnionHashSet.Clear();
+ k_LayerUnionHashSet.UnionWith(gameObjectLayers);
+ if (includeLayerMaskFields)
+ k_LayerUnionHashSet.UnionWith(layerMaskLayers);
+
+ return GetLayerNameList(k_LayerUnionHashSet, layerToName, layerFilter);
+ }
+
+ static string GetLayerNameList(IEnumerable layers, Dictionary layerToName, int layerFilter = k_InvalidLayer)
+ {
+ if (layerFilter >= 0)
+ return GetLayerNameString(layerToName, layerFilter);
+
+ k_StringBuilder.Length = 0;
+ foreach (var layer in layers)
+ {
+ k_StringBuilder.Append($"{GetLayerNameString(layerToName, layer)}, ");
+ }
+
+ // Remove the last ", ". If we didn't add any layers, the StringBuilder will be empty so skip this step
+ if (k_StringBuilder.Length >= 2)
+ k_StringBuilder.Length -= 2;
+
+ return k_StringBuilder.ToString();
+ }
+
+ static string GetLayerNameString(Dictionary layerToName, int layer)
+ {
+ layerToName.TryGetValue(layer, out var layerName);
+ if (string.IsNullOrEmpty(layerName))
+ layerName = layer.ToString();
+
+ return layerName;
+ }
+
+ static string GetTransformPath(Transform transform)
+ {
+ if (transform.parent == null)
+ return transform.name;
+
+ k_TransformPathStack.Clear();
+ while (transform != null)
+ {
+ k_TransformPathStack.Push(transform.name);
+ transform = transform.parent;
+ }
+
+ k_StringBuilder.Length = 0;
+ while (k_TransformPathStack.Count > 0)
+ {
+ k_StringBuilder.Append(k_TransformPathStack.Pop());
+ k_StringBuilder.Append("/");
+ }
+
+ k_StringBuilder.Length -= 1;
+ return k_StringBuilder.ToString();
+ }
+ }
+}
diff --git a/Editor/LayerUsers/PrefabLayerUsers.cs.meta b/Editor/LayerUsers/PrefabLayerUsers.cs.meta
new file mode 100644
index 0000000..e4eb1bf
--- /dev/null
+++ b/Editor/LayerUsers/PrefabLayerUsers.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6c6cd2b1d787a3145beb17890a5a9672
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/LayerUsers/PrefabTagUsers.cs b/Editor/LayerUsers/PrefabTagUsers.cs
new file mode 100644
index 0000000..66f4ba8
--- /dev/null
+++ b/Editor/LayerUsers/PrefabTagUsers.cs
@@ -0,0 +1,616 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEditor.Experimental.SceneManagement;
+using UnityEditorInternal;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+
+namespace Unity.Labs.SuperScience
+{
+ ///
+ /// Scans the project for prefabs which use tags.
+ /// Use this utility to track down where a certain tag is used.
+ ///
+ public class PrefabTagUsers : EditorWindow
+ {
+ ///
+ /// Tree structure for folder scan results.
+ /// This is the root object for the project scan, and represents the results in a hierarchy that matches the
+ /// project's folder structure for an easy to read presentation of tag users.
+ /// When the Scan method encounters a tag user, we search the parent folder for one of these using the asset path to file it where it belongs.
+ ///
+ class Folder
+ {
+ // TODO: Share code between this window and others that display a folder structure
+ const int k_IndentAmount = 15;
+ const int k_SeparatorLineHeight = 1;
+
+ readonly SortedDictionary m_Subfolders = new SortedDictionary();
+ readonly List m_Prefabs = new List();
+ readonly SortedDictionary m_CountPerTag = new SortedDictionary();
+ int m_TotalCount;
+ bool m_Expanded;
+
+ ///
+ /// Clear the contents of this container.
+ ///
+ public void Clear()
+ {
+ m_Subfolders.Clear();
+ m_Prefabs.Clear();
+ m_CountPerTag.Clear();
+ m_TotalCount = 0;
+ }
+
+ ///
+ /// Add a prefab to this folder at a given path.
+ ///
+ public void AddPrefab(PrefabRow prefabRow)
+ {
+ var folder = GetOrCreateFolderForAssetPath(prefabRow);
+ folder.m_Prefabs.Add(prefabRow);
+ }
+
+ ///
+ /// Get the Folder object which corresponds to the path of a given .
+ /// If this is the first asset encountered for a given folder, create a chain of folder objects
+ /// rooted with this one and return the folder at the end of that chain.
+ /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one more tag user.
+ ///
+ /// A object containing the prefab asset reference and its metadata
+ /// The folder object corresponding to the folder containing the tag users at the given path.
+ Folder GetOrCreateFolderForAssetPath(PrefabRow prefabRow)
+ {
+ var directories = prefabRow.Path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ var folder = this;
+ folder.AggregateCount(prefabRow.Tags);
+ var length = directories.Length - 1;
+ for (var i = 0; i < length; i++)
+ {
+ var directory = directories[i];
+ var subfolders = folder.m_Subfolders;
+ if (!subfolders.TryGetValue(directory, out var subfolder))
+ {
+ subfolder = new Folder();
+ subfolders[directory] = subfolder;
+ }
+
+ folder = subfolder;
+ folder.AggregateCount(prefabRow.Tags);
+ }
+
+ return folder;
+ }
+
+ ///
+ /// Draw GUI for this Folder.
+ ///
+ /// The name of the folder.
+ /// (Optional) Tag used to filter results.
+ public void Draw(string name, string tagFilter = null)
+ {
+ var wasExpanded = m_Expanded;
+ var tagList = GetTagList(m_CountPerTag.Keys, tagFilter);
+ var label = $"{name}: {m_TotalCount} {{{tagList}}}";
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+
+ DrawLineSeparator();
+
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ SetExpandedRecursively(m_Expanded);
+
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var kvp in m_Subfolders)
+ {
+ var folder = kvp.Value;
+ if (folder.GetCount(tagFilter) == 0)
+ continue;
+
+ folder.Draw(kvp.Key, tagFilter);
+ }
+
+ var showedPrefab = false;
+ foreach (var prefabRow in m_Prefabs)
+ {
+ var tags = prefabRow.Tags;
+ if (string.IsNullOrEmpty(tagFilter) || tags.Contains(tagFilter))
+ {
+ prefabRow.Draw(tagFilter);
+ showedPrefab = true;
+ }
+ }
+
+ if (showedPrefab)
+ DrawLineSeparator();
+ }
+ }
+
+ ///
+ /// Draw a separator line.
+ ///
+ static void DrawLineSeparator()
+ {
+ EditorGUILayout.Separator();
+ using (new GUILayout.HorizontalScope())
+ {
+ GUILayout.Space(EditorGUI.indentLevel * k_IndentAmount);
+ GUILayout.Box(GUIContent.none, Styles.LineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true));
+ }
+
+ EditorGUILayout.Separator();
+ }
+
+ ///
+ /// Set the expanded state of this folder, its contents and their children and all of its subfolders and their contents and children.
+ ///
+ /// Whether this object should be expanded in the GUI.
+ void SetExpandedRecursively(bool expanded)
+ {
+ m_Expanded = expanded;
+ foreach (var kvp in m_Subfolders)
+ {
+ kvp.Value.SetExpandedRecursively(expanded);
+ }
+ }
+
+ ///
+ /// Sort the contents of this folder and all subfolders by name.
+ ///
+ public void SortContentsRecursively()
+ {
+ m_Prefabs.Sort((a, b) => a.PrefabAsset.name.CompareTo(b.PrefabAsset.name));
+ foreach (var kvp in m_Subfolders)
+ {
+ kvp.Value.SortContentsRecursively();
+ }
+ }
+
+ public int GetCount(string tagFilter = null)
+ {
+ if (string.IsNullOrEmpty(tagFilter))
+ return m_TotalCount;
+
+ m_CountPerTag.TryGetValue(tagFilter, out var count);
+ return count;
+ }
+
+ void AggregateCount(SortedSet tags)
+ {
+ m_TotalCount++;
+ foreach (var tag in tags)
+ {
+ m_CountPerTag.TryGetValue(tag, out var count);
+ count++;
+ m_CountPerTag[tag] = count;
+ }
+ }
+ }
+
+ class PrefabRow
+ {
+ public string Path;
+ public GameObject PrefabAsset;
+ public SortedSet Tags;
+ public List TagUsers;
+
+ bool m_Expanded;
+
+ public void Draw(string tagFilter = null)
+ {
+ var tagList = GetTagList(Tags, tagFilter);
+ var label = $"{PrefabAsset.name} {{{tagList}}}";
+ if (TagUsers.Count > 0)
+ {
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+ EditorGUILayout.ObjectField(PrefabAsset, typeof(GameObject), false);
+ }
+ else
+ {
+ EditorGUILayout.ObjectField(label, PrefabAsset, typeof(GameObject), false);
+ }
+
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var user in TagUsers)
+ {
+ if (!string.IsNullOrEmpty(tagFilter) && !user.GameObject.CompareTag(tagFilter))
+ continue;
+
+ user.Draw();
+ }
+ }
+ }
+ }
+
+ struct GameObjectRow
+ {
+ public string TransformPath;
+ public GameObject GameObject;
+
+ public void Draw()
+ {
+ EditorGUILayout.ObjectField($"{TransformPath} - Tag: {GameObject.tag}", GameObject, typeof(GameObject), true);
+ }
+ }
+
+ static class Styles
+ {
+ internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft,
+ fontStyle = FontStyle.Bold
+ };
+
+ internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft
+ };
+
+ internal static readonly GUIStyle LineStyle = new GUIStyle
+ {
+ normal = new GUIStyleState
+ {
+#if UNITY_2019_4_OR_NEWER
+ background = Texture2D.grayTexture
+#else
+ background = Texture2D.whiteTexture
+#endif
+ }
+ };
+ }
+
+ const string k_MenuItemName = "Window/SuperScience/Prefab Tag Users";
+ const string k_WindowTitle = "Prefab Tag Users";
+ const string k_NoTagUsers = "No prefabs using any tags";
+ const string k_ProjectFolderName = "Project";
+ const int k_FilterPanelWidth = 180;
+ const int k_ObjectFieldWidth = 150;
+ const string k_Instructions = "Click the Scan button to scan your project for tag users. WARNING: " +
+ "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor.";
+ const string k_ScanFilter = "t:Prefab";
+ const int k_ProgressBarHeight = 15;
+ const int k_MaxScanUpdateTimeMilliseconds = 50;
+ const string k_UntaggedString = "Untagged";
+
+ static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for tag users");
+ static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan");
+ static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth);
+ static readonly Vector2 k_MinSize = new Vector2(400, 200);
+ static readonly HashSet k_BuiltInTags = new HashSet
+ {
+ k_UntaggedString,
+ "Respawn",
+ "Finish",
+ "EditorOnly",
+ "MainCamera",
+ "Player",
+ "GameController",
+ };
+
+ static readonly Stopwatch k_StopWatch = new Stopwatch();
+
+ Vector2 m_FilterListScrollPosition;
+ Vector2 m_FolderTreeScrollPosition;
+ readonly Folder m_ParentFolder = new Folder();
+ readonly SortedDictionary> m_FilterRows = new SortedDictionary>();
+ static readonly string[] k_ScanFolders = { "Assets", "Packages" };
+ int m_ScanCount;
+ int m_ScanProgress;
+ IEnumerator m_ScanEnumerator;
+ bool m_Scanned;
+
+ [SerializeField]
+ string m_TagFilter;
+
+ // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use
+ static readonly StringBuilder k_StringBuilder = new StringBuilder(4096);
+ static readonly Stack k_TransformPathStack = new Stack();
+
+ ///
+ /// Initialize the window
+ ///
+ [MenuItem(k_MenuItemName)]
+ static void Init()
+ {
+ GetWindow(k_WindowTitle).Show();
+ }
+
+ void OnEnable()
+ {
+ minSize = k_MinSize;
+ m_ScanCount = 0;
+ m_ScanProgress = 0;
+ m_Scanned = false;
+ }
+
+ void OnDisable()
+ {
+ m_ScanEnumerator = null;
+ }
+
+ void OnGUI()
+ {
+ EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth;
+
+ if (m_ScanEnumerator == null)
+ {
+ if (GUILayout.Button(k_ScanGUIContent))
+ Scan();
+ }
+ else
+ {
+ if (GUILayout.Button(k_CancelGUIContent))
+ m_ScanEnumerator = null;
+ }
+
+ if (!m_Scanned)
+ {
+ EditorGUILayout.HelpBox(k_Instructions, MessageType.Info);
+ GUIUtility.ExitGUI();
+ return;
+ }
+
+ using (new GUILayout.HorizontalScope())
+ {
+ using (new GUILayout.VerticalScope(k_FilterPanelWidthOption))
+ {
+ DrawFilters();
+ }
+
+ if (m_ParentFolder.GetCount(m_TagFilter) == 0)
+ {
+ GUILayout.Label(k_NoTagUsers);
+ }
+ else
+ {
+ using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition))
+ {
+ m_FolderTreeScrollPosition = scrollView.scrollPosition;
+ m_ParentFolder.Draw(k_ProjectFolderName, m_TagFilter);
+ }
+ }
+ }
+
+ if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0)
+ {
+ var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight);
+ EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}");
+ }
+ }
+
+ ///
+ /// Draw a list buttons for filtering based on tag.
+ ///
+ void DrawFilters()
+ {
+ var count = m_ParentFolder.GetCount();
+ var style = string.IsNullOrEmpty(m_TagFilter) ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"All ({count})", style))
+ m_TagFilter = null;
+
+ using (var scrollView = new GUILayout.ScrollViewScope(m_FilterListScrollPosition))
+ {
+ m_FilterListScrollPosition = scrollView.scrollPosition;
+ foreach (var kvp in m_FilterRows)
+ {
+ var tag = kvp.Key;
+
+ count = 0;
+ if (m_FilterRows.TryGetValue(tag, out var filterRow))
+ count = filterRow.Count;
+
+ style = m_TagFilter == tag ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"{tag} ({count})", style))
+ m_TagFilter = tag;
+ }
+ }
+ }
+
+ ///
+ /// Update the current scan coroutine.
+ ///
+ void UpdateScan()
+ {
+ if (m_ScanEnumerator == null)
+ return;
+
+ k_StopWatch.Reset();
+ k_StopWatch.Start();
+
+ // Process as many steps as possible within a given time frame
+ while (m_ScanEnumerator.MoveNext())
+ {
+ // Process for a maximum amount of time and early-out to keep the UI responsive
+ if (k_StopWatch.ElapsedMilliseconds > k_MaxScanUpdateTimeMilliseconds)
+ break;
+ }
+
+ m_ParentFolder.SortContentsRecursively();
+ Repaint();
+ }
+
+ ///
+ /// Coroutine for processing scan results.
+ ///
+ /// Prefab assets to scan.
+ /// IEnumerator used to run the coroutine.
+ IEnumerator ProcessScan(List<(string, GameObject)> prefabAssets)
+ {
+ m_Scanned = true;
+ m_ScanCount = prefabAssets.Count;
+ m_ScanProgress = 0;
+ foreach (var (path, prefabAsset) in prefabAssets)
+ {
+ FindTagUsersInPrefab(path, prefabAsset);
+ m_ScanProgress++;
+ yield return null;
+ }
+
+ m_ScanEnumerator = null;
+ EditorApplication.update -= UpdateScan;
+ }
+
+ void FindTagUsersInPrefab(string path, GameObject prefabAsset)
+ {
+ var tags = new SortedSet();
+ var tagUsers = new List();
+
+ var prefabRoot = prefabAsset;
+ var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
+ if (prefabStage != null && prefabStage.assetPath == path)
+ prefabRoot = prefabStage.prefabContentsRoot;
+
+ FindTagUsersRecursively(prefabRoot, tags, tagUsers);
+ if (tags.Count > 0)
+ {
+ var prefabRow = new PrefabRow
+ {
+ Path = path,
+ PrefabAsset = prefabAsset,
+ Tags = tags,
+ TagUsers = tagUsers
+ };
+
+ m_ParentFolder.AddPrefab(prefabRow);
+ foreach (var tag in tags)
+ {
+ var prefabs = GetOrCreatePrefabHashSetForTag(tag);
+ prefabs.Add(prefabAsset);
+ }
+ }
+ }
+
+ static void FindTagUsersRecursively(GameObject gameObject, SortedSet tags, List tagUsers)
+ {
+ var tag = gameObject.tag;
+ if (!gameObject.CompareTag(k_UntaggedString))
+ {
+ tags.Add(tag);
+ tagUsers.Add(new GameObjectRow
+ {
+ TransformPath = GetTransformPath(gameObject.transform),
+ GameObject = gameObject
+ });
+ }
+
+ foreach (Transform child in gameObject.transform)
+ {
+ FindTagUsersRecursively(child.gameObject, tags, tagUsers);
+ }
+ }
+
+ ///
+ /// Scan the project for tag users and populate the data structures for UI.
+ ///
+ void Scan()
+ {
+ var guids = AssetDatabase.FindAssets(k_ScanFilter, k_ScanFolders);
+ if (guids == null || guids.Length == 0)
+ return;
+
+ m_FilterRows.Clear();
+
+ // Add all tags to FilterRows to include tags with no users
+ foreach (var tag in InternalEditorUtility.tags)
+ {
+ if (k_BuiltInTags.Contains(tag))
+ continue;
+
+ m_FilterRows[tag] = new HashSet();
+ }
+
+ m_ParentFolder.Clear();
+ var prefabAssets = new List<(string, GameObject)>();
+ foreach (var guid in guids)
+ {
+ var path = AssetDatabase.GUIDToAssetPath(guid);
+ if (string.IsNullOrEmpty(path))
+ {
+ Debug.Log($"Could not convert {guid} to path");
+ continue;
+ }
+
+ var prefab = AssetDatabase.LoadAssetAtPath(path);
+ if (prefab == null)
+ {
+ Debug.LogWarning($"Could not load prefab at {path}");
+ continue;
+ }
+
+ prefabAssets.Add((path, prefab));
+ }
+
+ m_ScanEnumerator = ProcessScan(prefabAssets);
+ EditorApplication.update += UpdateScan;
+ }
+
+ ///
+ /// Get or create a HashSet<GameObject> for a given tag.
+ ///
+ /// The tag to use for this row.
+ /// The row for the tag.
+ HashSet GetOrCreatePrefabHashSetForTag(string tag)
+ {
+ if (m_FilterRows.TryGetValue(tag, out var filterRow))
+ return filterRow;
+
+ filterRow = new HashSet();
+ m_FilterRows[tag] = filterRow;
+ return filterRow;
+ }
+
+ static string GetTagList(IEnumerable tags, string tagFilter = null)
+ {
+ if (!string.IsNullOrEmpty(tagFilter))
+ return tagFilter;
+
+ k_StringBuilder.Length = 0;
+ foreach (var tag in tags)
+ {
+ k_StringBuilder.Append($"{tag}, ");
+ }
+
+ // Remove the last ", ". If we didn't add any tags, the StringBuilder will be empty so skip this step
+ if (k_StringBuilder.Length >= 2)
+ k_StringBuilder.Length -= 2;
+
+ return k_StringBuilder.ToString();
+ }
+
+ static string GetTransformPath(Transform transform)
+ {
+ if (transform.parent == null)
+ return transform.name;
+
+ k_TransformPathStack.Clear();
+ while (transform != null)
+ {
+ k_TransformPathStack.Push(transform.name);
+ transform = transform.parent;
+ }
+
+ k_StringBuilder.Length = 0;
+ while (k_TransformPathStack.Count > 0)
+ {
+ k_StringBuilder.Append(k_TransformPathStack.Pop());
+ k_StringBuilder.Append("/");
+ }
+
+ k_StringBuilder.Length -= 1;
+ return k_StringBuilder.ToString();
+ }
+ }
+}
diff --git a/Editor/LayerUsers/PrefabTagUsers.cs.meta b/Editor/LayerUsers/PrefabTagUsers.cs.meta
new file mode 100644
index 0000000..5821642
--- /dev/null
+++ b/Editor/LayerUsers/PrefabTagUsers.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 22c63d32dfd469744a9c8864c8260297
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/LayerUsers/SceneLayerUsers.cs b/Editor/LayerUsers/SceneLayerUsers.cs
new file mode 100644
index 0000000..7aba868
--- /dev/null
+++ b/Editor/LayerUsers/SceneLayerUsers.cs
@@ -0,0 +1,965 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEditor.Experimental.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace Unity.Labs.SuperScience
+{
+ ///
+ /// Scans all loaded scenes for GameObjects which use non-default layers and displays the results in an EditorWindow.
+ ///
+ public class SceneLayerUsers : EditorWindow
+ {
+ class SceneContainer
+ {
+ const string k_UntitledSceneName = "Untitled";
+
+ readonly string m_SceneName;
+ readonly List m_Roots;
+ readonly int m_TotalCount;
+ readonly int m_TotalWithoutLayerMasks;
+ readonly SortedDictionary m_CountPerLayer;
+ readonly SortedDictionary m_CountWithoutLayerMasksPerLayer;
+ readonly int[] m_Usages;
+ readonly int[] m_UsagesWithoutLayerMasks;
+
+ bool m_Expanded;
+
+ SceneContainer(string sceneName, List roots)
+ {
+ m_SceneName = sceneName;
+ m_Roots = roots;
+ m_CountPerLayer = new SortedDictionary();
+ m_CountWithoutLayerMasksPerLayer = new SortedDictionary();
+ foreach (var container in roots)
+ {
+ var layer = container.GameObject.layer;
+ if (layer != 0)
+ {
+ m_TotalCount++;
+ IncrementCountForLayer(layer, m_CountPerLayer);
+ IncrementCountForLayer(layer, m_CountWithoutLayerMasksPerLayer);
+ }
+
+ m_TotalCount += container.TotalUsagesInChildren;
+ m_TotalCount += container.TotalUsagesInComponents;
+ m_TotalWithoutLayerMasks += container.TotalUsagesInChildrenWithoutLayerMasks;
+ AggregateCountPerLayer(container.UsagesInChildrenPerLayer, m_CountPerLayer);
+ AggregateCountPerLayer(container.UsagesInComponentsPerLayer, m_CountPerLayer);
+ AggregateCountPerLayer(container.UsagesInChildrenWithoutLayerMasksPerLayer, m_CountWithoutLayerMasksPerLayer);
+ }
+
+ m_Usages = m_CountPerLayer.Keys.ToArray();
+ m_UsagesWithoutLayerMasks = m_CountPerLayer.Keys.ToArray();
+ }
+
+ public void Draw(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields)
+ {
+ var count = GetCount(layerFilter, includeLayerMaskFields);
+ if (count == 0)
+ return;
+
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append(m_SceneName);
+ k_StringBuilder.Append(" (");
+ k_StringBuilder.Append(count.ToString());
+ k_StringBuilder.Append(") {");
+ AppendLayerNameList(k_StringBuilder, m_Usages, m_UsagesWithoutLayerMasks, layerToName, layerFilter, includeLayerMaskFields);
+ k_StringBuilder.Append("}");
+ var label = k_StringBuilder.ToString();
+ var wasExpanded = m_Expanded;
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ {
+ foreach (var gameObjectContainer in m_Roots)
+ {
+ gameObjectContainer.SetExpandedRecursively(m_Expanded);
+ }
+ }
+
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var gameObjectContainer in m_Roots)
+ {
+ gameObjectContainer.Draw(layerToName, layerFilter, includeLayerMaskFields);
+ }
+ }
+ }
+
+ public static SceneContainer CreateIfNecessary(Scene scene, SortedDictionary filterRows,
+ Dictionary layerToName, int layerWithNoName)
+ {
+ var sceneName = scene.name;
+ if (string.IsNullOrEmpty(sceneName))
+ sceneName = k_UntitledSceneName;
+
+ List roots = null;
+ foreach (var gameObject in scene.GetRootGameObjects())
+ {
+ var rootContainer = GameObjectContainer.CreateIfNecessary(gameObject, filterRows, layerToName, layerWithNoName);
+ if (rootContainer != null)
+ {
+ roots ??= new List();
+ roots.Add(rootContainer);
+ }
+ }
+
+ return roots != null ? new SceneContainer(sceneName, roots) : null;
+ }
+
+ public int GetCount(int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ var unfiltered = layerFilter == k_InvalidLayer;
+ if (unfiltered && includeLayerMaskFields)
+ return m_TotalCount;
+
+ if (unfiltered)
+ return m_TotalWithoutLayerMasks;
+
+ if (includeLayerMaskFields)
+ {
+ m_CountPerLayer.TryGetValue(layerFilter, out var totalCount);
+ return totalCount;
+ }
+
+ m_CountWithoutLayerMasksPerLayer.TryGetValue(layerFilter, out var totalWithoutLayerMasks);
+ return totalWithoutLayerMasks;
+ }
+ }
+
+ ///
+ /// Tree structure for GameObject scan results
+ /// When the Scan method encounters a GameObject in a scene or a prefab in the project, we initialize one of
+ /// these using the GameObject as an argument. This scans the object and its components/children, retaining
+ /// the results for display in the GUI. The window calls into these helper objects to draw them, as well.
+ ///
+ class GameObjectContainer
+ {
+ ///
+ /// Container for component scan results. Just as with GameObjectContainer, we initialize one of these
+ /// using a component to scan it for missing references and retain the results
+ ///
+ internal class ComponentContainer
+ {
+ readonly Component m_Component;
+ readonly List m_LayerMaskFields;
+ readonly SortedDictionary m_UsagesPerLayer;
+ readonly int[] m_Usages;
+
+ bool m_Expanded;
+ public int Count => m_LayerMaskFields.Count;
+ public SortedDictionary UsagesPerLayer => m_UsagesPerLayer;
+ public bool Expanded { set { m_Expanded = value; } }
+
+ ComponentContainer(Component component, SortedDictionary usagesPerLayer, List layerMaskFields)
+ {
+ m_Component = component;
+ m_UsagesPerLayer = usagesPerLayer;
+ m_LayerMaskFields = layerMaskFields;
+ m_Usages = m_UsagesPerLayer.Keys.ToArray();
+ }
+
+ ///
+ /// Draw the missing references UI for this component
+ ///
+ public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer)
+ {
+ if (layerFilter != k_InvalidLayer && !m_UsagesPerLayer.ContainsKey(layerFilter))
+ return;
+
+ // Because we can potentially draw a lot of rows, the efficiency using a StringBuilder is worth the messy code
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append(m_Component.GetType().Name);
+ k_StringBuilder.Append(" {");
+ k_StringBuilder.Append(m_LayerMaskFields.Count.ToString());
+ k_StringBuilder.Append(") {");
+ AppendLayerNameList(k_StringBuilder, m_Usages, layerToName, layerFilter);
+ k_StringBuilder.Append("}");
+ var label = k_StringBuilder.ToString();
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ EditorGUILayout.ObjectField(m_Component, typeof(Component), true);
+ foreach (var property in m_LayerMaskFields)
+ {
+ using (var check = new EditorGUI.ChangeCheckScope())
+ {
+ EditorGUILayout.PropertyField(property);
+ if (check.changed)
+ property.serializedObject.ApplyModifiedProperties();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Initialize a ComponentContainer to represent the given Component
+ /// This will scan the component for missing references and retain the information for display in
+ /// the given window.
+ ///
+ /// The Component to scan for missing references
+ /// Layer to Name dictionary for fast lookup of layer names.
+ /// An unnamed layer to use for layer mask checks.
+ public static ComponentContainer CreateIfNecessary(Component component, Dictionary layerToName, int layerWithNoName)
+ {
+ var serializedObject = new SerializedObject(component);
+ var iterator = serializedObject.GetIterator();
+ SortedDictionary usagesPerLayer = null;
+ List layerMaskFields = null;
+ while (iterator.Next(true))
+ {
+ if (iterator.propertyType != SerializedPropertyType.LayerMask)
+ continue;
+
+ var layerMask = iterator.intValue;
+ usagesPerLayer = GetLayersFromLayerMask(usagesPerLayer, layerToName, layerWithNoName, layerMask);
+ layerMaskFields ??= new List();
+ layerMaskFields.Add(iterator.Copy());
+ }
+
+ return usagesPerLayer != null ? new ComponentContainer(component, usagesPerLayer, layerMaskFields) : null;
+ }
+
+ static SortedDictionary GetLayersFromLayerMask(SortedDictionary usagesPerLayer,
+ Dictionary layerToName, int layerWithNoName, int layerMask)
+ {
+ // If all layers are named, it is not possible to infer whether layer mask fields "use" a layer
+ if (layerWithNoName == k_InvalidLayer)
+ return null;
+
+ // Exclude the special cases where every layer is included or excluded
+ if (layerMask == k_InvalidLayer || layerMask == 0)
+ return null;
+
+ // Depending on whether or not the mask started out as "Everything" or "Nothing", a layer will count as "used" when the user toggles its state.
+ // We use the layer without a name to check whether or not the starting point is "Everything" or "Nothing." If this layer's bit is 0, we assume
+ // the mask started with "Nothing." Otherwise, if its bit is 1, we assume the mask started with "Everything."
+ var defaultValue = (layerMask & 1 << layerWithNoName) != 0;
+
+ foreach (var kvp in layerToName)
+ {
+ var layer = kvp.Key;
+
+ // Skip layer 0 since we only want non-default layers
+ if (layer == 0)
+ continue;
+
+ // We compare (using xor) this layer's bit value with the default value. If they are different, the layer counts as "used."
+ if ((layerMask & (1 << layer)) != 0 ^ defaultValue)
+ {
+ usagesPerLayer ??= new SortedDictionary();
+ IncrementCountForLayer(layer, usagesPerLayer);
+ }
+ }
+
+ return usagesPerLayer;
+ }
+ }
+
+ readonly GameObject m_GameObject;
+ readonly List m_Children;
+ readonly List m_Components;
+
+ readonly int m_TotalUsagesInComponents;
+ readonly SortedDictionary m_UsagesInComponentsPerLayer;
+
+ //TODO: Rename Users -> Usages
+ readonly int m_TotalUsagesInChildren;
+ readonly int m_TotalUsagesInChildrenWithoutLayerMasks;
+ readonly SortedDictionary m_UsagesInChildrenPerLayer;
+ readonly SortedDictionary m_UsagesInChildrenWithoutLayerMasksPerLayer;
+
+ readonly int[] m_Usages;
+ readonly int[] m_UsagesWithoutLayerMasks;
+ readonly int[] m_UsagesInComponents;
+ readonly int[] m_UsagesInChildren;
+ readonly int[] m_UsagesInChildrenWithoutLayerMasks;
+
+ bool m_Expanded;
+ bool m_ShowComponents;
+ bool m_ShowChildren;
+
+ public GameObject GameObject { get { return m_GameObject; } }
+ public int TotalUsagesInComponents => m_TotalUsagesInComponents;
+ public SortedDictionary UsagesInComponentsPerLayer => m_UsagesInComponentsPerLayer;
+ public int TotalUsagesInChildren => m_TotalUsagesInChildren;
+ public int TotalUsagesInChildrenWithoutLayerMasks => m_TotalUsagesInChildrenWithoutLayerMasks;
+ public SortedDictionary UsagesInChildrenPerLayer => m_UsagesInChildrenPerLayer;
+ public SortedDictionary UsagesInChildrenWithoutLayerMasksPerLayer => m_UsagesInChildrenWithoutLayerMasksPerLayer;
+
+ // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use
+ static readonly SortedSet k_Usages = new SortedSet();
+ static readonly SortedSet k_UsagesWithoutLayerMasks = new SortedSet();
+
+ GameObjectContainer(GameObject gameObject, List components, List children)
+ {
+ m_GameObject = gameObject;
+ m_Components = components;
+ m_Children = children;
+ k_Usages.Clear();
+ k_UsagesWithoutLayerMasks.Clear();
+
+ var layer = gameObject.layer;
+ if (layer != 0)
+ {
+ k_Usages.Add(layer);
+ k_UsagesWithoutLayerMasks.Add(layer);
+ }
+
+ if (components != null)
+ {
+ m_UsagesInComponentsPerLayer = new SortedDictionary();
+ foreach (var container in components)
+ {
+ AggregateCount(container, ref m_TotalUsagesInComponents);
+ }
+
+ m_UsagesInComponents = m_UsagesInComponentsPerLayer.Keys.ToArray();
+ k_Usages.UnionWith(m_UsagesInComponents);
+ }
+
+ if (children != null)
+ {
+ m_UsagesInChildrenPerLayer = new SortedDictionary();
+ m_UsagesInChildrenWithoutLayerMasksPerLayer = new SortedDictionary();
+ foreach (var container in children)
+ {
+ var componentUsages = container.m_UsagesInComponentsPerLayer;
+ if (componentUsages != null)
+ m_UsagesInComponentsPerLayer ??= new SortedDictionary();
+
+ AggregateCount(container, ref m_TotalUsagesInChildren, ref m_TotalUsagesInChildrenWithoutLayerMasks);
+ }
+
+ m_UsagesInChildren = m_UsagesInChildrenPerLayer.Keys.ToArray();
+ m_UsagesInChildrenWithoutLayerMasks = m_UsagesInChildrenWithoutLayerMasksPerLayer.Keys.ToArray();
+ k_Usages.UnionWith(m_UsagesInChildren);
+ k_UsagesWithoutLayerMasks.UnionWith(m_UsagesInChildrenWithoutLayerMasks);
+ }
+
+ m_Usages = k_Usages.ToArray();
+ m_UsagesWithoutLayerMasks = k_UsagesWithoutLayerMasks.ToArray();
+ }
+
+ ///
+ /// Initialize a GameObjectContainer to represent the given GameObject
+ /// This will scan the component for missing references and retain the information for display in
+ /// the given window.
+ ///
+ /// The GameObject to scan for missing references
+ /// Dictionary of FilterRow objects for counting usages per layer.
+ /// Layer to Name dictionary for fast lookup of layer names.
+ /// An unnamed layer to use for layer mask checks.
+ public static GameObjectContainer CreateIfNecessary(GameObject gameObject, SortedDictionary filterRows,
+ Dictionary layerToName, int layerWithNoName)
+ {
+ // GetComponents will clear the list, so we don't havWe to
+ gameObject.GetComponents(k_Components);
+ List components = null;
+ foreach (var component in k_Components)
+ {
+ var container = ComponentContainer.CreateIfNecessary(component, layerToName, layerWithNoName);
+ if (container != null)
+ {
+ components ??= new List();
+ components.Add(container);
+ }
+ }
+
+ // Clear the list after we're done to avoid lingering references
+ k_Components.Clear();
+
+ List children = null;
+ foreach (Transform child in gameObject.transform)
+ {
+ var childContainer = CreateIfNecessary(child.gameObject, filterRows, layerToName, layerWithNoName);
+ if (childContainer != null)
+ {
+ children ??= new List();
+ children.Add(childContainer);
+ }
+ }
+
+ var layer = gameObject.layer;
+ var isLayerUser = layer != 0;
+ if (isLayerUser || components != null || children != null)
+ {
+ if (isLayerUser)
+ {
+ var filterRow = GetOrCreateFilterRowForLayer(filterRows, layer);
+ filterRow.UsersWithoutLayerMasks.Add(gameObject);
+ filterRow.AllUsers.Add(gameObject);
+ }
+
+ var newContainer = new GameObjectContainer(gameObject, components, children);
+ if (components != null)
+ {
+ foreach (var kvp in newContainer.m_UsagesInComponentsPerLayer)
+ {
+ var filterRow = GetOrCreateFilterRowForLayer(filterRows, kvp.Key);
+ filterRow.AllUsers.Add(gameObject);
+ }
+ }
+
+ return newContainer;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Draw layer user information for this GameObjectContainer
+ ///
+ public void Draw(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields)
+ {
+ var layer = m_GameObject.layer;
+ var isLayerUser = layerFilter == k_InvalidLayer ? layer != 0 : layer == layerFilter;
+
+ var componentCount = 0;
+ var hasComponents = false;
+ if (includeLayerMaskFields)
+ {
+ componentCount = GetComponentCount(layerFilter);
+ hasComponents = componentCount > 0;
+ }
+
+ var childCount = GetChildCount(layerFilter, includeLayerMaskFields);
+ var hasChildren = childCount > 0;
+ if (!(isLayerUser || hasChildren || hasComponents))
+ return;
+
+ var count = componentCount + childCount;
+ var label = GetLabel(layerToName, layerFilter, includeLayerMaskFields, count, isLayerUser, layer);
+ var wasExpanded = m_Expanded;
+ if (hasChildren || hasComponents)
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+ else
+ EditorGUILayout.LabelField(label);
+
+
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ {
+ if (m_Components != null)
+ {
+ foreach (var gameObjectContainer in m_Components)
+ {
+ gameObjectContainer.Expanded = m_Expanded;
+ }
+ }
+
+ if (m_Children != null)
+ {
+ foreach (var gameObjectContainer in m_Children)
+ {
+ gameObjectContainer.SetExpandedRecursively(m_Expanded);
+ }
+ }
+ }
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ if (isLayerUser)
+ EditorGUILayout.ObjectField(m_GameObject, typeof(GameObject), true);
+
+ if (!m_Expanded)
+ return;
+
+ if (hasComponents && m_Components != null)
+ DrawComponents(layerToName, layerFilter, componentCount);
+
+ if (hasChildren && m_Children != null)
+ DrawChildren(layerToName, layerFilter, includeLayerMaskFields, childCount);
+ }
+ }
+
+ string GetLabel(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields, int count, bool isLayerUser, int layer)
+ {
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append(m_GameObject.name);
+ k_StringBuilder.Append(" (");
+ k_StringBuilder.Append(count.ToString());
+ if (isLayerUser)
+ {
+ var layerName = GetLayerNameString(layerToName, layer);
+ k_StringBuilder.Append(") - Layer: ");
+ k_StringBuilder.Append(layerName);
+ k_StringBuilder.Append(" {");
+ }
+ else
+ {
+ k_StringBuilder.Append(") {");
+ }
+
+ AppendLayerNameList(k_StringBuilder, m_Usages, m_UsagesWithoutLayerMasks, layerToName, layerFilter, includeLayerMaskFields);
+ k_StringBuilder.Append("}");
+
+ return k_StringBuilder.ToString();
+ }
+
+ void DrawComponents(Dictionary layerToName, int layerFilter, int componentCount)
+ {
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append("Components (");
+ k_StringBuilder.Append(componentCount.ToString());
+ k_StringBuilder.Append(") {");
+ AppendLayerNameList(k_StringBuilder, m_UsagesInComponents, layerToName, layerFilter);
+ k_StringBuilder.Append("}");
+ var label = k_StringBuilder.ToString();
+ m_ShowComponents = EditorGUILayout.Foldout(m_ShowComponents, label, true);
+ if (m_ShowComponents)
+ {
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var component in m_Components)
+ {
+ component.Draw(layerToName, layerFilter);
+ }
+ }
+ }
+ }
+
+ void DrawChildren(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields, int childCount)
+ {
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append("Children (");
+ k_StringBuilder.Append(childCount.ToString());
+ k_StringBuilder.Append(") {");
+ AppendLayerNameList(k_StringBuilder, m_UsagesInChildren, m_UsagesInChildrenWithoutLayerMasks, layerToName, layerFilter, includeLayerMaskFields);
+ k_StringBuilder.Append("}");
+ var label = k_StringBuilder.ToString();
+ m_ShowChildren = EditorGUILayout.Foldout(m_ShowChildren, label, true);
+ if (m_ShowChildren)
+ {
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var child in m_Children)
+ {
+ child.Draw(layerToName, layerFilter, includeLayerMaskFields);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Set the expanded state of this object and all of its children
+ ///
+ /// Whether this object should be expanded in the GUI
+ public void SetExpandedRecursively(bool expanded)
+ {
+ m_Expanded = expanded;
+ m_ShowComponents = expanded;
+ m_ShowChildren = expanded;
+
+ if (m_Components != null)
+ {
+ foreach (var component in m_Components)
+ {
+ component.Expanded = expanded;
+ }
+ }
+
+ if (m_Children != null)
+ {
+ foreach (var child in m_Children)
+ {
+ child.SetExpandedRecursively(expanded);
+ }
+ }
+ }
+
+ int GetComponentCount(int layerFilter = k_InvalidLayer)
+ {
+ if (m_Components == null)
+ return 0;
+
+ if (layerFilter == k_InvalidLayer)
+ return m_TotalUsagesInComponents;
+
+ m_UsagesInComponentsPerLayer.TryGetValue(layerFilter, out var count);
+ return count;
+ }
+
+ int GetChildCount(int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ if (m_Children == null)
+ return 0;
+
+ var unfiltered = layerFilter == k_InvalidLayer;
+ if (unfiltered && includeLayerMaskFields)
+ return m_TotalUsagesInChildren;
+
+ if (unfiltered)
+ return m_TotalUsagesInChildrenWithoutLayerMasks;
+
+ if (includeLayerMaskFields)
+ {
+ m_UsagesInChildrenPerLayer.TryGetValue(layerFilter, out var totalCount);
+ return totalCount;
+ }
+
+ m_UsagesInChildrenWithoutLayerMasksPerLayer.TryGetValue(layerFilter, out var totalWithoutLayerMasks);
+ return totalWithoutLayerMasks;
+ }
+
+ void AggregateCount(GameObjectContainer child, ref int usagesInChildren,
+ ref int usagesInChildrenWithoutLayerMasks)
+ {
+ var layer = child.GameObject.layer;
+ if (layer != 0)
+ {
+ usagesInChildren++;
+ usagesInChildrenWithoutLayerMasks++;
+ IncrementCountForLayer(layer, m_UsagesInChildrenPerLayer);
+ IncrementCountForLayer(layer, m_UsagesInChildrenWithoutLayerMasksPerLayer);
+ }
+
+ usagesInChildren += child.m_TotalUsagesInChildren;
+ usagesInChildrenWithoutLayerMasks += child.m_TotalUsagesInChildrenWithoutLayerMasks;
+ AggregateCountPerLayer(child.m_UsagesInChildrenPerLayer, m_UsagesInChildrenPerLayer);
+ AggregateCountPerLayer(child.m_UsagesInChildrenWithoutLayerMasksPerLayer, m_UsagesInChildrenWithoutLayerMasksPerLayer);
+
+ usagesInChildren += child.m_TotalUsagesInComponents;
+ AggregateCountPerLayer(child.m_UsagesInComponentsPerLayer, m_UsagesInChildrenPerLayer);
+ }
+
+ void AggregateCount(ComponentContainer container, ref int layerUsagesInComponents)
+ {
+ layerUsagesInComponents += container.Count;
+ AggregateCountPerLayer(container.UsagesPerLayer, m_UsagesInComponentsPerLayer);
+ }
+ }
+
+ class FilterRow
+ {
+ public readonly HashSet AllUsers = new HashSet();
+ public readonly HashSet UsersWithoutLayerMasks = new HashSet();
+ }
+
+ static class Styles
+ {
+ internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft,
+ fontStyle = FontStyle.Bold
+ };
+
+ internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft
+ };
+ }
+
+ const string k_MenuItemName = "Window/SuperScience/Scene Layer Users";
+ const string k_WindowTitle = "Scene Layer Users";
+ const string k_NoUsages = "No scene objects using a non-default layer";
+ const int k_FilterPanelWidth = 180;
+ const int k_ObjectFieldWidth = 150;
+ const string k_Instructions = "Click the Scan button to scan your project for users of non-default layers. WARNING: " +
+ "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor.";
+ const int k_ProgressBarHeight = 15;
+ const int k_InvalidLayer = -1;
+
+ static readonly GUIContent k_IncludeLayerMaskFieldsGUIContent = new GUIContent("Include LayerMask Fields",
+ "Include layers from layer mask fields in the results. This is only possible if there is at least one layer without a name.");
+
+ static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for users of non-default layers");
+ static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan");
+ static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth);
+ static readonly Vector2 k_MinSize = new Vector2(400, 200);
+
+ Vector2 m_ColorListScrollPosition;
+ Vector2 m_FolderTreeScrollPosition;
+ readonly List m_SceneContainers = new List();
+ readonly SortedDictionary m_FilterRows = new SortedDictionary();
+ int m_ScanCount;
+ int m_ScanProgress;
+ IEnumerator m_ScanEnumerator;
+ readonly Dictionary m_LayerToName = new Dictionary();
+ int m_LayerWithNoName = k_InvalidLayer;
+
+ [SerializeField]
+ bool m_IncludeLayerMaskFields = true;
+
+ [SerializeField]
+ int m_LayerFilter = k_InvalidLayer;
+
+ // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use
+ static readonly StringBuilder k_StringBuilder = new StringBuilder(4096);
+ static readonly List k_Components = new List();
+
+ ///
+ /// Initialize the window
+ ///
+ [MenuItem(k_MenuItemName)]
+ static void Init()
+ {
+ GetWindow(k_WindowTitle).Show();
+ }
+
+ void OnEnable()
+ {
+ minSize = k_MinSize;
+ m_ScanCount = 0;
+ m_ScanProgress = 0;
+ }
+
+ void OnDisable()
+ {
+ m_ScanEnumerator = null;
+ }
+
+ void OnGUI()
+ {
+ EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth;
+
+ if (m_ScanEnumerator == null)
+ {
+ if (GUILayout.Button(k_ScanGUIContent))
+ Scan();
+ }
+ else
+ {
+ if (GUILayout.Button(k_CancelGUIContent))
+ m_ScanEnumerator = null;
+ }
+
+ // If m_LayerToName hasn't been set up, we haven't scanned yet
+ // This dictionary will always at least include the built-in layer names
+ if (m_LayerToName.Count == 0)
+ {
+ EditorGUILayout.HelpBox(k_Instructions, MessageType.Info);
+ GUIUtility.ExitGUI();
+ return;
+ }
+
+ using (new GUILayout.HorizontalScope())
+ {
+ using (new GUILayout.VerticalScope(k_FilterPanelWidthOption))
+ {
+ DrawFilters();
+ }
+
+ using (new GUILayout.VerticalScope())
+ {
+ using (new EditorGUI.DisabledScope(m_LayerWithNoName == k_InvalidLayer))
+ {
+ m_IncludeLayerMaskFields = EditorGUILayout.Toggle(k_IncludeLayerMaskFieldsGUIContent, m_IncludeLayerMaskFields);
+ }
+
+ var count = 0;
+ foreach (var container in m_SceneContainers)
+ {
+ count += container.GetCount(m_LayerFilter, m_IncludeLayerMaskFields);
+ }
+
+ if (count == 0)
+ {
+ GUILayout.Label(k_NoUsages);
+ }
+ else
+ {
+ using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition))
+ {
+ m_FolderTreeScrollPosition = scrollView.scrollPosition;
+ foreach (var container in m_SceneContainers)
+ {
+ container.Draw(m_LayerToName, m_LayerFilter, m_IncludeLayerMaskFields);
+ }
+ }
+ }
+ }
+ }
+
+ if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0)
+ {
+ var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight);
+ EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}");
+ }
+ }
+
+ ///
+ /// Draw a list buttons for filtering based on layer.
+ ///
+ void DrawFilters()
+ {
+ var count = 0;
+ foreach (var container in m_SceneContainers)
+ {
+ count += container.GetCount(includeLayerMaskFields: m_IncludeLayerMaskFields);
+ }
+
+ var style = m_LayerFilter == k_InvalidLayer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"All ({count})", style))
+ m_LayerFilter = k_InvalidLayer;
+
+ using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition))
+ {
+ m_ColorListScrollPosition = scrollView.scrollPosition;
+ foreach (var kvp in m_FilterRows)
+ {
+ var layer = kvp.Key;
+
+ // Skip the default layer
+ if (layer == 0)
+ continue;
+
+ count = 0;
+ if (m_FilterRows.TryGetValue(layer, out var filterRow))
+ count = m_IncludeLayerMaskFields ? filterRow.AllUsers.Count : filterRow.UsersWithoutLayerMasks.Count;
+
+ var layerName = GetLayerNameString(m_LayerToName, layer);
+ style = m_LayerFilter == layer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"{layer}: {layerName} ({count})", style))
+ m_LayerFilter = layer;
+ }
+ }
+ }
+
+ ///
+ /// Scan the project for layer users and populate the data structures for UI.
+ ///
+ void Scan()
+ {
+ m_LayerWithNoName = k_InvalidLayer;
+ m_LayerToName.Clear();
+ m_FilterRows.Clear();
+ for (var i = 0; i < 32; i++)
+ {
+ var layerName = LayerMask.LayerToName(i);
+ if (!string.IsNullOrEmpty(layerName))
+ {
+ m_LayerToName.Add(i, layerName);
+ m_FilterRows.Add(i, new FilterRow());
+ }
+ else
+ {
+ m_LayerWithNoName = i;
+ }
+ }
+
+ // LayerMask field scanning requires at least one layer without a name
+ if (m_LayerWithNoName == k_InvalidLayer)
+ m_IncludeLayerMaskFields = false;
+
+ m_SceneContainers.Clear();
+
+ // If we are in prefab isolation mode, scan the prefab stage instead of the active scene
+ var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
+ if (prefabStage != null)
+ {
+ ScanScene(prefabStage.scene);
+ return;
+ }
+
+ var loadedSceneCount = SceneManager.sceneCount;
+ for (var i = 0; i < loadedSceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (!scene.IsValid())
+ continue;
+
+ ScanScene(scene);
+ }
+ }
+
+ void ScanScene(Scene scene)
+ {
+ var sceneContainer = SceneContainer.CreateIfNecessary(scene, m_FilterRows, m_LayerToName, m_LayerWithNoName);
+ if (sceneContainer != null)
+ m_SceneContainers.Add(sceneContainer);
+ }
+
+ ///
+ /// Get or create a for a given layer value.
+ ///
+ /// Dictionary of FilterRow objects for counting usages per layer.
+ /// The layer value to use for this row.
+ /// The row for the layer value.
+ static FilterRow GetOrCreateFilterRowForLayer(SortedDictionary filterRows, int layer)
+ {
+ if (filterRows.TryGetValue(layer, out var filterRow))
+ return filterRow;
+
+ filterRow = new FilterRow();
+ filterRows[layer] = filterRow;
+ return filterRow;
+ }
+
+ static void AppendLayerNameList(StringBuilder stringBuilder, int[] usages, int[] usagesWithoutLayerMasks,
+ Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true)
+ {
+ if (layerFilter >= 0)
+ {
+ stringBuilder.Append(GetLayerNameString(layerToName, layerFilter));
+ return;
+ }
+
+ AppendLayerNameList(stringBuilder, includeLayerMaskFields ? usages : usagesWithoutLayerMasks, layerToName, layerFilter);
+ }
+
+ static void AppendLayerNameList(StringBuilder stringBuilder, int[] layers, Dictionary layerToName, int layerFilter = k_InvalidLayer)
+ {
+ if (layerFilter >= 0)
+ {
+ stringBuilder.Append(GetLayerNameString(layerToName, layerFilter));
+ return;
+ }
+
+ var length = layers.Length;
+ if (length == 0)
+ return;
+
+ var lengthMinusOne = length - 1;
+ for (var i = 0; i < lengthMinusOne; i++)
+ {
+ stringBuilder.Append(GetLayerNameString(layerToName, layers[i]));
+ stringBuilder.Append(", ");
+ }
+
+ stringBuilder.Append(GetLayerNameString(layerToName, layers[lengthMinusOne]));
+ }
+
+ static string GetLayerNameString(Dictionary layerToName, int layer)
+ {
+ layerToName.TryGetValue(layer, out var layerName);
+ if (string.IsNullOrEmpty(layerName))
+ layerName = layer.ToString();
+
+ return layerName;
+ }
+
+ static void IncrementCountForLayer(int layer, SortedDictionary countPerLayer)
+ {
+ countPerLayer.TryGetValue(layer, out var count);
+ count++;
+ countPerLayer[layer] = count;
+ }
+
+ static void AggregateCountPerLayer(SortedDictionary source, SortedDictionary destination)
+ {
+ if (source == null)
+ return;
+
+ foreach (var kvp in source)
+ {
+ var layer = kvp.Key;
+ destination.TryGetValue(layer, out var count);
+ count += kvp.Value;
+ destination[layer] = count;
+ }
+ }
+ }
+}
diff --git a/Editor/LayerUsers/SceneLayerUsers.cs.meta b/Editor/LayerUsers/SceneLayerUsers.cs.meta
new file mode 100644
index 0000000..dd93ad6
--- /dev/null
+++ b/Editor/LayerUsers/SceneLayerUsers.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 55d2b3e96ab4fd94b8d3195eff448abf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/LayerUsers/SceneTagUsers.cs b/Editor/LayerUsers/SceneTagUsers.cs
new file mode 100644
index 0000000..6367a36
--- /dev/null
+++ b/Editor/LayerUsers/SceneTagUsers.cs
@@ -0,0 +1,600 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEditor.Experimental.SceneManagement;
+using UnityEditorInternal;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace Unity.Labs.SuperScience
+{
+ ///
+ /// Scans all loaded scenes for GameObjects which use custom tags and displays the results in an EditorWindow.
+ ///
+ public class SceneTagUsers : EditorWindow
+ {
+ class SceneContainer
+ {
+ const string k_UntitledSceneName = "Untitled";
+
+ readonly string m_SceneName;
+ readonly List m_Roots;
+ readonly int m_TotalCount;
+ readonly SortedDictionary m_CountPerTag;
+ readonly string[] m_Usages;
+
+ bool m_Expanded;
+
+ SceneContainer(string sceneName, List roots)
+ {
+ m_SceneName = sceneName;
+ m_Roots = roots;
+ m_CountPerTag = new SortedDictionary();
+ foreach (var container in roots)
+ {
+ var gameObject = container.GameObject;
+ var tag = gameObject.tag;
+ if (!gameObject.CompareTag(k_UntaggedString))
+ {
+ m_TotalCount++;
+ IncrementCountForTag(tag, m_CountPerTag);
+ }
+
+ m_TotalCount += container.TotalUsagesInChildren;
+ AggregateCountPerTag(container.UsagesInChildrenPerTag, m_CountPerTag);
+ }
+
+ m_Usages = m_CountPerTag.Keys.ToArray();
+ }
+
+ public void Draw(string tagFilter)
+ {
+ var count = GetCount(tagFilter);
+ if (count == 0)
+ return;
+
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append(m_SceneName);
+ k_StringBuilder.Append(" (");
+ k_StringBuilder.Append(count.ToString());
+ k_StringBuilder.Append(") {");
+ AppendTagNameList(k_StringBuilder, m_Usages, tagFilter);
+ k_StringBuilder.Append("}");
+ var label = k_StringBuilder.ToString();
+ var wasExpanded = m_Expanded;
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ {
+ foreach (var gameObjectContainer in m_Roots)
+ {
+ gameObjectContainer.SetExpandedRecursively(m_Expanded);
+ }
+ }
+
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var gameObjectContainer in m_Roots)
+ {
+ gameObjectContainer.Draw(tagFilter);
+ }
+ }
+ }
+
+ public static SceneContainer CreateIfNecessary(Scene scene, SortedDictionary> filterRows)
+ {
+ var sceneName = scene.name;
+ if (string.IsNullOrEmpty(sceneName))
+ sceneName = k_UntitledSceneName;
+
+ List roots = null;
+ foreach (var gameObject in scene.GetRootGameObjects())
+ {
+ var rootContainer = GameObjectContainer.CreateIfNecessary(gameObject, filterRows);
+ if (rootContainer != null)
+ {
+ roots ??= new List();
+ roots.Add(rootContainer);
+ }
+ }
+
+ return roots != null ? new SceneContainer(sceneName, roots) : null;
+ }
+
+ public int GetCount(string tagFilter = null)
+ {
+ var unfiltered = string.IsNullOrEmpty(tagFilter);
+ if (unfiltered)
+ return m_TotalCount;
+
+ m_CountPerTag.TryGetValue(tagFilter, out var count);
+ return count;
+ }
+ }
+
+ ///
+ /// Tree structure for GameObject scan results
+ /// When the Scan method encounters a GameObject in a scene or a prefab in the project, we initialize one of
+ /// these using the GameObject as an argument. This scans the object and its components/children, retaining
+ /// the results for display in the GUI. The window calls into these helper objects to draw them, as well.
+ ///
+ class GameObjectContainer
+ {
+ readonly GameObject m_GameObject;
+ readonly List m_Children;
+
+ //TODO: Rename Users -> Usages
+ readonly int m_TotalUsagesInChildren;
+ readonly SortedDictionary m_UsagesInChildrenPerTag;
+
+ readonly string[] m_Usages;
+
+ bool m_Expanded;
+
+ public GameObject GameObject { get { return m_GameObject; } }
+ public int TotalUsagesInChildren => m_TotalUsagesInChildren;
+ public SortedDictionary UsagesInChildrenPerTag => m_UsagesInChildrenPerTag;
+
+ // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use
+ static readonly SortedSet k_Usages = new SortedSet();
+
+ GameObjectContainer(GameObject gameObject, List children)
+ {
+ m_GameObject = gameObject;
+ m_Children = children;
+ k_Usages.Clear();
+
+ var tag = gameObject.tag;
+ if (!gameObject.CompareTag(k_UntaggedString))
+ k_Usages.Add(tag);
+
+ if (children != null)
+ {
+ m_UsagesInChildrenPerTag = new SortedDictionary();
+ foreach (var container in children)
+ {
+ AggregateCount(container, ref m_TotalUsagesInChildren);
+ }
+
+ k_Usages.UnionWith(m_UsagesInChildrenPerTag.Keys);
+ }
+
+ m_Usages = k_Usages.ToArray();
+ }
+
+ ///
+ /// Initialize a GameObjectContainer to represent the given GameObject
+ /// This will scan the component for missing references and retain the information for display in
+ /// the given window.
+ ///
+ /// The GameObject to scan for missing references
+ /// Dictionary of HashSet>GameObject< objects for counting usages per tag.
+ public static GameObjectContainer CreateIfNecessary(GameObject gameObject, SortedDictionary> filterRows)
+ {
+ List children = null;
+ foreach (Transform child in gameObject.transform)
+ {
+ var childContainer = CreateIfNecessary(child.gameObject, filterRows);
+ if (childContainer != null)
+ {
+ children ??= new List();
+ children.Add(childContainer);
+ }
+ }
+
+ var tag = gameObject.tag;
+ var isTagUser = !gameObject.CompareTag(k_UntaggedString);
+ if (isTagUser || children != null)
+ {
+ if (isTagUser)
+ {
+ var hashSet = GetOrCreateHashSetForTag(filterRows, tag);
+ hashSet.Add(gameObject);
+ }
+
+ return new GameObjectContainer(gameObject, children);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Draw tag usage information for this GameObjectContainer
+ ///
+ public void Draw(string tagFilter = null)
+ {
+ var tag = m_GameObject.tag;
+ var isTagUser = string.IsNullOrEmpty(tagFilter) ? !m_GameObject.CompareTag(k_UntaggedString) : m_GameObject.CompareTag(tagFilter);
+
+ var childCount = GetChildCount(tagFilter);
+ var hasChildren = childCount > 0;
+ if (!(isTagUser || hasChildren))
+ return;
+
+ var label = GetLabel(tagFilter, childCount, isTagUser, tag);
+ var wasExpanded = m_Expanded;
+ if (hasChildren)
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true);
+ else
+ EditorGUILayout.LabelField(label);
+
+
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ {
+ if (m_Children != null)
+ {
+ foreach (var gameObjectContainer in m_Children)
+ {
+ gameObjectContainer.SetExpandedRecursively(m_Expanded);
+ }
+ }
+ }
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ if (isTagUser)
+ EditorGUILayout.ObjectField(m_GameObject, typeof(GameObject), true);
+
+ if (!m_Expanded)
+ return;
+
+ if (hasChildren && m_Children != null)
+ {
+ foreach (var child in m_Children)
+ {
+ child.Draw(tagFilter);
+ }
+ }
+ }
+ }
+
+ string GetLabel(string tagFilter, int count, bool isTagUser, string tag)
+ {
+ k_StringBuilder.Length = 0;
+ k_StringBuilder.Append(m_GameObject.name);
+ k_StringBuilder.Append(" (");
+ k_StringBuilder.Append(count.ToString());
+ if (isTagUser)
+ {
+ k_StringBuilder.Append(") - Tag: ");
+ k_StringBuilder.Append(tag);
+ k_StringBuilder.Append(" {");
+ }
+ else
+ {
+ k_StringBuilder.Append(") {");
+ }
+
+ AppendTagNameList(k_StringBuilder, m_Usages, tagFilter);
+ k_StringBuilder.Append("}");
+
+ return k_StringBuilder.ToString();
+ }
+
+ ///
+ /// Set the expanded state of this object and all of its children
+ ///
+ /// Whether this object should be expanded in the GUI
+ public void SetExpandedRecursively(bool expanded)
+ {
+ m_Expanded = expanded;
+ if (m_Children != null)
+ {
+ foreach (var child in m_Children)
+ {
+ child.SetExpandedRecursively(expanded);
+ }
+ }
+ }
+
+ int GetChildCount(string tagFilter = null)
+ {
+ if (m_Children == null)
+ return 0;
+
+ var unfiltered = string.IsNullOrEmpty(tagFilter);
+ if (unfiltered)
+ return m_TotalUsagesInChildren;
+
+ m_UsagesInChildrenPerTag.TryGetValue(tagFilter, out var totalCount);
+ return totalCount;
+ }
+
+ void AggregateCount(GameObjectContainer child, ref int usagesInChildren)
+ {
+ var gameObject = child.m_GameObject;
+ var tag = gameObject.tag;
+ if (!gameObject.CompareTag(k_UntaggedString))
+ {
+ usagesInChildren++;
+ IncrementCountForTag(tag, m_UsagesInChildrenPerTag);
+ }
+
+ usagesInChildren += child.m_TotalUsagesInChildren;
+ AggregateCountPerTag(child.m_UsagesInChildrenPerTag, m_UsagesInChildrenPerTag);
+ }
+ }
+
+ static class Styles
+ {
+ internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft,
+ fontStyle = FontStyle.Bold
+ };
+
+ internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button)
+ {
+ alignment = TextAnchor.MiddleLeft
+ };
+ }
+
+ const string k_MenuItemName = "Window/SuperScience/Scene Tag Users";
+ const string k_WindowTitle = "Scene Tag Users";
+ const string k_NoUsages = "No scene objects using any custom tags";
+ const int k_FilterPanelWidth = 180;
+ const int k_ObjectFieldWidth = 150;
+ const string k_Instructions = "Click the Scan button to scan your project for users of custom tags. WARNING: " +
+ "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor.";
+ const int k_ProgressBarHeight = 15;
+ const string k_UntaggedString = "Untagged";
+
+ static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the loaded scenes for usages of custom tags");
+ static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan");
+ static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth);
+ static readonly Vector2 k_MinSize = new Vector2(400, 200);
+ static readonly HashSet k_BuiltInTags = new HashSet
+ {
+ k_UntaggedString,
+ "Respawn",
+ "Finish",
+ "EditorOnly",
+ "MainCamera",
+ "Player",
+ "GameController",
+ };
+
+ Vector2 m_ColorListScrollPosition;
+ Vector2 m_FolderTreeScrollPosition;
+ readonly List m_SceneContainers = new List();
+ readonly SortedDictionary> m_FilterRows = new SortedDictionary>();
+ int m_ScanCount;
+ int m_ScanProgress;
+ IEnumerator m_ScanEnumerator;
+
+ [NonSerialized]
+ bool m_Scanned;
+
+ [SerializeField]
+ string m_TagFilter;
+
+ // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use
+ static readonly StringBuilder k_StringBuilder = new StringBuilder(4096);
+
+ ///
+ /// Initialize the window
+ ///
+ [MenuItem(k_MenuItemName)]
+ static void Init()
+ {
+ GetWindow(k_WindowTitle).Show();
+ }
+
+ void OnEnable()
+ {
+ minSize = k_MinSize;
+ m_ScanCount = 0;
+ m_ScanProgress = 0;
+ }
+
+ void OnDisable()
+ {
+ m_ScanEnumerator = null;
+ }
+
+ void OnGUI()
+ {
+ EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth;
+
+ if (m_ScanEnumerator == null)
+ {
+ if (GUILayout.Button(k_ScanGUIContent))
+ Scan();
+ }
+ else
+ {
+ if (GUILayout.Button(k_CancelGUIContent))
+ m_ScanEnumerator = null;
+ }
+
+ if (!m_Scanned)
+ {
+ EditorGUILayout.HelpBox(k_Instructions, MessageType.Info);
+ GUIUtility.ExitGUI();
+ return;
+ }
+
+ using (new GUILayout.HorizontalScope())
+ {
+ using (new GUILayout.VerticalScope(k_FilterPanelWidthOption))
+ {
+ DrawFilters();
+ }
+
+ using (new GUILayout.VerticalScope())
+ {
+ var count = 0;
+ foreach (var container in m_SceneContainers)
+ {
+ count += container.GetCount(m_TagFilter);
+ }
+
+ if (count == 0)
+ {
+ GUILayout.Label(k_NoUsages);
+ }
+ else
+ {
+ using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition))
+ {
+ m_FolderTreeScrollPosition = scrollView.scrollPosition;
+ foreach (var container in m_SceneContainers)
+ {
+ container.Draw(m_TagFilter);
+ }
+ }
+ }
+ }
+ }
+
+ if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0)
+ {
+ var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight);
+ EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}");
+ }
+ }
+
+ ///
+ /// Draw a list buttons for filtering based on tag.
+ ///
+ void DrawFilters()
+ {
+ var count = 0;
+ foreach (var container in m_SceneContainers)
+ {
+ count += container.GetCount();
+ }
+
+ var style = string.IsNullOrEmpty(m_TagFilter) ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"All ({count})", style))
+ m_TagFilter = null;
+
+ using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition))
+ {
+ m_ColorListScrollPosition = scrollView.scrollPosition;
+ foreach (var kvp in m_FilterRows)
+ {
+ var tag = kvp.Key;
+ count = 0;
+ if (m_FilterRows.TryGetValue(tag, out var hashSet))
+ count = hashSet.Count;
+
+ style = m_TagFilter == tag ? Styles.ActiveFilterButton : Styles.InactiveFilterButton;
+ if (GUILayout.Button($"{tag}: ({count})", style))
+ m_TagFilter = tag;
+ }
+ }
+ }
+
+ ///
+ /// Scan the project for tag usages and populate the data structures for UI.
+ ///
+ void Scan()
+ {
+ m_Scanned = true;
+ m_SceneContainers.Clear();
+ m_FilterRows.Clear();
+
+ // Add all tags to FilterRows to include tags with no users
+ foreach (var tag in InternalEditorUtility.tags)
+ {
+ if (k_BuiltInTags.Contains(tag))
+ continue;
+
+ m_FilterRows[tag] = new HashSet();
+ }
+
+ // If we are in prefab isolation mode, scan the prefab stage instead of the active scene
+ var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
+ if (prefabStage != null)
+ {
+ ScanScene(prefabStage.scene);
+ return;
+ }
+
+ var loadedSceneCount = SceneManager.sceneCount;
+ for (var i = 0; i < loadedSceneCount; i++)
+ {
+ var scene = SceneManager.GetSceneAt(i);
+ if (!scene.IsValid())
+ continue;
+
+ ScanScene(scene);
+ }
+ }
+
+ void ScanScene(Scene scene)
+ {
+ var sceneContainer = SceneContainer.CreateIfNecessary(scene, m_FilterRows);
+ if (sceneContainer != null)
+ m_SceneContainers.Add(sceneContainer);
+ }
+
+ ///
+ /// Get or create a HashSet>GameObject< for a given tag.
+ ///
+ /// Dictionary of HashSet>GameObject< objects for counting usages per tag.
+ /// The tag to use for this row.
+ /// The HashSet>GameObject< for the tag.
+ static HashSet GetOrCreateHashSetForTag(SortedDictionary> filterRows, string tag)
+ {
+ if (filterRows.TryGetValue(tag, out var filterRow))
+ return filterRow;
+
+ filterRow = new HashSet();
+ filterRows[tag] = filterRow;
+ return filterRow;
+ }
+
+ static void AppendTagNameList(StringBuilder stringBuilder, string[] tags, string tagFilter = null)
+ {
+ if (!string.IsNullOrEmpty(tagFilter))
+ {
+ stringBuilder.Append(tagFilter);
+ return;
+ }
+
+ var length = tags.Length;
+ if (length == 0)
+ return;
+
+ var lengthMinusOne = length - 1;
+ for (var i = 0; i < lengthMinusOne; i++)
+ {
+ stringBuilder.Append(tags[i]);
+ stringBuilder.Append(", ");
+ }
+
+ stringBuilder.Append(tags[lengthMinusOne]);
+ }
+
+ static void IncrementCountForTag(string tag, SortedDictionary countPerTag)
+ {
+ countPerTag.TryGetValue(tag, out var count);
+ count++;
+ countPerTag[tag] = count;
+ }
+
+ static void AggregateCountPerTag(SortedDictionary source, SortedDictionary destination)
+ {
+ if (source == null)
+ return;
+
+ foreach (var kvp in source)
+ {
+ var tag = kvp.Key;
+ destination.TryGetValue(tag, out var count);
+ count += kvp.Value;
+ destination[tag] = count;
+ }
+ }
+ }
+}
diff --git a/Editor/LayerUsers/SceneTagUsers.cs.meta b/Editor/LayerUsers/SceneTagUsers.cs.meta
new file mode 100644
index 0000000..c4bf978
--- /dev/null
+++ b/Editor/LayerUsers/SceneTagUsers.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e30d9050684006c45ba69e4fa9d43e51
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/MissingReferences/MissingProjectReferences.cs b/Editor/MissingReferences/MissingProjectReferences.cs
index cf4f928..c5efb4f 100644
--- a/Editor/MissingReferences/MissingProjectReferences.cs
+++ b/Editor/MissingReferences/MissingProjectReferences.cs
@@ -33,12 +33,20 @@ class AssetContainer : MissingReferencesContainer
const string k_SubAssetsLabelFormat = "Sub-assets: {0}";
readonly UnityObject m_Object;
- bool m_SubAssetsVisible;
- public readonly List SubAssets = new List();
- public readonly List PropertiesWithMissingReferences = new List();
+ readonly List m_SubAssets;
+ readonly List m_PropertiesWithMissingReferences;
+
+ bool m_SubAssetsExpanded;
public override UnityObject Object => m_Object;
+ AssetContainer(UnityObject unityObject, List subAssets, List propertiesWithMissingReferences)
+ {
+ m_Object = unityObject;
+ m_SubAssets = subAssets;
+ m_PropertiesWithMissingReferences = propertiesWithMissingReferences;
+ }
+
///
/// Initialize an AssetContainer to represent the given UnityObject
/// This will scan the object for missing references and retain the information for display in
@@ -47,33 +55,47 @@ class AssetContainer : MissingReferencesContainer
/// The main UnityObject for this asset
/// The path to this asset, for gathering sub-assets
/// User-configurable options for this view
- public AssetContainer(UnityObject unityObject, string path, Options options)
+ public static AssetContainer CreateIfNecessary(UnityObject unityObject, string path, Options options)
{
- m_Object = unityObject;
- CheckForMissingReferences(unityObject, PropertiesWithMissingReferences, options);
+ var missingReferences = CheckForMissingReferences(unityObject, options);
// Collect any sub-asset references
+ List subAssets = null;
foreach (var asset in AssetDatabase.LoadAllAssetRepresentationsAtPath(path))
{
if (asset is GameObject prefab)
{
- var gameObjectContainer = new GameObjectContainer(prefab, options);
- if (gameObjectContainer.Count > 0)
- SubAssets.Add(gameObjectContainer);
+ var gameObjectContainer = GameObjectContainer.CreateIfNecessary(prefab, options);
+ if (gameObjectContainer != null)
+ {
+ subAssets ??= new List();
+ subAssets.Add(gameObjectContainer);
+ }
}
else
{
- var assetContainer = new AssetContainer(asset, options);
- if (assetContainer.PropertiesWithMissingReferences.Count > 0)
- SubAssets.Add(assetContainer);
+ var assetContainer = CreateIfNecessary(asset, options);
+ if (assetContainer != null)
+ {
+ subAssets ??= new List();
+ subAssets.Add(assetContainer);
+ }
}
}
+
+ if (missingReferences != null || subAssets != null)
+ return new AssetContainer(unityObject, subAssets, missingReferences);
+
+ return null;
}
- AssetContainer(UnityObject unityObject, Options options)
+ static AssetContainer CreateIfNecessary(UnityObject unityObject, Options options)
{
- m_Object = unityObject;
- CheckForMissingReferences(unityObject, PropertiesWithMissingReferences, options);
+ var missingReferences = CheckForMissingReferences(unityObject, options);
+ if (missingReferences != null)
+ return new AssetContainer(unityObject, null, missingReferences);
+
+ return null;
}
///
@@ -83,33 +105,36 @@ public override void Draw()
{
using (new EditorGUI.IndentLevelScope())
{
- DrawPropertiesWithMissingReferences(PropertiesWithMissingReferences);
+ DrawPropertiesWithMissingReferences(m_PropertiesWithMissingReferences);
+
+ if (m_SubAssets == null)
+ return;
- var count = SubAssets.Count;
+ var count = m_SubAssets.Count;
if (count == 0)
return;
- m_SubAssetsVisible = EditorGUILayout.Foldout(m_SubAssetsVisible, string.Format(k_SubAssetsLabelFormat, count));
- if (!m_SubAssetsVisible)
+ m_SubAssetsExpanded = EditorGUILayout.Foldout(m_SubAssetsExpanded, string.Format(k_SubAssetsLabelFormat, count));
+ if (!m_SubAssetsExpanded)
return;
- foreach (var asset in SubAssets)
+ foreach (var asset in m_SubAssets)
{
asset.Draw();
}
}
}
- public override void SetVisibleRecursively(bool visible)
+ public override void SetExpandedRecursively(bool expanded)
{
- m_SubAssetsVisible = visible;
+ m_SubAssetsExpanded = expanded;
}
}
const string k_LabelFormat = "{0}: {1}";
readonly SortedDictionary m_Subfolders = new SortedDictionary();
readonly List m_Assets = new List();
- bool m_Visible;
+ bool m_Expanded;
internal List Assets => m_Assets;
internal SortedDictionary Subfolders => m_Subfolders;
@@ -142,14 +167,14 @@ public void AddAssetAtPath(string path, Options options)
// Model prefabs may contain materials which we want to scan. The "real prefab" as a sub-asset
if (asset is GameObject prefab && PrefabUtility.GetPrefabAssetType(asset) != PrefabAssetType.Model)
{
- var gameObjectContainer = new GameObjectContainer(prefab, options);
- if (gameObjectContainer.Count > 0)
+ var gameObjectContainer = GameObjectContainer.CreateIfNecessary(prefab, options);
+ if (gameObjectContainer != null)
GetOrCreateFolderForAssetPath(path).m_Assets.Add(gameObjectContainer);
}
else
{
- var assetContainer = new AssetContainer(asset, path, options);
- if (assetContainer.PropertiesWithMissingReferences.Count > 0 || assetContainer.SubAssets.Count > 0)
+ var assetContainer = AssetContainer.CreateIfNecessary(asset, path, options);
+ if (assetContainer != null)
GetOrCreateFolderForAssetPath(path).m_Assets.Add(assetContainer);
}
}
@@ -193,14 +218,14 @@ Folder GetOrCreateFolderForAssetPath(string path)
/// The name of the folder
public void Draw(string name)
{
- var wasVisible = m_Visible;
- m_Visible = EditorGUILayout.Foldout(m_Visible, string.Format(k_LabelFormat, name, Count), true);
+ var wasExpanded = m_Expanded;
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, string.Format(k_LabelFormat, name, Count), true);
- // Hold alt to apply visibility state to all children (recursively)
- if (m_Visible != wasVisible && Event.current.alt)
- SetVisibleRecursively(m_Visible);
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ SetExpandedRecursively(m_Expanded);
- if (!m_Visible)
+ if (!m_Expanded)
return;
using (new EditorGUI.IndentLevelScope())
@@ -223,20 +248,20 @@ public void Draw(string name)
}
///
- /// Set the visibility state of this folder, its contents and their children and all of its subfolders and their contents and children
+ /// Set the expanded state of this folder, its contents and their children and all of its subfolders and their contents and children
///
- /// Whether this object and its children should be visible in the GUI
- void SetVisibleRecursively(bool visible)
+ /// Whether this object should be expanded in the GUI
+ void SetExpandedRecursively(bool expanded)
{
- m_Visible = visible;
+ m_Expanded = expanded;
foreach (var asset in m_Assets)
{
- asset.SetVisibleRecursively(visible);
+ asset.SetExpandedRecursively(expanded);
}
foreach (var kvp in m_Subfolders)
{
- kvp.Value.SetVisibleRecursively(visible);
+ kvp.Value.SetExpandedRecursively(expanded);
}
}
diff --git a/Editor/MissingReferences/MissingReferencesWindow.cs b/Editor/MissingReferences/MissingReferencesWindow.cs
index 018337e..95114cb 100644
--- a/Editor/MissingReferences/MissingReferencesWindow.cs
+++ b/Editor/MissingReferences/MissingReferencesWindow.cs
@@ -4,6 +4,7 @@
using System.Reflection;
using UnityEditor;
using UnityEngine;
+using UnityEngine.SceneManagement;
using UnityObject = UnityEngine.Object;
namespace Unity.Labs.SuperScience
@@ -20,7 +21,75 @@ protected abstract class MissingReferencesContainer
{
public abstract void Draw();
public abstract UnityObject Object { get; }
- public abstract void SetVisibleRecursively(bool visible);
+ public abstract void SetExpandedRecursively(bool expanded);
+ }
+
+ protected class SceneContainer
+ {
+ const string k_UntitledSceneName = "Untitled";
+
+ readonly string m_SceneName;
+ readonly List m_Roots;
+ readonly int m_Count;
+
+ bool m_Expanded;
+
+ public List Roots => m_Roots;
+
+ public SceneContainer(string sceneName, List roots, int count)
+ {
+ m_SceneName = sceneName;
+ m_Roots = roots;
+ m_Count = count;
+ }
+
+ public void Draw()
+ {
+ var wasExpanded = m_Expanded;
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, $"{m_SceneName} ({m_Count})", true, Styles.RichTextFoldout);
+
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ {
+ foreach (var gameObjectContainer in m_Roots)
+ {
+ gameObjectContainer.SetExpandedRecursively(m_Expanded);
+ }
+ }
+
+ if (!m_Expanded)
+ return;
+
+ using (new EditorGUI.IndentLevelScope())
+ {
+ foreach (var gameObjectContainer in Roots)
+ {
+ gameObjectContainer.Draw();
+ }
+ }
+ }
+
+ public static SceneContainer CreateIfNecessary(Scene scene, Options options)
+ {
+ var sceneName = scene.name;
+ if (string.IsNullOrEmpty(sceneName))
+ sceneName = k_UntitledSceneName;
+
+ var count = 0;
+ List roots = null;
+ foreach (var gameObject in scene.GetRootGameObjects())
+ {
+ var rootContainer = GameObjectContainer.CreateIfNecessary(gameObject, options);
+ if (rootContainer != null)
+ {
+ roots ??= new List();
+ roots.Add(rootContainer);
+ count += rootContainer.Count;
+ }
+ }
+
+ return roots != null ? new SceneContainer(sceneName, roots, count) : null;
+ }
}
///
@@ -40,19 +109,14 @@ internal class ComponentContainer
const string k_MissingScriptLabel = "Missing Script!";
readonly Component m_Component;
- public readonly List PropertiesWithMissingReferences = new List();
+ readonly List m_PropertiesWithMissingReferences;
- ///
- /// Initialize a ComponentContainer to represent the given Component
- /// This will scan the component for missing references and retain the information for display in
- /// the given window.
- ///
- /// The Component to scan for missing references
- /// User-configurable options for this view
- public ComponentContainer(Component component, Options options)
+ public List PropertiesWithMissingReferences => m_PropertiesWithMissingReferences;
+
+ public ComponentContainer(Component component, List propertiesWithMissingReferences)
{
m_Component = component;
- CheckForMissingReferences(component, PropertiesWithMissingReferences, options);
+ m_PropertiesWithMissingReferences = propertiesWithMissingReferences;
}
///
@@ -70,9 +134,28 @@ public void Draw()
return;
}
- DrawPropertiesWithMissingReferences(PropertiesWithMissingReferences);
+ DrawPropertiesWithMissingReferences(m_PropertiesWithMissingReferences);
}
}
+
+ ///
+ /// Initialize a ComponentContainer to represent the given Component
+ /// This will scan the component for missing references and retain the information for display in
+ /// the given window.
+ ///
+ /// The Component to scan for missing references
+ /// User-configurable options for this view
+ public static ComponentContainer CreateIfNecessary(Component component, Options options)
+ {
+ var missingReferences = CheckForMissingReferences(component, options);
+
+ // If the component is null, this is a missing script. Otherwise, if there are missing references,
+ // create and return a container
+ if (component == null || missingReferences != null)
+ return new ComponentContainer(component, missingReferences);
+
+ return null;
+ }
}
const int k_PingButtonWidth = 35;
@@ -83,24 +166,38 @@ public void Draw()
const string k_ChildrenGroupLabelFormat = "Children: {0}";
readonly GameObject m_GameObject;
- readonly List m_Children = new List();
- readonly List m_Components = new List();
- internal List Children => m_Children;
-
- bool m_IsMissingPrefab;
- int m_MissingReferencesInChildren;
- int m_MissingReferencesInComponents;
+ readonly bool m_IsMissingPrefab;
+ readonly List m_Children;
+ readonly List m_Components;
+ readonly int m_MissingReferencesInChildren;
+ readonly int m_MissingReferencesInComponents;
+ readonly int m_MissingReferences;
internal bool HasMissingReferences => m_IsMissingPrefab || m_MissingReferencesInComponents > 0;
+ internal List Children => m_Children;
- bool m_Visible;
+ bool m_Expanded;
bool m_ShowComponents;
bool m_ShowChildren;
- public int Count { get; private set; }
public override UnityObject Object => m_GameObject;
+ public int Count => m_MissingReferences;
- public GameObjectContainer() { }
+ // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use
+ static readonly List k_Components = new List();
+
+ public GameObjectContainer(GameObject gameObject, bool isMissingPrefab,
+ int missingReferencesInChildren, int missingReferencesInComponents,
+ List children, List components)
+ {
+ m_GameObject = gameObject;
+ m_IsMissingPrefab = isMissingPrefab;
+ m_MissingReferencesInChildren = missingReferencesInChildren;
+ m_MissingReferencesInComponents = missingReferencesInComponents;
+ m_Children = children;
+ m_Components = components;
+ m_MissingReferences = missingReferencesInComponents + missingReferencesInChildren;
+ }
///
/// Initialize a GameObjectContainer to represent the given GameObject
@@ -109,60 +206,66 @@ public GameObjectContainer() { }
///
/// The GameObject to scan for missing references
/// User-configurable options for this view
- internal GameObjectContainer(GameObject gameObject, Options options)
+ public static GameObjectContainer CreateIfNecessary(GameObject gameObject, Options options)
{
- m_GameObject = gameObject;
-
+ var isMissingPrefab = false;
if (PrefabUtility.IsAnyPrefabInstanceRoot(gameObject))
- m_IsMissingPrefab = PrefabUtility.IsPrefabAssetMissing(gameObject);
+ isMissingPrefab = PrefabUtility.IsPrefabAssetMissing(gameObject);
+
+ List components = null;
+ var missingReferencesInComponents = 0;
+ var missingReferencesInChildren = 0;
- foreach (var component in gameObject.GetComponents())
+ // GetComponents will clear the list, so we don't have to
+ gameObject.GetComponents(k_Components);
+ foreach (var component in k_Components)
{
- var container = new ComponentContainer(component, options);
+ var container = ComponentContainer.CreateIfNecessary(component, options);
+ if (container == null)
+ continue;
+
if (component == null)
{
- m_Components.Add(container);
- Count++;
- m_MissingReferencesInComponents++;
+ components ??= new List();
+ components.Add(container);
+ missingReferencesInComponents++;
continue;
}
- var count = container.PropertiesWithMissingReferences.Count;
+ var missingReferences = container.PropertiesWithMissingReferences;
+ if (missingReferences == null)
+ continue;
+
+ var count = missingReferences.Count;
if (count > 0)
{
- m_Components.Add(container);
- Count += count;
- m_MissingReferencesInComponents += count;
+ components ??= new List();
+ components.Add(container);
+ missingReferencesInComponents += count;
}
}
+ List children = null;
foreach (Transform child in gameObject.transform)
{
- AddChild(child.gameObject, options);
+ var childContainer = CreateIfNecessary(child.gameObject, options);
+ if (childContainer != null)
+ {
+ children ??= new List();
+ children.Add(childContainer);
+ missingReferencesInChildren += childContainer.m_MissingReferences;
+ if (childContainer.m_IsMissingPrefab)
+ missingReferencesInChildren++;
+ }
}
- }
- ///
- /// Add a child GameObject to this GameObjectContainer
- ///
- /// The GameObject to scan for missing references
- /// User-configurable options for this view
- public void AddChild(GameObject gameObject, Options options)
- {
- var child = new GameObjectContainer(gameObject, options);
- var childCount = child.Count;
- Count += childCount;
- m_MissingReferencesInChildren += childCount;
-
- var isMissingPrefab = child.m_IsMissingPrefab;
- if (isMissingPrefab)
+ if (isMissingPrefab || missingReferencesInComponents > 0 || missingReferencesInChildren > 0)
{
- m_MissingReferencesInChildren++;
- Count++;
+ return new GameObjectContainer(gameObject, isMissingPrefab, missingReferencesInChildren,
+ missingReferencesInComponents, children, components);
}
- if (childCount > 0 || isMissingPrefab)
- m_Children.Add(child);
+ return null;
}
///
@@ -170,13 +273,16 @@ public void AddChild(GameObject gameObject, Options options)
///
public override void Draw()
{
- var wasVisible = m_Visible;
- var label = string.Format(k_LabelFormat, m_GameObject ? m_GameObject.name : "Scene Root", Count);
+ if (m_GameObject == null)
+ return;
+
+ var wasExpanded = m_Expanded;
+ var label = string.Format(k_LabelFormat, m_GameObject.name, m_MissingReferences);
if (m_IsMissingPrefab)
label = string.Format(k_MissingPrefabLabelFormat, label);
// If this object has 0 missing references but is being drawn, it is a missing prefab with no overrides
- if (Count == 0)
+ if (m_MissingReferences == 0)
{
using (new EditorGUILayout.HorizontalScope())
{
@@ -188,24 +294,17 @@ public override void Draw()
return;
}
- m_Visible = EditorGUILayout.Foldout(m_Visible, label, true, Styles.RichTextFoldout);
+ m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true, Styles.RichTextFoldout);
- // Hold alt to apply visibility state to all children (recursively)
- if (m_Visible != wasVisible && Event.current.alt)
- SetVisibleRecursively(m_Visible);
+ // Hold alt to apply expanded state to all children (recursively)
+ if (m_Expanded != wasExpanded && Event.current.alt)
+ SetExpandedRecursively(m_Expanded);
- if (!m_Visible)
+ if (!m_Expanded)
return;
using (new EditorGUI.IndentLevelScope())
{
- // If m_GameObject is null, this is a scene
- if (m_GameObject == null)
- {
- DrawChildren();
- return;
- }
-
if (m_MissingReferencesInComponents > 0)
{
EditorGUILayout.ObjectField(m_GameObject, typeof(GameObject), true);
@@ -231,39 +330,44 @@ public override void Draw()
{
using (new EditorGUI.IndentLevelScope())
{
- DrawChildren();
+ foreach (var child in m_Children)
+ {
+ var childObject = child.m_GameObject;
+
+ // Check for null in case of destroyed object
+ if (childObject)
+ child.Draw();
+ }
}
}
}
}
-
- void DrawChildren()
- {
- foreach (var child in m_Children)
- {
- var childObject = child.m_GameObject;
-
- // Check for null in case of destroyed object
- if (childObject)
- child.Draw();
- }
- }
}
///
- /// Set the visibility state of this object and all of its children
+ /// Set the expanded state of this object and all of its children
///
- /// Whether this object and its children should be visible in the GUI
- public override void SetVisibleRecursively(bool visible)
+ /// Whether this object should be expanded in the GUI
+ public override void SetExpandedRecursively(bool expanded)
{
- m_Visible = visible;
- m_ShowComponents = visible;
- m_ShowChildren = visible;
+ m_Expanded = expanded;
+ m_ShowComponents = expanded;
+ m_ShowChildren = expanded;
+ if (m_Children == null)
+ return;
+
foreach (var child in m_Children)
{
- child.SetVisibleRecursively(visible);
+ child.SetExpandedRecursively(expanded);
}
}
+
+ public void Clear()
+ {
+
+ m_Children.Clear();
+ m_Components.Clear();
+ }
}
static class Styles
@@ -343,20 +447,26 @@ protected virtual void DrawItem(Rect selectionRect)
/// Check a UnityObject for missing serialized references
///
/// The UnityObject to be scanned
- /// A list to which properties with missing references will be added
/// User-configurable options for this view
- /// True if the object has any missing references
- protected static void CheckForMissingReferences(UnityObject obj, List properties, Options options)
+ /// A list of properties with missing references.
+ /// Null if no properties have missing references.
+ protected static List CheckForMissingReferences(UnityObject obj, Options options)
{
if (obj == null)
- return;
+ return null;
+ List properties = null;
var property = new SerializedObject(obj).GetIterator();
while (property.NextVisible(true)) // enterChildren = true to scan all properties
{
if (CheckForMissingReferences(property, options))
+ {
+ properties ??= new List();
properties.Add(property.Copy()); // Use a copy of this property because we are iterating on it
+ }
}
+
+ return properties;
}
static bool CheckForMissingReferences(SerializedProperty property, Options options)
@@ -440,6 +550,9 @@ static bool CheckForMissingReferences(SerializedProperty property, Options optio
/// A list of SerializedProperty objects known to have missing references
internal static void DrawPropertiesWithMissingReferences(List properties)
{
+ if (properties == null)
+ return;
+
foreach (var property in properties)
{
switch (property.propertyType)
diff --git a/Editor/MissingReferences/MissingSceneReferences.cs b/Editor/MissingReferences/MissingSceneReferences.cs
index d326fb5..691e360 100644
--- a/Editor/MissingReferences/MissingSceneReferences.cs
+++ b/Editor/MissingReferences/MissingSceneReferences.cs
@@ -22,14 +22,13 @@ sealed class MissingSceneReferences : MissingReferencesWindow
"WARNING: For large scenes, this may take a long time and/or crash the Editor.";
const string k_NoMissingReferences = "No missing references in active scene";
- const string k_UntitledSceneName = "Untitled";
// Bool fields will be serialized to maintain state between domain reloads, but our list of GameObjects will not
[NonSerialized]
bool m_Scanned;
Vector2 m_ScrollPosition;
- readonly List> m_SceneRoots = new List>();
+ readonly List m_SceneContainers = new List();
ILookup m_AllMissingReferences;
@@ -43,7 +42,7 @@ sealed class MissingSceneReferences : MissingReferencesWindow
protected override void Scan(Options options)
{
m_Scanned = true;
- m_SceneRoots.Clear();
+ m_SceneContainers.Clear();
// If we are in prefab isolation mode, scan the prefab stage instead of the active scene
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
@@ -67,16 +66,23 @@ protected override void Scan(Options options)
void AddToList(List list, GameObjectContainer container)
{
- list.AddRange(container.Children.Where(x => x.HasMissingReferences));
- foreach (var child in container.Children)
+ var children = container.Children;
+ if (children == null)
+ return;
+
+ list.AddRange(children.Where(x => x.HasMissingReferences));
+ foreach (var child in children)
{
AddToList(list, child);
}
}
- foreach (var kvp in m_SceneRoots)
+ foreach (var container in m_SceneContainers)
{
- AddToList(allMissingReferencesContainers, kvp.Value);
+ foreach (var gameObjectContainer in container.Roots)
+ {
+ AddToList(allMissingReferencesContainers, gameObjectContainer);
+ }
}
m_AllMissingReferences = allMissingReferencesContainers.ToLookup(x => x.Object.GetInstanceID());
@@ -91,20 +97,9 @@ void AddToList(List list, GameObjectContainer container)
void ScanScene(Scene scene, Options options)
{
- var rootObjectContainer = new GameObjectContainer();
- foreach (var gameObject in scene.GetRootGameObjects())
- {
- rootObjectContainer.AddChild(gameObject, options);
- }
-
- if (rootObjectContainer.Count > 0)
- {
- var sceneName = scene.name;
- if (string.IsNullOrEmpty(sceneName))
- sceneName = k_UntitledSceneName;
-
- m_SceneRoots.Add(new KeyValuePair(sceneName, rootObjectContainer));
- }
+ var sceneContainer = SceneContainer.CreateIfNecessary(scene, options);
+ if (sceneContainer != null)
+ m_SceneContainers.Add(sceneContainer);
}
protected override void OnGUI()
@@ -117,7 +112,7 @@ protected override void OnGUI()
GUIUtility.ExitGUI();
}
- if (m_SceneRoots.Count == 0)
+ if (m_SceneContainers.Count == 0)
{
GUILayout.Label(k_NoMissingReferences);
}
@@ -126,9 +121,9 @@ protected override void OnGUI()
using (var scrollView = new GUILayout.ScrollViewScope(m_ScrollPosition))
{
m_ScrollPosition = scrollView.scrollPosition;
- foreach (var kvp in m_SceneRoots)
+ foreach (var container in m_SceneContainers)
{
- kvp.Value.Draw();
+ container.Draw();
}
}
}
diff --git a/Editor/Unity.Labs.SuperScience.Editor.api b/Editor/Unity.Labs.SuperScience.Editor.api
index 800e665..ad6f312 100644
--- a/Editor/Unity.Labs.SuperScience.Editor.api
+++ b/Editor/Unity.Labs.SuperScience.Editor.api
@@ -34,11 +34,31 @@ namespace Unity.Labs.SuperScience
[System.Runtime.CompilerServices.Extension] public static void StopRunInEditMode(UnityEngine.MonoBehaviour behaviour);
}
+ public class PrefabLayerUsers : UnityEditor.EditorWindow
+ {
+ public PrefabLayerUsers() {}
+ }
+
+ public class PrefabTagUsers : UnityEditor.EditorWindow
+ {
+ public PrefabTagUsers() {}
+ }
+
public class RunInEditHelper : UnityEditor.EditorWindow
{
public RunInEditHelper() {}
}
+ public class SceneLayerUsers : UnityEditor.EditorWindow
+ {
+ public SceneLayerUsers() {}
+ }
+
+ public class SceneTagUsers : UnityEditor.EditorWindow
+ {
+ public SceneTagUsers() {}
+ }
+
public class SolidColorTextures : UnityEditor.EditorWindow
{
public SolidColorTextures() {}
diff --git a/Runtime/Unity.Labs.SuperScience.api b/Runtime/Unity.Labs.SuperScience.api
index be64dfa..d552016 100644
--- a/Runtime/Unity.Labs.SuperScience.api
+++ b/Runtime/Unity.Labs.SuperScience.api
@@ -34,6 +34,7 @@ namespace Unity.Labs.SuperScience
public class ExampleSceneMetadata : UnityEngine.ScriptableObject
{
+ public int TotalComponents { get; }
public ExampleSceneMetadata() {}
public void UpdateFromScene(UnityEngine.SceneManagement.Scene scene);
}