My Addressables Feedback #2

I was trying to integrate Addressables in an existing project and in one project that we build from scratch. This is my feedback for you.

This post took me quite some time to write, so please don’t feel offended or think I’m trying to put your work in a bad spot. I’m providing this feedback to help you to improve Addressables, which helps me with my own work too.

Missing AssetReference / GUID / AssetPath / Address lookup table

I use the AssetReference class to store references to scenes. However, I didn’t find a way to check if the scene from that AssetReference is loaded already.

The AssetReference.RuntimeKey represents the GUID, but I didn’t find a practical way to look-up the assetName or assetPath for that GUID. Why would I want this I hear you ask. Because to check if a scene is loaded (UnityEngine.SceneManager) I need the scene name or path.

A lookup table in Addressables that translates a GUID to its assetPath and vica versa would be tremendously helpful. A GetSceneByGUID method in the UnityEngine.SceneManager API could be useful too.

Missing lookup table workaround

My workaround to the missing lookup table is to implement one myself.

However, this shouldn’t be necessary, because this information must exist in Addressables already. How would you map an AssetReference to the Address otherwise?!

My approach is to have a Component that holds AssetReference’s to all addressable assets in the project. At initialization, I trigger a gazillion Addressables.LoadResourceLocationsAsync operations and then store the result in my own data-structure that the game then uses to quickly look up meta information of an AssetReference.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;

public class AssetReferenceLookup : MonoBehaviour
{
    [SerializeField] List<AssetReference> m_AssetReferences = default;

    public struct Entry
    {
        public string guid;
        public string address;
        public bool valid;
    }

    static List<Entry> s_Lookup = new List<Entry>();

    public static bool isReady
    {
        get;
        private set;
    }

    public static Entry Find(AssetReference assetReference)
    {
        return Find(assetReference.RuntimeKey as string);
    }

    public static Entry Find(string key)
    {
        var index = FindIndex(key);
        if (index != -1)
            return s_Lookup[index];

        Dbg.Error(null, $"Could not find addressable entry for '{key}'.");
        var r = new Entry();
        r.address = key;
        r.guid = key;
        return r;
    }

    static int FindIndex(string key)
    {
        for (var n = 0; n < s_Lookup.Count; ++n)
        {
            var e = s_Lookup[n];

            if (string.Equals(e.guid, key, System.StringComparison.OrdinalIgnoreCase))
                return n;

            if (string.Equals(e.address, key, System.StringComparison.OrdinalIgnoreCase))
                return n;
        }

        return -1;
    }

    IEnumerator Start()
    {
        isReady = false;

        yield return null;
        yield return Addressables.InitializeAsync();

        var asyncOperations = new Dictionary<AssetReference, UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<IList<UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation>>>();
        foreach(var r in m_AssetReferences)
        {
            var op = Addressables.LoadResourceLocationsAsync(r);
            asyncOperations.Add(r, op);
        }

        var isDone = false;
        while (!isDone)
        {
            isDone = true;
            foreach (var pair in asyncOperations)
            {
                if (!pair.Value.IsDone)
                {
                    isDone = false;
                    break;
                }
            }

            yield return null;
        }

        foreach(var pair in asyncOperations)
        {
            foreach(var r in pair.Value.Result)
            {
                var e = new Entry();
                e.guid = pair.Key.RuntimeKey as string;
                e.address = r.PrimaryKey;
                e.valid = true;
                s_Lookup.Add(e);
            }
        }

        isReady = true;

        //foreach(var e in s_Lookup)
        //{
        //    Dbg.Log(null, $"address: {e.address}, guid: {e.guid}");
        //}
    }

    void OnDestroy()
    {
        isReady = false;
        s_Lookup = new List<Entry>();
    }
}

Using the Address as SceneName hack

In order to find out whether a scene is loaded already I need the scene name or path.

But since I don’t seem to be able to lookup the assetName/Path from an AssetReference, I use the Address as name instead.

