Detect unused addressables [Feature request]

So I’m still transferring over my project to make full use of addressables. So far everything has been mostly good as I’m figuring out how to use the system. One of my big things in making everything go smoothly was figuring out that it seems to work best if I make nearly everything used in my project an addressable and have as absolute little as possible built into the final build. I am however, foreseeing something that could potentially be wasteful. Let’s take the following example:

-You have multiple scenes in your game, which all reference the same tree, rock, etc prefabs. So rightfully each tree, rock and such prefabs are also marked as addressable and are set to be included in a large asset bundle via the addressables named “misc stuff”.

-Some time down the road, you edit the scenes and end up removing all instances of a said rock prefab or such. Heck, let’s say quite a big period of time goes by and you end up removing dozens of the packaged addressables from the scenes. A fairly normal thing that may occur over time while working on a scene and changing your mind how you want something to look.

-The issue with this is that they would no longer be referenced in any of the scenes and therefore have no reason to get included in the “misc stuff” asset bundle that will get created by the addressables. Regardless though, the addressables system would still include them in the bundle (I think it would anyway?). In the case now, you are forced to remember what it being used and not used in your project. So you’d have to go through the large “misc stuff” addressables group and make sure you don’t have any unused junk in there.

-So I imagine it could be really useful if the “Addressables Analyze” window had something that could scan all of the addressables and detect if any of them being unused and could therefore be removed by the user to save space. It would work very similar to the way the fixable rules already work in that window.

Hope that all makes sense. Maybe anyone else could chime in about this if it seems like something that would be useful. I imagine I could possible make an editor script myself, but haven’t looked into how much time that would take at the moment.

3 Likes

Thanks for the suggestion! I’ll kick it over to the team for their thoughts!

@therobby3 that’s a good idea. I can’t speak to when we’d get to something like this so I’d like to recommend creating your own Analyze Rule. You should be able to create your own custom Analyze Rule by inheriting from BundleRuleBase and registering the rule using AnalyzeSystem.RegisterNewRule(). You can see examples of this in CheckBundleDupeDependencies or any of our built in rules.

I hope this helps

Thanks! I’ll give the custom rules a shot. After using the addressables for a few weeks now, I already know I have to have several cases where there are unused assets chillin’ in a few bundles. So I am already having this be a pretty needed thing. In about a month I’ll give this a shot as I imagine I’ll have even more waste in unused addressables.

Anyone that has made this custom rule by chance?

Unfortunately I still haven’t got around to making this rule yet, too busy with the main game. =/ I know I have to have racked up quite a bit of unused addressables by now too.

Ok I got it working somewhat but there’s an issue.
Maybe someone from unity or anyone else can help me out.

When the addressables are referenced in a script in the scene they seem to get ignored by the assetdatabase when doing a AssetDatabase.GetDependencies(“scenePath”).
Any idea how to solve that?
I can add them manually to the scene just for analyzing the scene and removing them again afterwards but I want them included in the dependency list.

using System.Collections.Generic;
using System.Linq;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.SceneManagement;

namespace UnityEditor.AddressableAssets.Build.AnalyzeRules {
    public class UnusedAddressableRule : AnalyzeRule {
        List<GUID> sceneAssets = new List<GUID>();
        List<GUID> addressableAssets = new List<GUID>();
        List<GUID> AssetDifference = new List<GUID>();
        List<string> unusedAssetPaths = new List<string>();

        public override bool CanFix {
            get { return false; }
        }

        public override string ruleName {
            get { return "Check Unused Addressable Assets"; }
        }

        public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings) {
            ClearAnalysis();

            List<AnalyzeResult> results = new List<AnalyzeResult>();

         
            string scenePath = EditorSceneManager.GetActiveScene().path;

            //Problem: Addressable assets get ignored by the AssetDatabase when they are referenced in scene?
            //Temporarily add them to a empty game object in the scene, save your scene and remove them again afterwards
            GetSceneAssets(scenePath);
            GetAddressableAssets(settings);
            FilterLists();
            Convert_GUID_ToPath();

            for (int i = 0; i < unusedAssetPaths.Count; i++) {
                results.Add(new AnalyzeResult { resultName = unusedAssetPaths[i]+"", severity = MessageType.Warning });
            }

            if (results.Count == 0)
                results.Add(new AnalyzeResult { resultName = ruleName + " - No unused assets found." });

            return results;
        }
    
