Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Detect unused addressables [Feature request]

Discussion in 'Addressables' started by therobby3, Oct 28, 2020.

  1. therobby3

    therobby3

    Joined:
    Jan 30, 2019
    Posts:
    131
    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.
     
    Last edited: Nov 6, 2020
    andreiagmu, crekri and LudiKha like this.
  2. TreyK-47

    TreyK-47

    Unity Technologies

    Joined:
    Oct 22, 2019
    Posts:
    1,816
    Thanks for the suggestion! I'll kick it over to the team for their thoughts!
     
  3. davidla_unity

    davidla_unity

    Unity Technologies

    Joined:
    Nov 17, 2016
    Posts:
    762
    @robscherer123 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<T>(). You can see examples of this in CheckBundleDupeDependencies or any of our built in rules.

    I hope this helps
     
  4. therobby3

    therobby3

    Joined:
    Jan 30, 2019
    Posts:
    131
    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.
     
  5. lejean

    lejean

    Joined:
    Jul 4, 2013
    Posts:
    392
    Anyone that has made this custom rule by chance?
     
  6. therobby3

    therobby3

    Joined:
    Jan 30, 2019
    Posts:
    131
    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.
     
  7. lejean

    lejean

    Joined:
    Jul 4, 2013
    Posts:
    392
    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.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using UnityEditor.AddressableAssets.Settings;
    4. using UnityEditor.SceneManagement;
    5.  
    6. namespace UnityEditor.AddressableAssets.Build.AnalyzeRules {
    7.     public class UnusedAddressableRule : AnalyzeRule {
    8.         List<GUID> sceneAssets = new List<GUID>();
    9.         List<GUID> addressableAssets = new List<GUID>();
    10.         List<GUID> AssetDifference = new List<GUID>();
    11.         List<string> unusedAssetPaths = new List<string>();
    12.  
    13.         public override bool CanFix {
    14.             get { return false; }
    15.         }
    16.  
    17.         public override string ruleName {
    18.             get { return "Check Unused Addressable Assets"; }
    19.         }
    20.  
    21.         public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings) {
    22.             ClearAnalysis();
    23.  
    24.             List<AnalyzeResult> results = new List<AnalyzeResult>();
    25.  
    26.          
    27.             string scenePath = EditorSceneManager.GetActiveScene().path;
    28.  
    29.             //Problem: Addressable assets get ignored by the AssetDatabase when they are referenced in scene?
    30.             //Temporarily add them to a empty game object in the scene, save your scene and remove them again afterwards
    31.             GetSceneAssets(scenePath);
    32.             GetAddressableAssets(settings);
    33.             FilterLists();
    34.             Convert_GUID_ToPath();
    35.  
    36.             for (int i = 0; i < unusedAssetPaths.Count; i++) {
    37.                 results.Add(new AnalyzeResult { resultName = unusedAssetPaths[i]+"", severity = MessageType.Warning });
    38.             }
    39.  
    40.             if (results.Count == 0)
    41.                 results.Add(new AnalyzeResult { resultName = ruleName + " - No unused assets found." });
    42.  
    43.             return results;
    44.         }
    45.    
    46.         void GetSceneAssets(string resourcePath) {            
    47.             string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
    48.             sceneAssets.Clear();
    49.             sceneAssets.AddRange(from dependency in dependencies
    50.                                                          select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
    51.         }
    52.  
    53.         void GetAddressableAssets(AddressableAssetSettings settings) {
    54.             addressableAssets.Clear();
    55.             addressableAssets = (from aaGroup in settings.groups
    56.                                    where aaGroup != null
    57.                                    from entry in aaGroup.entries
    58.                                    select new GUID(entry.guid)).ToList();
    59.         }
    60.  
    61.         void FilterLists() {
    62.             AssetDifference.Clear();
    63.             AssetDifference = addressableAssets.Except(sceneAssets).ToList();
    64.         }
    65.  
    66.         void Convert_GUID_ToPath() {
    67.             unusedAssetPaths.Clear();
    68.             unusedAssetPaths = (from guid in AssetDifference
    69.                                 select AssetDatabase.GUIDToAssetPath(guid.ToString())).ToList();
    70.         }
    71.     }
    72.  
    73.     [InitializeOnLoad]
    74.     class RegisterUnusedAssetRule {
    75.         static RegisterUnusedAssetRule() {
    76.             AnalyzeSystem.RegisterNewRule<UnusedAddressableRule>();
    77.         }
    78.     }
    79. }
     
    Last edited: Mar 11, 2021
    andreiagmu and crekri like this.
  8. JJRivers

    JJRivers

    Joined:
    Oct 16, 2018
    Posts:
    137
    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.
     
    andreiagmu likes this.
  9. andreiagmu

    andreiagmu

    Joined:
    Feb 20, 2014
    Posts:
    175
    @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. :(

    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.
     
    Last edited: Feb 21, 2023
  10. andreiagmu

    andreiagmu

    Joined:
    Feb 20, 2014
    Posts:
    175
    I'm making a custom version of UnusedAddressableRule.cs, I may share it on this thread if the results are good. ;)
    But maybe one of the other users already implemented this with success? So, I may be doing redundant work. :p
    @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 Did you update your script based on JJRivers' advice? Can you share it?
     
    Last edited: Feb 21, 2023
  11. andreiagmu

    andreiagmu

    Joined:
    Feb 20, 2014
    Posts:
    175
    Here's my current version of UnusedAddressableRule.cs
    I implemented it based on @lejean'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... o_O

    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.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using UnityEditor.AddressableAssets.Settings;
    4. using UnityEditor.SceneManagement;
    5.  
    6. namespace UnityEditor.AddressableAssets.Build.AnalyzeRules
    7. {
    8.     public class UnusedAddressableRule : AnalyzeRule
    9.     {
    10.         List<GUID> sceneAssets = new List<GUID>();
    11.         List<GUID> addressableAssets = new List<GUID>();
    12.         List<SceneAsset> addressableSceneAssets = new List<SceneAsset>();
    13.         List<GUID> AssetDifference = new List<GUID>();
    14.         List<string> unusedAssetPaths = new List<string>();
    15.  
    16.         public override bool CanFix
    17.         {
    18.             get { return true; }
    19.         }
    20.  
    21.         public override string ruleName
    22.         {
    23.             get { return "Check Unused Addressable Assets"; }
    24.         }
    25.  
    26.         public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings)
    27.         {
    28.             ClearAnalysis();
    29.             return CheckUnusedAddressableAssets(settings);
    30.         }
    31.  
    32.         protected List<AnalyzeResult> CheckUnusedAddressableAssets(AddressableAssetSettings settings)
    33.         {
    34.             List<AnalyzeResult> results = new List<AnalyzeResult>();
    35.  
    36.             List<string> scenePaths = new List<string>();
    37.             GetAddressableSceneAssets(settings);
    38.  
    39.             foreach (var sceneAsset in addressableSceneAssets)
    40.             {
    41.                 scenePaths.Add(AssetDatabase.GetAssetPath(sceneAsset));
    42.             }
    43.  
    44.             //Problem: Addressable assets get ignored by the AssetDatabase when they are referenced in scene?
    45.             //Temporarily add them to a empty game object in the scene, save your scene and remove them again afterwards
    46.  
    47.             //string scenePath = EditorSceneManager.GetActiveScene().path;
    48.             //GetSceneAssets(scenePath);
    49.             GetSceneAssets(scenePaths);
    50.  
    51.             GetAddressableAssets(settings);
    52.             FilterLists();
    53.             Convert_GUID_ToPath();
    54.  
    55.             for (int i = 0; i < unusedAssetPaths.Count; i++)
    56.             {
    57.                 results.Add(new AnalyzeResult { resultName = unusedAssetPaths[i]+"", severity = MessageType.Warning });
    58.             }
    59.  
    60.             if (results.Count == 0)
    61.                 results.Add(new AnalyzeResult { resultName = ruleName + " - No unused assets found." });
    62.  
    63.             return results;
    64.         }
    65.  
    66.         void GetSceneAssets(string resourcePath)
    67.         {
    68.             string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
    69.             sceneAssets.Clear();
    70.             sceneAssets.AddRange(from dependency in dependencies
    71.                 select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
    72.         }
    73.  
    74.         void GetSceneAssets(List<string> resourcePaths)
    75.         {
    76.             sceneAssets.Clear();
    77.  
    78.             foreach (var resourcePath in resourcePaths)
    79.             {
    80.                 string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
    81.                 sceneAssets.AddRange(from dependency in dependencies
    82.                     select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
    83.             }
    84.  
    85.             // Remove duplicate entries
    86.             sceneAssets = sceneAssets.Distinct().ToList();
    87.         }
    88.  
    89.         void GetAddressableAssets(AddressableAssetSettings settings)
    90.         {
    91.             addressableAssets.Clear();
    92.             addressableAssets = (from aaGroup in settings.groups
    93.                 where aaGroup != null
    94.                 from entry in aaGroup.entries
    95.                 select new GUID(entry.guid)).ToList();
    96.         }
    97.  
    98.         void GetAddressableSceneAssets(AddressableAssetSettings settings)
    99.         {
    100.             addressableSceneAssets.Clear();
    101.             addressableSceneAssets = (from aaGroup in settings.groups
    102.                 where aaGroup != null
    103.                 from entry in aaGroup.entries
    104.                 where entry.IsScene
    105.                 select entry.MainAsset as SceneAsset).ToList();
    106.         }
    107.  
    108.         void FilterLists()
    109.         {
    110.             AssetDifference.Clear();
    111.             AssetDifference = addressableAssets.Except(sceneAssets).ToList();
    112.         }
    113.  
    114.         void Convert_GUID_ToPath()
    115.         {
    116.             unusedAssetPaths.Clear();
    117.             unusedAssetPaths = (from guid in AssetDifference
    118.                 select AssetDatabase.GUIDToAssetPath(guid.ToString())).ToList();
    119.         }
    120.  
    121.         public override void FixIssues(AddressableAssetSettings settings)
    122.         {
    123.             if (AssetDifference == null)
    124.                 CheckUnusedAddressableAssets(settings);
    125.  
    126.             if (AssetDifference == null || AssetDifference.Count == 0)
    127.                 return;
    128.  
    129.             foreach (var asset in AssetDifference)
    130.                 settings.RemoveAssetEntry(asset.ToString(), false);
    131.  
    132.             settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
    133.         }
    134.     }
    135.  
    136.     [InitializeOnLoad]
    137.     class RegisterUnusedAssetRule
    138.     {
    139.         static RegisterUnusedAssetRule()
    140.         {
    141.             AnalyzeSystem.RegisterNewRule<UnusedAddressableRule>();
    142.         }
    143.     }
    144. }
    145.  
     
    Last edited: Feb 21, 2023
  12. andreiagmu

    andreiagmu

    Joined:
    Feb 20, 2014
    Posts:
    175
    I fixed all the issues I mentioned above and implemented the custom rule, basing myself on both @lejean's script and @JJRivers' comments. Thank you so much!

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

    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.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using UnityEditor.AddressableAssets.Settings;
    4. using UnityEditor.SceneManagement;
    5. using UnityEngine;
    6. using UnityEngine.SceneManagement;
    7.  
    8. namespace UnityEditor.AddressableAssets.Build.AnalyzeRules
    9. {
    10.     public class UnusedAddressableRule : AnalyzeRule
    11.     {
    12.         private List<GUID> sceneAssets = new List<GUID>();
    13.         private List<GUID> addressableAssets = new List<GUID>();
    14.         private List<SceneAsset> addressableSceneAssets = new List<SceneAsset>();
    15.         private List<GUID> AssetDifference = new List<GUID>();
    16.         private List<string> unusedAssetPaths = new List<string>();
    17.         private Dictionary<GUID, List<GUID>> addressableFoldersData = new Dictionary<GUID, List<GUID>>();
    18.  
    19.         private List<string> addressableGroupsToIgnore = new List<string>
    20.         {
    21.             "Shaders-Unity-Terrain",
    22.         };
    23.  
    24.         public override bool CanFix
    25.         {
    26.             get { return true; }
    27.         }
    28.  
    29.         public override string ruleName
    30.         {
    31.             get { return "Check Unused Addressable Assets"; }
    32.         }
    33.  
    34.         public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings)
    35.         {
    36.             ClearAnalysis();
    37.             return CheckUnusedAddressableAssets(settings);
    38.         }
    39.  
    40.         protected List<AnalyzeResult> CheckUnusedAddressableAssets(AddressableAssetSettings settings)
    41.         {
    42.             List<AnalyzeResult> results = new List<AnalyzeResult>();
    43.             sceneAssets.Clear();
    44.  
    45.             GetAddressableSceneAssets(settings);
    46.  
    47.             var lastActiveScenePath = SceneManager.GetActiveScene().path;
    48.             var scenesProcessed = 0;
    49.  
    50.             foreach (var sceneAsset in addressableSceneAssets)
    51.             {
    52.                 var scenePath = AssetDatabase.GetAssetPath(sceneAsset);
    53.  
    54.                 scenesProcessed++;
    55.                 var progress = (float)scenesProcessed / (float)addressableSceneAssets.Count;
    56.                 if (EditorUtility.DisplayCancelableProgressBar(
    57.                         $"Analyzing scene ({scenesProcessed}/{addressableSceneAssets.Count})", scenePath, progress))
    58.                 {
    59.                     break;
    60.                 }
    61.  
    62.                 var scene = EditorSceneManager.OpenScene(scenePath);
    63.                 GetSceneAssets(scene);
    64.                 GetSceneAssets(scenePath);
    65.             }
    66.  
    67.             EditorSceneManager.OpenScene(lastActiveScenePath);
    68.  
    69.             // Remove duplicate entries
    70.             sceneAssets = sceneAssets.Distinct().ToList();
    71.  
    72.             GetAddressableAssets(settings);
    73.             FilterLists();
    74.             Convert_GUID_ToPath();
    75.  
    76.             for (int i = 0; i < unusedAssetPaths.Count; i++)
    77.             {
    78.                 results.Add(new AnalyzeResult { resultName = unusedAssetPaths[i]+"", severity = MessageType.Warning });
    79.             }
    80.  
    81.             if (results.Count == 0)
    82.                 results.Add(new AnalyzeResult { resultName = ruleName + " - No unused assets found." });
    83.  
    84.             return results;
    85.         }
    86.  
    87.         private void GetSceneAssets(Scene scene)
    88.         {
    89.             List<GameObject> rootGameObjects = scene.GetRootGameObjects().ToList();
    90.             List<Object> objects = rootGameObjects.ConvertAll(root => (Object)root);
    91.  
    92.             Object[] dependencies = EditorUtility.CollectDependencies(objects.ToArray());
    93.  
    94.             foreach (var obj in dependencies)
    95.             {
    96.                 if (obj == null)
    97.                     continue;
    98.  
    99.                 if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _))
    100.                 {
    101.                     sceneAssets.Add(new GUID(guid));
    102.                 }
    103.             }
    104.         }
    105.  
    106.         private void GetSceneAssets(string resourcePath)
    107.         {
    108.             string[] dependencies = AssetDatabase.GetDependencies(resourcePath);
    109.             sceneAssets.AddRange(from dependency in dependencies
    110.                 select new GUID(AssetDatabase.AssetPathToGUID(dependency)));
    111.         }
    112.  
    113.         private void GetAddressableAssets(AddressableAssetSettings settings)
    114.         {
    115.             addressableAssets.Clear();
    116.             addressableFoldersData.Clear();
    117.  
    118.             foreach (var aaGroup in settings.groups)
    119.             {
    120.                 if (aaGroup == null)
    121.                     continue;
    122.  
    123.                 if (addressableGroupsToIgnore.Contains(aaGroup.Name))
    124.                     continue;
    125.  
    126.                 foreach (var entry in aaGroup.entries)
    127.                 {
    128.                     if (entry.AssetPath == "*/Resources/" || entry.AssetPath == "Scenes In Build")
    129.                     {
    130.                         continue;
    131.                     }
    132.  
    133.                     // Don't add scenes to the addressableAssets list
    134.                     if (entry.IsScene)
    135.                     {
    136.                         continue;
    137.                     }
    138.  
    139.                     // Scan addressables' sub-assets (inside addressable folders, .fbx models, etc.)
    140.                     var splitPath = entry.AssetPath.Split('/');
    141.                     if (entry.IsFolder || !splitPath.Last().Contains('.'))
    142.                     {
    143.                         string[] folderGuids = AssetDatabase.FindAssets("t:folder", new[] {entry.AssetPath});
    144.                         string[] guids = AssetDatabase.FindAssets("", new[] {entry.AssetPath});
    145.                         List<GUID> filteredGuids = new List<GUID>();
    146.  
    147.                         filteredGuids.AddRange(from guid in guids
    148.                             where !folderGuids.Contains(guid) // Don't add folders to the list
    149.                                   && !AssetDatabase.GUIDToAssetPath(guid).EndsWith(".cs") // Don't add scripts to the list
    150.                             select new GUID(guid));
    151.  
    152.                         addressableFoldersData.Add(new GUID(entry.guid), filteredGuids);
    153.                     }
    154.                     else
    155.                     {
    156.                         addressableAssets.Add(new GUID(entry.guid));
    157.                     }
    158.                 }
    159.             }
    160.         }
    161.  
    162.         private void GetAddressableSceneAssets(AddressableAssetSettings settings)
    163.         {
    164.             addressableSceneAssets.Clear();
    165.             addressableSceneAssets = (from aaGroup in settings.groups
    166.                 where aaGroup != null
    167.                 from entry in aaGroup.entries
    168.                 where entry.IsScene
    169.                 select entry.MainAsset as SceneAsset).ToList();
    170.         }
    171.  
    172.         private void FilterLists()
    173.         {
    174.             AssetDifference.Clear();
    175.             AssetDifference = addressableAssets.Except(sceneAssets).ToList();
    176.  
    177.             foreach (var addressableFolderData in addressableFoldersData)
    178.             {
    179.                 if (addressableFolderData.Value.Intersect(sceneAssets).Any())
    180.                     continue; // At least one sub-asset from this addressable folder is being used in the game
    181.  
    182.                 // Mark addressable folder as unused, if ALL of its sub-assets are unused
    183.                 AssetDifference.Add(addressableFolderData.Key);
    184.             }
    185.         }
    186.  
    187.         private void Convert_GUID_ToPath()
    188.         {
    189.             unusedAssetPaths.Clear();
    190.             unusedAssetPaths = (from guid in AssetDifference
    191.                 select AssetDatabase.GUIDToAssetPath(guid.ToString())).ToList();
    192.         }
    193.  
    194.         public override void FixIssues(AddressableAssetSettings settings)
    195.         {
    196.             if (AssetDifference == null)
    197.                 CheckUnusedAddressableAssets(settings);
    198.  
    199.             if (AssetDifference == null || AssetDifference.Count == 0)
    200.                 return;
    201.  
    202.             foreach (var asset in AssetDifference)
    203.                 settings.RemoveAssetEntry(asset.ToString(), false);
    204.  
    205.             settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
    206.         }
    207.     }
    208.  
    209.     [InitializeOnLoad]
    210.     class RegisterUnusedAssetRule
    211.     {
    212.         static RegisterUnusedAssetRule()
    213.         {
    214.             AnalyzeSystem.RegisterNewRule<UnusedAddressableRule>();
    215.         }
    216.     }
    217. }
    218.  

    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. :)

    Related post regarding the long paths issue:
    https://forum.unity.com/threads/add...generated-asset-bundles.1104511/#post-7109326
     
    Last edited: Feb 22, 2023
    lejean likes this.
  13. JJRivers

    JJRivers

    Joined:
    Oct 16, 2018
    Posts:
    137
    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.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using System.Text;
    4. using UnityEditor;
    5. using UnityEditor.AddressableAssets.Build;
    6. using UnityEditor.AddressableAssets.Settings;
    7. using UnityEditor.AddressableAssets.Settings.GroupSchemas;
    8. using UnityEditor.SceneManagement;
    9. using UnityEngine;
    10. using UnityEngine.SceneManagement;
    11.  
    12. [InitializeOnLoad]
    13. class RegisterFindUnusedAssets
    14. {
    15.     static RegisterFindUnusedAssets()
    16.     {
    17.         AnalyzeSystem.RegisterNewRule<FindUnusedAssets>();
    18.     }
    19. }
    20.  
    21. /// <summary>
    22. /// 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.
    23. /// 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)
    24. /// </summary>
    25. public class FindUnusedAssets : UnityEditor.AddressableAssets.Build.AnalyzeRules.AnalyzeRule
    26. {
    27.     /// <summary>
    28.     /// Make sure this group has it's Include in Build tag disabled if you don't want them in your builds.
    29.     /// </summary>
    30.     private const string UnusedAssetsGroupName = "UnusedAssets";
    31.     /// <summary>
    32.     /// 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.
    33.     /// </summary>
    34.     private const string CodeLoadedAssetsGroupName = "CodeLoadedAssets";
    35.     private const string FolderSeparator = "/";
    36.  
    37.     public override bool CanFix
    38.     {
    39.         get { return true; }
    40.         set { }
    41.     }
    42.  
    43.     public override string ruleName
    44.     {
    45.         get { return "Find unused assets"; }
    46.     }
    47.  
    48.     [SerializeField]
    49.     Dictionary<AddressableAssetEntry, AddressableAssetGroup> MisplacedEntries = new();
    50.  
    51.     /// <summary>
    52.     /// Addressables Analysis rule to find assets not referenced in any scene
    53.     /// </summary>
    54.     /// <param name="settings">Currently active <see cref="AddressableAssetSettings"/> asset, supplied by Addressables Analyze tool.</param>
    55.     /// <returns>List of issues found, this method should only ever analyze and list issues, fixes are done via <see cref="FixIssues"/></returns>
    56.     public override List<AnalyzeResult> RefreshAnalysis(AddressableAssetSettings settings)
    57.     {
    58.         var restoreOpenScene = EditorSceneManager.GetActiveScene().path;
    59.  
    60.         List<AnalyzeResult> results = new();
    61.  
    62.         List<AddressableAssetEntry> sceneEntries = new();
    63.  
    64.         // Process groups with scenes in them, collecting all dependencies in the scene group.
    65.         foreach ( var group in settings.groups )
    66.         {
    67.             //Skip built-in data, these are shown in addressables groups as well but are not addressables hence don't touch them.
    68.             if ( IsBuiltInData(group) )
    69.             {
    70.                 continue;
    71.             }
    72.  
    73.             // Identify groups that contain scenes, collecting all the dependencies in the group into a guid list associated with that group.
    74.             foreach ( var entry in group.entries )
    75.             {
    76.                 if ( IsSceneAsset(entry) )
    77.                 {
    78.                     if ( !sceneEntries.Contains(entry) )
    79.                     {
    80.                         sceneEntries.Add(entry);
    81.                     }
    82.                 }
    83.             }
    84.         }
    85.  
    86.         List<string> sceneGuids = new();
    87.  
    88.         foreach ( var sceneEntry in sceneEntries )
    89.         {
    90.             // Collect all the dependencies in the scene to the guid list of the scene.
    91.             EditorSceneManager.OpenScene(sceneEntry.AssetPath);
    92.  
    93.             var rootGameObjects = SceneManager.GetActiveScene().GetRootGameObjects().ToList();
    94.  
    95.             List<Object> objects = rootGameObjects.ConvertAll(root => (Object)root);
    96.  
    97.             // CRITICAL; Must use EditorUtility.CollectDependencies, other dependency gathering methods
    98.             // do not list dependencies of overridden prefabs resulting in implicit intra-bundle dependencies.
    99.             var dependencies = EditorUtility.CollectDependencies(rootGameObjects.ToArray());
    100.  
    101.             // Convert dependency objects into GUID lists for fast asset identification.
    102.             foreach ( var rootObject in dependencies )
    103.             {
    104.                 string guid = "";
    105.  
    106.                 if ( AssetDatabase.TryGetGUIDAndLocalFileIdentifier(rootObject, out guid, out long _) )
    107.                 {
    108.                     sceneGuids.Add(guid);
    109.                 }
    110.             }
    111.         }
    112.  
    113.         var guidHashSet = sceneGuids.ToHashSet();
    114.         AddressableAssetGroup unusedAssetsGroup = settings.groups.Where(group => group.Name.Contains(UnusedAssetsGroupName)).First();
    115.         AddressableAssetGroup codeLoadedAssetsGroup = settings.groups.Where(group => group.Name.Contains(CodeLoadedAssetsGroupName)).First();
    116.         int entriesWithNoHits = 0;
    117.  
    118.         foreach ( var group in settings.groups )
    119.         {
    120.             foreach ( var entry in group.entries )
    121.             {
    122.                 //Skip built-in data, these are shown in addressables groups as well but are not addressables hence don't touch them.
    123.                 if ( IsBuiltInData(group) )
    124.                 {
    125.                     continue;
    126.                 }
    127.  
    128.                 // Skip the scenes themselves as they are the basis of finding unused references.
    129.                 if ( IsSceneAsset(entry) )
    130.                 {
    131.                     continue;
    132.                 }
    133.  
    134.                 if ( entry.parentGroup != codeLoadedAssetsGroup && !guidHashSet.Contains(entry.guid) && entry.AssetPath.Contains(FolderSeparator) )
    135.                 {
    136.                     results.Add(new AnalyzeResult { resultName = group.Name + kDelimiter + entry.address, severity = MessageType.Warning });
    137.                     MisplacedEntries.Add(entry, unusedAssetsGroup);
    138.                     entriesWithNoHits++;
    139.                 }
    140.             }
    141.         }
    142.  
    143.         if ( entriesWithNoHits > 0 )
    144.         {
    145.             Debug.LogWarning($"Found {entriesWithNoHits} entries with no hits in any scene.\nConsider reviewing if the assets are actually in use.");
    146.         }
    147.  
    148.         if ( results.Count == 0 )
    149.         {
    150.             results.Add(new AnalyzeResult { resultName = "No issues found." });
    151.         }
    152.  
    153.         EditorSceneManager.OpenScene(restoreOpenScene);
    154.  
    155.         return results;
    156.     }
    157.  
    158.     private static bool IsBuiltInData(AddressableAssetGroup group)
    159.     {
    160.         return group.HasSchema<PlayerDataGroupSchema>();
    161.     }
    162.  
    163.     private static bool IsSceneAsset(AddressableAssetEntry entry)
    164.     {
    165.         return entry.AssetPath.Contains(".unity") && !entry.AssetPath.Contains("com.unity.");
    166.     }
    167.  
    168.     public override void ClearAnalysis()
    169.     {
    170.         MisplacedEntries.Clear();
    171.     }
    172.  
    173.     /// <summary>
    174.     /// TODO; Implement moving the assets to an UnusedAssets group that is not included in build, pre-requisites: filtering for script loaded assets.
    175.     /// </summary>
    176.     public override void FixIssues(AddressableAssetSettings settings)
    177.     {
    178.         int movedAssets = 0;
    179.  
    180.         StringBuilder movedAssetsLog = new();
    181.  
    182.         foreach ( var misplacedEntry in MisplacedEntries )
    183.         {
    184.             settings.CreateOrMoveEntry(misplacedEntry.Key.guid, misplacedEntry.Value, false, false);
    185.             movedAssetsLog.AppendLine(misplacedEntry.Key.address);
    186.             movedAssets++;
    187.         }
    188.  
    189.         settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
    190.  
    191.         Debug.Log($"Moved: {movedAssets} assets to their initial groups." + $"\nMovedAssets:\n" + movedAssetsLog.ToString());
    192.  
    193.         StringBuilder emptyGroupsWarning = new();
    194.         int emptyGroups = 0;
    195.  
    196.         foreach ( var group in settings.groups )
    197.         {
    198.             if ( group.entries.Count == 0 )
    199.             {
    200.                 emptyGroupsWarning.AppendLine($"{group.Name} is now empty, consider removing it if it isn't needed anymore.");
    201.                 emptyGroups++;
    202.             }
    203.         }
    204.  
    205.         if ( emptyGroups > 0 )
    206.         {
    207.             Debug.LogWarning($"There are {emptyGroups} empty groups after fixing Addressables Groupings:\n" + emptyGroupsWarning);
    208.         }
    209.  
    210.         MisplacedEntries.Clear();
    211.     }
    212. }
    213.  
     
    lejean and andreiagmu like this.