In the code I then just assume (and hope) that the specified address really is the scene name and use it in SceneManager.GetSceneByName(address).

This is a hack to be honest and can easily fall apart if the Addressables address and scene name do not match. For example, if the scene was renamed and someone forgot to rename its address too.

Why the need for the custom AssetReferenceLookup?

Perhaps you’re still wondering why I store the guid / address mapping in my own data structure and not use Addressables.LoadResourceLocationsAsync where I need it?

Because LoadResourceLocationsAsync doesn’t allow to quickly lookup the Address.

For example the game features an achievement/trophy “Enter secret level something”. The game code checks if the player entered a certain scene like this:

public class AwardEnterScene : Award
{
    [SerializeField] AssetReference m_SceneReference;

    void OnSceneEntered()
    {
        var address = AssetReferenceLookup.Find(m_SceneReference).address;
        if (SceneManager.GetActiveScene().name == address)
            Debug.Log("Trigger award");
    }
}

AssetReferenceLookup is the code I implemented to workaround using Addressables.LoadResourceLocationsAsync.

Otherwise I would have to implement async handling in many places throughout the game code, where it actually has no benefit and would just over-complicate things.

Without AssetReferenceLookup I guess the code would look something like this instead:

public class AwardEnterScene : Award
{
    [SerializeField] AssetReference m_SceneReference;

    void OnSceneEntered()
    {
        this.StartCoroutine(OnSceneEnteredCoroutine());
    }

    System.Collections.IEnumerator OnSceneEnteredCoroutine()
    {
        var op = Addressables.LoadResourceLocationsAsync(m_SceneReference);
        yield return op;

        if (!op.IsValid())
        {
            Debug.LogError("error");
            yield break;
        }

        foreach (var r in op.Result)
        {
            if (SceneManager.GetActiveScene().name == r.PrimaryKey)
            {
                Debug.Log("Trigger award");
            }
        }
    }
}

While this technically works, there is no benefit of writing that code in an async way. And there are also many occurences where turning something async is just not feasable.

Missing non-async API

Which leads me to missing non-async methods. Using Addressables over-complicated things for me in various cases, because it features async loading only.

Sometimes I want to load non-async on purpose, especially when I know that the content is stored in StreamingAssets.

Not having the ability to do so limits the usefulness of Addressables and causes me to find workarounds which are often more complex than necessary.

For example, I had something like this to lazy load an audio mixer asset:

public static class SoundMixer
{
    static AudioMixer s_AudioMixer = null;

    public static AudioMixer audioMixer
    {
        get
        {
            if (s_AudioMixer != null)
                return s_AudioMixer;

            s_AudioMixer = Resources.Load<AudioMixer>("AudioMixer");
            return s_AudioMixer;
        }
    }
}

Since Addressables does not allow blocking calls, I would have to rewrite all the code where SoundMixer.mixer is used, to handle its async’ness. This wasn’t feasable!

My workaround was to turn SoundMixer to a Component instead:

public class SoundMixer : MonoBehaviour
{
    [SerializeField] AudioMixer m_AudioMixer = null;

    public static AudioMixer audioMixer
    {
        get;
        private set;
    }

    void Awake()
    {
        audioMixer = m_AudioMixer;
        GameObject.DontDestroyOnLoad(gameObject);
    }
}

This isn’t even a bad change I would say! Now the “SoundMixer” Component must be available at all times and across scene changes. I achieve this by storing a GameObject with the SoundMixer Component in a globals scene and mark it as DontDestroyOnLoad.

Unsupported GameObject.DontDestroyOnLoad

This led me to learn that Addressables does not seem to support GameObject.DontDestroyOnLoad.

I have a “loader” scene which is the first scene that the engine loads and its only responsibility is to load a so called “globals” scene via Addressables.