        void GetSceneAssets(string resourcePath) {            
            string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
            sceneAssets.Clear();
            sceneAssets.AddRange(from dependency in dependencies
                                                         select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
        }

        void GetAddressableAssets(AddressableAssetSettings settings) {
            addressableAssets.Clear();
            addressableAssets = (from aaGroup in settings.groups
                                   where aaGroup != null
                                   from entry in aaGroup.entries
                                   select new GUID(entry.guid)).ToList();
        }

        void FilterLists() {
            AssetDifference.Clear();
            AssetDifference = addressableAssets.Except(sceneAssets).ToList();
        }

        void Convert_GUID_ToPath() {
            unusedAssetPaths.Clear();
            unusedAssetPaths = (from guid in AssetDifference
                                select AssetDatabase.GUIDToAssetPath(guid.ToString())).ToList();
        }
    }

    [InitializeOnLoad]
    class RegisterUnusedAssetRule {
        static RegisterUnusedAssetRule() {
            AnalyzeSystem.RegisterNewRule<UnusedAddressableRule>();
        }
    }
}
2 Likes

I hit this issue too, GetDependencies and CalculatePlayerDependencies are designed for use in built-in workflows and as such they find references that are Actually in use, which is not what you want here.
If prefab A refs prefab B but it’s fields are overriden by something else in the scene, B’s original refs are not shown since it’s not “in” the scene.

Figuring out that took some time, the solution is to use EditorUtility.CollectDependencies on the scene which will show All dependencies even if unused.

1 Like

@TreyK-47 @davidla_unity
Any news regarding this feature request?
I think it would contribute greatly to making the addressables workflow easier.

To be honest, I expected the “Calculating Scene/Asset Dependency Data” steps from the Addressables build would already take care of this. :frowning:

I made some tests today after adding to my addressable groups many assets from some large folders (and setting the groups’ Bundle Mode to “Pack Separately”).
I was very disappointed when I made my game build and realized that the addressables system didn’t auto-exclude the unused assets.

To have an idea, I have some pretty big assets on my project, the main one is “Game Kit Controller” (GKC). I also have many animations from a GKC companion package, etc.
My game’s scenes are all Addressable scenes (I only have a bootstrap loader scene on my Build Settings). My main player character prefab (from GKC) is an addressable.

If I don’t mark all those GKC assets (prefabs, sounds, animations, textures, etc.) as addressables, my .apk build size is 156 MB.
It seems Unity’s default resource manager already took care of the unused assets/dependencies, but (naturally) the addressables’ analyze rules nagged about many duplicate bundle dependencies from GKC.
I expected I could further reduce my .apk size by getting rid of those duplicate dependencies in my scenes.

So, after adding nearly all GKC assets/animations/etc. to addressable groups (I tried this to quickly get rid of the duplicate references, but without having to mark “one by one” the used assets - it’s a lot of assets), my .apk build size became 1,04 GB. (!)
This was NOT the result I was expecting, from using Addressables.

I’m making a custom version of UnusedAddressableRule.cs, I may share it on this thread if the results are good. :wink:
But maybe one of the other users already implemented this with success? So, I may be doing redundant work. :stuck_out_tongue:
@therobby3

@JJRivers Did you also make a custom version of this script? Can you share it here?
I’m having difficulty figuring out exactly how to use EditorUtility.CollectDependencies like you mentioned.

@lejean_1 Did you update your script based on JJRivers’ advice? Can you share it?

Here’s my current version of UnusedAddressableRule.cs
I implemented it based on @lejean_1 's version, but I made it scan all the scenes I have in my addressable groups, instead of only scanning the currently active scene.
I also made it a fixable rule, so it can remove the unused addressable entries from the addressable groups.

It’s not perfect, though. It did remove most unused entries, but it’s also removing a lot of false-positive entries that are actually being used in my game.

In special, it removed lots of ScriptableObjects, some prefabs, scene .lighting settings, some shaders, textures used by some materials, etc.
Maybe those false-positives aren’t scene references directly (?), but they are being used in my scenes’ gameobjects in one way or another.

My build size is currently 454 MB, I guess because of duplicate bundle dependencies related to the removed false-positives.
That’s still quite far from the original 156 MB I had when I used much fewer addressables. Which, from my understanding, would imply more duplicate dependencies in my scenes, but the build size was actually smaller at that time than now… :eyes:

EDIT: I think one potential reason for the false-positives being removed, is something related to “addressable folders” entries inside the addressable groups. And also possibly other kinds of addressables with sub-assets.
Maybe those sub-assets aren’t being correctly detected by the current script.

using System.Collections.Generic;
using System.Linq;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.SceneManagement;

namespace UnityEditor.AddressableAssets.Build.AnalyzeRules
{
    public class UnusedAddressableRule : AnalyzeRule
    {
        List<GUID> sceneAssets = new List<GUID>();
        List<GUID> addressableAssets = new List<GUID>();
        List<SceneAsset> addressableSceneAssets = new List<SceneAsset>();
        List<GUID> AssetDifference = new List<GUID>();
        List<string> unusedAssetPaths = new List<string>();

        public override bool CanFix
        {
            get { return true; }
        }

        public override string ruleName
        {
            get { return "Check Unused Addressable Assets"; }
        }

        public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings)
        {
            ClearAnalysis();
            return CheckUnusedAddressableAssets(settings);
        }

        protected List<AnalyzeResult> CheckUnusedAddressableAssets(AddressableAssetSettings settings)
        {
            List<AnalyzeResult> results = new List<AnalyzeResult>();

            List<string> scenePaths = new List<string>();
            GetAddressableSceneAssets(settings);

            foreach (var sceneAsset in addressableSceneAssets)
            {
                scenePaths.Add(AssetDatabase.GetAssetPath(sceneAsset));
            }

            //Problem: Addressable assets get ignored by the AssetDatabase when they are referenced in scene?
            //Temporarily add them to a empty game object in the scene, save your scene and remove them again afterwards

            //string scenePath = EditorSceneManager.GetActiveScene().path;
            //GetSceneAssets(scenePath);
            GetSceneAssets(scenePaths);

            GetAddressableAssets(settings);
            FilterLists();
            Convert_GUID_ToPath();

            for (int i = 0; i < unusedAssetPaths.Count; i++)
            {
                results.Add(new AnalyzeResult { resultName = unusedAssetPaths[i]+"", severity = MessageType.Warning });
            }

            if (results.Count == 0)
                results.Add(new AnalyzeResult { resultName = ruleName + " - No unused assets found." });

            return results;
        }