The globals scene contains all the objects that must stay alive for the entire application uptime. Those objects are set to DontDestroyOnLoad. However, some of those objects have references to assets, such as the SoundMixer Component shown above, which has a reference to an AudioMixer asset.

When the game now loads a scene, Addressables throws out all the assets that are actually used by those global DontDestroyOnLoad objects.

This is blocking me from further integrating Addressables, I filed a bug-report for this issue:
(Case 1297620) Addressables unloads assets that are still in use

The same problem occurs in the loading screen. The loading screen is an additive scene that’s loaded and all its objects are set to DontDestroyOnLoad. When the loading scene then loads the requested game scene, Addressables throws out the loading screen assets.

Again, I have no idea how I can fix this.

Addressables bundles editor-only assets

One problem with Addressables is that you have to mark assets as addressable. Thus I don’t mark every single asset in question as addressable, but an entire directory instead.
This makes it not only easier to work with, it also allows non-tech people with no Addressables knowledge to add content to the game.

However, Addressables pulls in editor assets when a parent directory is marked as addressable.

For example our levels are made of several scenes. We have one directory per level. We mark the “level directory” as addressable, so we can add further scenes to it without having to mark each individual scene as addressable.

Our level building process also generates editor-only level specific assets and stores them in an “editor” folder under the “level directory”. The reason why those assets are stored in the “level directory” is that these are specific/unique to this level only. If we remove the “level directory” all its related assets are deleted too, so we don’t end up with “dead assets” in the project, which is especially useful when creating/testing a lot of level ideas.

However, Addressables pulls editor assets in too and they end up in a build. That’s not useful for me and I didn’t expect that Addressables would do this.

I submitted the following bug-report for this issue:
(Case 1296505) Addressables pulls in assets from an Editor folder

Unforunatelety, you didn’t acknowledge this as an issue and closed the bug-report with the following comment:

It’s beyond me why you think this isn’t an issue or at least a missing feature.

Give us functionality to exclude assets from getting bundled. If you provide such functionality, I could simply add a rule to exclude all “editor” folders myself and Addressables would immediately become a lot more useful.

By the way, this does not only affect user assets, Unity itself stores editor-only assets in a scene directory: (Case 1296514) Addressables outputs "Type LightingDataAsset is in editor assembly" warning

Anyway, since Addressables does not exclude editor-only assets, I thought I could trick the system and mark all those assets with HideFlags.DontSaveInBuild instead.
However, doing this causes Addressables build process to fail.
(Case 1296496) Using HideFlags.DontSaveInBuild causes Addressables to fail
https://forum.unity.com/threads/case-1296496-using-hideflags-dontsaveinbuild-causes-addressables-to-fail.1014991/

Individual loads at the same time

I just learned Addressables has issues with “individual loads at the same time” where the system is sometimes “messing something up”.

This also sounds like a major limitation of the system. Please see my response to this limitation here:

I think I stop here.

  • The unfortunate outcome for me is that Addressables doesn’t seem production ready for me. It seems it’s missing fundamental functionality or perhaps I didn’t find the documentation so I don’t know how to use the system.
  • The few Addressables package updates I have done caused tremendous issues. I would welcome it if you improve your tests and QA.
  • The documentation is lacking and does not meet “Unity standard” yet.
  • The communication with Unity staff in the Addressables sub-forum is non-existent most of the time. Which perhaps wouldn’t even be necessary if the documentation would be better.

Thank you for reading.

PS: In 2018 I was already wrote some Addressables feedback and I guess it’s still spot on:

5 Likes

Check if this could help you in getting the scene name/path :

        var myHandle2 =  Addressables.LoadResourceLocationsAsync(myAssetReference);
        myHandle2.Completed += OnLoadedLocation;

        void OnLoadedLocation(AsyncOperationHandle<IList<IResourceLocation>> obj)
        {
            foreach (var item in obj.Result)
            {
                Debug.Log(item.PrimaryKey); // there are other properties, check them out
            }

        }