        void GetSceneAssets(string resourcePath)
        {
            string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
            sceneAssets.Clear();
            sceneAssets.AddRange(from dependency in dependencies
                select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
        }

        void GetSceneAssets(List<string> resourcePaths)
        {
            sceneAssets.Clear();

            foreach (var resourcePath in resourcePaths)
            {
                string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
                sceneAssets.AddRange(from dependency in dependencies
                    select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
            }

            // Remove duplicate entries
            sceneAssets = sceneAssets.Distinct().ToList();
        }

        void GetAddressableAssets(AddressableAssetSettings settings)
        {
            addressableAssets.Clear();
            addressableAssets = (from aaGroup in settings.groups
                where aaGroup != null
                from entry in aaGroup.entries
                select new GUID(entry.guid)).ToList();
        }

        void GetAddressableSceneAssets(AddressableAssetSettings settings)
        {
            addressableSceneAssets.Clear();
            addressableSceneAssets = (from aaGroup in settings.groups
                where aaGroup != null
                from entry in aaGroup.entries
                where entry.IsScene
                select entry.MainAsset as SceneAsset).ToList();
        }

        void FilterLists()
        {
            AssetDifference.Clear();
            AssetDifference = addressableAssets.Except(sceneAssets).ToList();
        }

        void Convert_GUID_ToPath()
        {
            unusedAssetPaths.Clear();
            unusedAssetPaths = (from guid in AssetDifference
                select AssetDatabase.GUIDToAssetPath(guid.ToString())).ToList();
        }

        public override void FixIssues(AddressableAssetSettings settings)
        {
            if (AssetDifference == null)
                CheckUnusedAddressableAssets(settings);

            if (AssetDifference == null || AssetDifference.Count == 0)
                return;

            foreach (var asset in AssetDifference)
                settings.RemoveAssetEntry(asset.ToString(), false);

            settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
        }
    }

    [InitializeOnLoad]
    class RegisterUnusedAssetRule
    {
        static RegisterUnusedAssetRule()
        {
            AnalyzeSystem.RegisterNewRule<UnusedAddressableRule>();
        }
    }
}

I fixed all the issues I mentioned above and implemented the custom rule, basing myself on both @lejean_1 's script and @JJRivers ’ comments. Thank you so much!

I managed to shrink my original 156 MB .apk build to 72,5 MB. :slight_smile:

I’ll edit this post with my findings later (I really need to sleep now, haha)

EDIT: Here’s my version of FindUnusedAssets. I pretty much came up with more or less the same logic as @JJRivers 's script from the post below, with some differences.

I kept both the EditorUtility.CollectDependencies() scan for scene root objects and the
AssetDatabase.GetDependencies() scan for scene paths, because each of them finds some unique “used asset” entries.

using System.Collections.Generic;
using System.Linq;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace UnityEditor.AddressableAssets.Build.AnalyzeRules
{
    public class UnusedAddressableRule : AnalyzeRule
    {
        private List<GUID> sceneAssets = new List<GUID>();
        private List<GUID> addressableAssets = new List<GUID>();
        private List<SceneAsset> addressableSceneAssets = new List<SceneAsset>();
        private List<GUID> AssetDifference = new List<GUID>();
        private List<string> unusedAssetPaths = new List<string>();
        private Dictionary<GUID, List<GUID>> addressableFoldersData = new Dictionary<GUID, List<GUID>>();

        private List<string> addressableGroupsToIgnore = new List<string>
        {
            "Shaders-Unity-Terrain",
        };

        public override bool CanFix
        {
            get { return true; }
        }

        public override string ruleName
        {
            get { return "Check Unused Addressable Assets"; }
        }

        public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings)
        {
            ClearAnalysis();
            return CheckUnusedAddressableAssets(settings);
        }

        protected List<AnalyzeResult> CheckUnusedAddressableAssets(AddressableAssetSettings settings)
        {
            List<AnalyzeResult> results = new List<AnalyzeResult>();
            sceneAssets.Clear();

            GetAddressableSceneAssets(settings);

            var lastActiveScenePath = SceneManager.GetActiveScene().path;
            var scenesProcessed = 0;

            foreach (var sceneAsset in addressableSceneAssets)
            {
                var scenePath = AssetDatabase.GetAssetPath(sceneAsset);

                scenesProcessed++;
                var progress = (float)scenesProcessed / (float)addressableSceneAssets.Count;
                if (EditorUtility.DisplayCancelableProgressBar(
                        $"Analyzing scene ({scenesProcessed}/{addressableSceneAssets.Count})", scenePath, progress))
                {
                    break;
                }

                var scene = EditorSceneManager.OpenScene(scenePath);
                GetSceneAssets(scene);
                GetSceneAssets(scenePath);
            }

            EditorSceneManager.OpenScene(lastActiveScenePath);

            // Remove duplicate entries
            sceneAssets = sceneAssets.Distinct().ToList();

            GetAddressableAssets(settings);
            FilterLists();
            Convert_GUID_ToPath();

            for (int i = 0; i < unusedAssetPaths.Count; i++)
            {
                results.Add(new AnalyzeResult { resultName = unusedAssetPaths[i]+"", severity = MessageType.Warning });
            }

            if (results.Count == 0)
                results.Add(new AnalyzeResult { resultName = ruleName + " - No unused assets found." });

            return results;
        }

        private void GetSceneAssets(Scene scene)
        {
            List<GameObject> rootGameObjects = scene.GetRootGameObjects().ToList();
            List<Object> objects = rootGameObjects.ConvertAll(root => (Object)root);

            Object[] dependencies = EditorUtility.CollectDependencies(objects.ToArray());

            foreach (var obj in dependencies)
            {
                if (obj == null)
                    continue;

                if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _))
                {
                    sceneAssets.Add(new GUID(guid));
                }
            }
        }

        private void GetSceneAssets(string resourcePath)
        {
            string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
            sceneAssets.AddRange(from dependency in dependencies
                select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
        }

        private void GetAddressableAssets(AddressableAssetSettings settings)
        {
            addressableAssets.Clear();
            addressableFoldersData.Clear();

            foreach (var aaGroup in settings.groups)
            {
                if (aaGroup == null)
                    continue;

                if (addressableGroupsToIgnore.Contains(aaGroup.Name))
                    continue;

                foreach (var entry in aaGroup.entries)
                {
                    if (entry.AssetPath == "*/Resources/" || entry.AssetPath == "Scenes In Build")
                    {
                        continue;
                    }

                    // Don't add scenes to the addressableAssets list
                    if (entry.IsScene)
                    {
                        continue;
                    }

                    // Scan addressables' sub-assets (inside addressable folders, .fbx models, etc.)
                    var splitPath = entry.AssetPath.Split('/');
                    if (entry.IsFolder || !splitPath.Last().Contains('.'))
                    {
                        string[] folderGuids = AssetDatabase.FindAssets("t:folder", new[] {entry.AssetPath});
                        string[] guids = AssetDatabase.FindAssets("", new[] {entry.AssetPath});
                        List<GUID> filteredGuids = new List<GUID>();

                        filteredGuids.AddRange(from guid in guids
                            where !folderGuids.Contains(guid) // Don't add folders to the list
                                  && !AssetDatabase.GUIDToAssetPath(guid).EndsWith(".cs") // Don't add scripts to the list
                            select new GUID(guid));

                        addressableFoldersData.Add(new GUID(entry.guid), filteredGuids);
                    }
                    else
                    {
                        addressableAssets.Add(new GUID(entry.guid));
                    }
                }
            }
        }

        private void GetAddressableSceneAssets(AddressableAssetSettings settings)
        {
            addressableSceneAssets.Clear();
            addressableSceneAssets = (from aaGroup in settings.groups
                where aaGroup != null
                from entry in aaGroup.entries
                where entry.IsScene
                select entry.MainAsset as SceneAsset).ToList();
        }

        private void FilterLists()
        {
            AssetDifference.Clear();
            AssetDifference = addressableAssets.Except(sceneAssets).ToList();

            foreach (var addressableFolderData in addressableFoldersData)
            {
                if (addressableFolderData.Value.Intersect(sceneAssets).Any())
                    continue; // At least one sub-asset from this addressable folder is being used in the game

                // Mark addressable folder as unused, if ALL of its sub-assets are unused
                AssetDifference.Add(addressableFolderData.Key);
            }
        }

        private void Convert_GUID_ToPath()
        {
            unusedAssetPaths.Clear();
            unusedAssetPaths = (from guid in AssetDifference
                select AssetDatabase.GUIDToAssetPath(guid.ToString())).ToList();
        }

        public override void FixIssues(AddressableAssetSettings settings)
        {
            if (AssetDifference == null)
                CheckUnusedAddressableAssets(settings);

            if (AssetDifference == null || AssetDifference.Count == 0)
                return;

            foreach (var asset in AssetDifference)
                settings.RemoveAssetEntry(asset.ToString(), false);

            settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
        }
    }

    [InitializeOnLoad]
    class RegisterUnusedAssetRule
    {
        static RegisterUnusedAssetRule()
        {
            AnalyzeSystem.RegisterNewRule<UnusedAddressableRule>();
        }
    }
}

Also, regarding the big build sizes during my tests, even after I removed addressables from my groups (like when I reported my 454 MB build size in a previous post).

Turns out there were some previously generated asset bundles with very long filepaths (their addressable groups were set to Pack Separately), and Unity’s addressable build process wasn’t able to delete those files and folders from the build cache/Gradle cache.
So, Unity ended up adding those unused “ghost bundles” from the build cache to my builds.

I had to manually delete the build cache folder (in my case, the “Library\Bee\Android\Prj\Mono2x\Gradle” folder) to get rid of those ghost bundles with long paths.
But after that, shortening the addressable names (for instance, using the “Simplify Addressable Names” option) fixed the cache issue.
Then, if I remove any addressables from my addressable groups, Unity’s addressable build process is able to delete the unused bundles correctly, as their paths are much shorter. :slight_smile:

Related post regarding the long paths issue:

1 Like

Here’s our version of FindUnusedAssets, has an extra feature for excluding a specific group from unused asset filtering. I kinda get why the Addressables team didn’t include this by default since there are quite a few assumptions one has to make with it, but this one should be readable enough for any C# coder.

No warranties on comments being up to date or anything else but it does work quite nicely.

using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.AddressableAssets.Build;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

[InitializeOnLoad]
class RegisterFindUnusedAssets
{
    static RegisterFindUnusedAssets()
    {
        AnalyzeSystem.RegisterNewRule<FindUnusedAssets>();
    }
}