According to what they said in one post, the inability to get the key/path with just myAssetReference was a calculated decision, something about wanting a cleaner solution. Maybe storing the path there would incur in memory overhead?
Regarding the non-async load possibility, they also said in a post that it was on purpose (I don’t remember the reason now, probably has to do with cleaniness too), so, don’t get your hopes up.
About the “don’t destroy on load” part: I could be wrong, this is just a hypothesis: Maybe you are using only InstantiateAsync. The InstantiateAsync increases the ref-count by 1 and instantiates the addressable asset, but when you change the scene, maybe the ref-count goes down even if they are not destroyed? If the ref-count goes to 0, the asset is released and you will lose what was instantiated. If my hypothesis is right, all you would need to do it call LoadAssetAsync once for each AssetReference you don’t want to be released (the ref-count generated by this do not decrease when you change the scene).

Thank you for the feedback, @Peter77 . I’m forwarding this over to the team for them to review.

2 Likes

I just wanted to chime in with my experience with Addressables.
I spent a few days playing around with Addressables and decided not to pursue using it further because it would be too much of a headache, and I’m going to try to explain the main issue I ran into.

My use case is what I would consider on the simpler end of the spectrum - I wanted to use addressables to only load in sprites when they are needed, rather than having all my sprites dragged into a sprite manager script in the inspector. (I noticed this method ends up with every sprite loaded into RAM.)
To this end I used “AssetReferenceSprite” and the sample code on GitHub.

Ok here’s my issue.
My app is mostly UI based and I mainly want to use AssetReferenceSprites for UI Images. (For portraits, inventory icons, etc.)
In my existing project my Images are referenced in my “Menu” script, and when I want to change what sprite is set to an image I would call “someImage.sprite = SpriteManager.GetPortraitByID(123);”

But AssetReferenceSprite in the sample is implemented to use this:
singleSpriteReference.LoadAssetAsync().Completed += SingleDone;
Then SingleDone() sets the loaded sprite to a predetermined SpriteRenderer literally hardcoded into the “SingleDone()” method with no way to pass a parameter (like an Image) that you want the sprite set to AND no way to return the sprite out of SingleDone() either…

To bypass this horrible limitation, in some places I’ve seen code that sets a reference to the SpriteRenderer or Image you want the sprite set to before calling the LoadAssetAsync method… but it’s an ASYNC method.
If you ever happen to load 2 sprites consecutively and sprite #2 finishes loading in before sprite #1… well you just got sprite #1 being shown where sprite #2 should be, and whatever renderer you were going to set sprite #1 to is now blank.

To implement AssetReferenceSprite properly I would have to go through adding some kind of manager script on every image object with a reference to the image I want to set on every single image that can be modified via code.
That might be fine (albeit annoying) if I was starting a new project, but the way AssetReferenceSprite is currently implemented seems basically unusable for an existing project, or at very least more trouble than it’s worth.

Or maybe the sample code is just awful and there is a way of doing it that is actually viable in a real world use case?

Hey all, I really appreciate the detailed feedback on this one. I’ll have to go through it in more detail but I wanted to ahead and note a couple things:


1) Missing non-sync APIs
We are actually working on this right now and will hopefully have this available in a preview version of the package in January (I can’t swear to any specific timeframe). When Addressables was first designed we went with a more opinionated approach for the API and have since received many requests to have sync APIs.

2) Not supporting DontDestroyOnLoad
I thought this was documented but maybe I need to double check. If it isn’t we definitely need to document this. The reason we don’t support the use case @Peter77 is running into is that when a scene is loaded via Addressables we keep a reference count on the AssetBundle that loaded that scene. We can’t track when a GameObject moves scenes (which is what happens when you mark an Object as “DontDestroyOnLoad”, it is moved to a special “DontDestroyOnLoad” scene). Even if we could I don’t know that we would want to because it would be us keeping around an entire scene AssetBundle for a handful of Objects.

That said, we have mechanisms in place to allow users to do this. If you want to keep the scene AssetBundle loaded that contained the Objects that have been marked as DontDestroyOnLoad you can call Addressables.ResourceManager.Aquire(mySceneLoadHandle) which will bump the ref count of that AssetBundle until you explicitly Release the handle.

In general, if you’re able to pull those DontDestroyOnLoad objects out of the scene and use Addressables to Instantiate them before calling DontDestroyOnLoad it might be better. That way those objects have their own ref count and AssetBundles to keep track of. But, we realize that that doesn’t work for everyone.

Again, thank you for your feedback.

4 Likes

Wow, this is really great news. Thank you for listening to your community feedback.

Hey David,

thank you for the response. I’m looking forward to your full reply when you had a chance to look at my feedback in more detail.

Thank you for that info. I wasn’t aware of ResourceManager.Acquire, perhaps because it’s not mentioned in the ResourceManager documentation. The lack of documentation could have been another point on my list above, but I mentioned it here already.

Anyway… I tested ResourceManager.Acquire using the example attached to this post , but it doesn’t keep the loaded assets alive.

Either I’m missing something (again) or it really doesn’t work.

I haven’t looked at it in detail, but I maybe the reason could be that the asset is a dependency, with its own assetBundle, of the scene? When I call Acquire on the scene, perhaps it doesn’t bump the ref-count of all its dependencies, thus dependencies are being released?

// The globals scene contains manager objects that must be kept in memory the entire application lifetime
var op = Addressables.LoadSceneAsync("globals", SceneManagement.LoadSceneMode.Additive);
yield return op;

// Release is never called on op anywhere, to keep all its objects the entire application lifetime
Addressables.ResourceManager.Acquire(op);
yield return null;

Oh, lame, there’s a bug when calling UnloadSceneAsync. There’s a section in UnloadSceneOp.UnloadSceneCompleted where we unload the handle again if it’s still valid. We likely added this to ensure bundles weren’t being kept around in memory when they shouldn’t be but we may be releasing one too many times.

As a workaround, if you call Acquire twice on your load scene operation handle it should work (it did in my little test anyway). Definitely a bug though. You can file a ticket or I can make one on our end since the repro is so straightforward. Or both. Thanks for the info.

Have you tried a lambda?

Something like;

singleSpriteReference.LoadAssetAsync<Sprite>().Completed += (AsyncOperationHandle<Sprite> handle) =>
{
    spriteRenderer.sprite = handle.Result;
};

We’ve been looking through this post as a team, and I wanted to add some notes to the parts David hasn’t gotten to yet…

If your AwardEnterScene does the scene loading, then this is straightforward. The load returns a handle to the SceneInstance. From there you could just check if the handle is valid, or use the SceneInstance.Scene to get name or path.
I’m guessing this isn’t the case, however. Instead you are having an AssetReference in AwardEnterScene that you never actually load. You just want to use it as a lookup into the SceneManager. Is that right? That basically you want one system to load the scene, and another to use its own AssetReference to check on the scene. If so, it’s a bit of an unintended use-case for AssetReferences. That’s why there’s no out of the box answer for your use case, but it is still solvable. Obviously your workarounds are viable, but I’d recommend another. Basically it’s to create a dictionary that maps either key to handle, or key to scene path, depending on your exact need. Here the map is empty before you load anything. As you load scenes, the map fills out. This prevents you from doing a ton of up front map-filling as you do in your current setup. If the key isn’t in the map, then the scene hasn’t been loaded.
If you only have one master system doing your scene loading, then the map is easy to create there. If instead anyone could load scenes, then I’d recommend building a lightweight wrapper around Addressables.LoadSceneAsync that creates this map.

As David said, coming soon.

Also covered.