/// <summary>
/// Analyzes Addressable asset setup in the project by identifying Groups that have scene in them, then filters for assets that have no references in any scene.
/// Excludes assets found in <see cref="CodeLoadedAssetsGroupName"/>. (Dynamic loads can't be detected in the scene files, so exclude those we Know aren't in them)
/// </summary>
public class FindUnusedAssets : UnityEditor.AddressableAssets.Build.AnalyzeRules.AnalyzeRule
{
    /// <summary>
    /// Make sure this group has it's Include in Build tag disabled if you don't want them in your builds.
    /// </summary>
    private const string UnusedAssetsGroupName = "UnusedAssets";
    /// <summary>
    /// Use this to exclude assets from the filtering if you have some small assets you want to ensure stay included in the build but are not referenced in any scene intentionally.
    /// </summary>
    private const string CodeLoadedAssetsGroupName = "CodeLoadedAssets";
    private const string FolderSeparator = "/";

    public override bool CanFix
    {
        get { return true; }
        set { }
    }

    public override string ruleName
    {
        get { return "Find unused assets"; }
    }

    [SerializeField]
    Dictionary<AddressableAssetEntry, AddressableAssetGroup> MisplacedEntries = new();

    /// <summary>
    /// Addressables Analysis rule to find assets not referenced in any scene
    /// </summary>
    /// <param name="settings">Currently active <see cref="AddressableAssetSettings"/> asset, supplied by Addressables Analyze tool.</param>
    /// <returns>List of issues found, this method should only ever analyze and list issues, fixes are done via <see cref="FixIssues"/></returns>
    public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings)
    {
        var restoreOpenScene = EditorSceneManager.GetActiveScene().path;

        List<AnalyzeResult> results = new();

        List<AddressableAssetEntry> sceneEntries = new();

        // Process groups with scenes in them, collecting all dependencies in the scene group.
        foreach ( var group in settings.groups )
        {
            //Skip built-in data, these are shown in addressables groups as well but are not addressables hence don't touch them.
            if ( IsBuiltInData(group) )
            {
                continue;
            }

            // Identify groups that contain scenes, collecting all the dependencies in the group into a guid list associated with that group.
            foreach ( var entry in group.entries )
            {
                if ( IsSceneAsset(entry) )
                {
                    if ( !sceneEntries.Contains(entry) )
                    {
                        sceneEntries.Add(entry);
                    }
                }
            }
        }

        List<string> sceneGuids = new();

        foreach ( var sceneEntry in sceneEntries )
        {
            // Collect all the dependencies in the scene to the guid list of the scene.
            EditorSceneManager.OpenScene(sceneEntry.AssetPath);

            var rootGameObjects = SceneManager.GetActiveScene().GetRootGameObjects().ToList();

            List<Object> objects = rootGameObjects.ConvertAll(root => (Object)root);

            // CRITICAL; Must use EditorUtility.CollectDependencies, other dependency gathering methods
            // do not list dependencies of overridden prefabs resulting in implicit intra-bundle dependencies.
            var dependencies = EditorUtility.CollectDependencies(rootGameObjects.ToArray());

            // Convert dependency objects into GUID lists for fast asset identification.
            foreach ( var rootObject in dependencies )
            {
                string guid = "";

                if ( AssetDatabase.TryGetGUIDAndLocalFileIdentifier(rootObject, out guid, out long _) )
                {
                    sceneGuids.Add(guid);
                }
            }
        }

        var guidHashSet = sceneGuids.ToHashSet();
        AddressableAssetGroup unusedAssetsGroup = settings.groups.Where(group => group.Name.Contains(UnusedAssetsGroupName)).First();
        AddressableAssetGroup codeLoadedAssetsGroup = settings.groups.Where(group => group.Name.Contains(CodeLoadedAssetsGroupName)).First();
        int entriesWithNoHits = 0;

        foreach ( var group in settings.groups )
        {
            foreach ( var entry in group.entries )
            {
                //Skip built-in data, these are shown in addressables groups as well but are not addressables hence don't touch them.
                if ( IsBuiltInData(group) )
                {
                    continue;
                }

                // Skip the scenes themselves as they are the basis of finding unused references.
                if ( IsSceneAsset(entry) )
                {
                    continue;
                }

                if ( entry.parentGroup != codeLoadedAssetsGroup && !guidHashSet.Contains(entry.guid) && entry.AssetPath.Contains(FolderSeparator) )
                {
                    results.Add(new AnalyzeResult { resultName = group.Name + kDelimiter + entry.address, severity = MessageType.Warning });
                    MisplacedEntries.Add(entry, unusedAssetsGroup);
                    entriesWithNoHits++;
                }
            }
        }

        if ( entriesWithNoHits > 0 )
        {
            Debug.LogWarning($"Found {entriesWithNoHits} entries with no hits in any scene.\nConsider reviewing if the assets are actually in use.");
        }

        if ( results.Count == 0 )
        {
            results.Add(new AnalyzeResult { resultName = "No issues found." });
        }

        EditorSceneManager.OpenScene(restoreOpenScene);

        return results;
    }

    private static bool IsBuiltInData(AddressableAssetGroup group)
    {
        return group.HasSchema<PlayerDataGroupSchema>();
    }

    private static bool IsSceneAsset(AddressableAssetEntry entry)
    {
        return entry.AssetPath.Contains(".unity") && !entry.AssetPath.Contains("com.unity.");
    }

    public override void ClearAnalysis()
    {
        MisplacedEntries.Clear();
    }

    /// <summary>
    /// TODO; Implement moving the assets to an UnusedAssets group that is not included in build, pre-requisites: filtering for script loaded assets.
    /// </summary>
    public override void FixIssues(AddressableAssetSettings settings)
    {
        int movedAssets = 0;

        StringBuilder movedAssetsLog = new();

        foreach ( var misplacedEntry in MisplacedEntries )
        {
            settings.CreateOrMoveEntry(misplacedEntry.Key.guid, misplacedEntry.Value, false, false);
            movedAssetsLog.AppendLine(misplacedEntry.Key.address);
            movedAssets++;
        }

        settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);

        Debug.Log($"Moved: {movedAssets} assets to their initial groups." + $"\nMovedAssets:\n" + movedAssetsLog.ToString());

        StringBuilder emptyGroupsWarning = new();
        int emptyGroups = 0;

        foreach ( var group in settings.groups )
        {
            if ( group.entries.Count == 0 )
            {
                emptyGroupsWarning.AppendLine($"{group.Name} is now empty, consider removing it if it isn't needed anymore.");
                emptyGroups++;
            }
        }

        if ( emptyGroups > 0 )
        {
            Debug.LogWarning($"There are {emptyGroups} empty groups after fixing Addressables Groupings:\n" + emptyGroupsWarning);
        }

        MisplacedEntries.Clear();
    }
}
2 Likes

doesn’t works for Unity 6, it cause errors to assembly.