I’m the one that closed the ticket, because, by my reading, it was about trying to treat assets as special because they are in a folder called “editor”. Unity treats scripts in an “editor” folder special. Putting them into a separate assembly from the rest of the game code, and giving that assembly access to UnityEditor code. But Unity doesn’t inherently treat assets in an “editor” folder as any kind of special.
So, “building assets in a folder called editor” is something we’ll continue to do. This is as designed. But, there are two other related issues…

One is our ignoring “HideFlags.DontSaveInBuild”. This is might be a bug, though I’m pretty sure this isn’t the actual intent of this flag. I believe it’s intent is game objects instantiated into a scene in-editor, but not built into the player. Either way, we have a ticket open on our end to look into it, and haven’t dug into it in depth yet. I will say that if you’re going to manually try to flag something as “don’t put it in the build”, this is not the way I’d recommend doing it. My suggestion would be to mark these assets as addressable explicitly in a different group. Then, on that group, uncheck “include in build”. This isn’t exactly the intent of that checkbox, but it’ll get the job done. This is obviously a cumbersome and manual process, but so is tagging each thing with a HideFlag.

Second is the warning we print when an actual editor asset is included in the build (an asset with an editor only script on it). This is particularly relevant as Unity generates some such files next to each scene. Here, we’re still undecided on how to proceed. These files shouldn’t be included in the build, and correctly are not included. The annoyance that worries you is the warning we print about them. Should we stop printing the warning? I don’t think that’s the right fix, because if someone tells us a file is addressable, and we can’t build it, that feels at least like a warning. I think the better fix here is to give you more power over how you mark things as addressable. For example some sort of group or folder filter that says “mark this folder as addressable, but only scene files in it”. That’s a very fuzzy idea, but the point is, I think a blanket silent-fail when editor assets are marked as addressable feels wrong. I realize this may not be super helpful, as I don’t have a clear solution for this, nor a clear timeline. I understand the warnings are bothersome. And I understand that Unity is the one putting files next to your scenes. We just haven’t gotten to a good solution here yet.

As was clarified in the thread you link, we are not saying “we don’t support individual loads at the same time”. We were saying “that sounds like a weird bug, I wonder what could have caused it, as we’ve never seen it. Perhaps some sort of weird race condition. If we can get a way to repro it, we’ll fix it”. So far I’ve not seen the repro for this, and in my experience, I’ve never run into issues loading many things at once.

Shifting to the other feedback

As covered here https://docs.unity3d.com/Packages/com.unity.addressables@1.16/manual/AddressableAssetsAsyncOperationHandle.html there are three different ways to wait for an async operation to finish. One of them is the callback, which @LightStriker_1 pointed out can be used with a lamda for your needs. Either of the other two should also meet your code layout need: yield return or async await.
Or you can wait for our sync APIs to come out soon.

1 Like

Quick update for DoNotDestroyOnLoad where there was a bug when calling UnloadSceneAsync, there is a fix for this set to release in Addressables version 17.7-preview which should be out in the next week or so.

I’m working on a different project now and here we again need to convert GUIDs from and to Address/AssetPath.

This information is available in Addressables, isn’t it? Can’t you add methods to Addressables, similar to AssetDatabase.AssetPathToGUID and AssetDatabase.GUIDToAssetPath?

I’m fighting over this missing functionality in every project that uses Addressables and it costs me/us so much time to find workarounds that usually end up quite complex.

Hey @Peter77 I’ve made a ticket for us to look into doing this. Just as a note, we’ve had to slow down how often we release new public API so even if we do add this it likely won’t be available for a few weeks at the very least. I’ll link this thread in the ticket so we can come back here and update as needed.

1 Like

Depending on the group settings, the address/path may not exist at run time. Or there may be additional keys for all of the labels. At run time, everything is boiled down to a key->location. There are usually more than 1 keys pointing to the same location, so it would be possible to run through all keys and find matching locations but this would not be efficient at all and we do not see the benefit of adding additional bookkeeping overhead for something like this. Exposing new API to process this would require a major version bump as well. I would suggest that you create your own mapping of this information and load it separately from addressables.