Search Unity

  1. Get all the Unite Berlin 2018 news on the blog.
    Dismiss Notice
  2. Unity 2018.2 has arrived! Read about it here.
    Dismiss Notice
  3. Improve your Unity skills with a certified instructor in a private, interactive classroom. Learn more.
    Dismiss Notice
  4. ARCore is out of developer preview! Read about it here.
    Dismiss Notice
  5. Magic Leap’s Lumin SDK Technical Preview for Unity lets you get started creating content for Magic Leap One™. Find more information on our blog!
    Dismiss Notice
  6. Want to see the most recent patch releases? Take a peek at the patch release page.
    Dismiss Notice

My Addressables Feedback

Discussion in 'Addressables' started by Peter77, Jun 24, 2018.

  1. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    2,362
    Hi,

    I looked at the AddressableAssetSystem_Demo and Addressable System 0.1.2 and would like to provide some feedback, because I wrote a system that seems to be similar in a few ways, which we used several years and thus have a few learnings that I would like to share with you.

    First, I would like to describe our "AssetReference" approach, what was bad about it and what seems to be superior to the current Addressable implementation. I called it WeakAssetReference, which I also use in the following text to make a distinction between your AssetReference and the one from our system.

    AssetReference

    The implementation looks like:
    Code (CSharp):
    1. struct WeakAssetReference
    2. {
    3.    public string assetGUID;
    4.    #if UNITY_EDITOR || DEBUG
    5.    public string editorAssetPath;
    6.    #endif
    7.  
    8.    UnityEngine.Object cachedAsset;
    9.  
    10.    public T Load<T>() where T : UnityEngine.Object
    11.    {
    12.        if (cachedAsset != null) return cachedAsset as T;
    13.  
    14.        // load asset in blocking fashion. if preloaded already,
    15.        // it's just an table lookup for the loaded asset and fast.
    16.        cachedAsset = ContentManager.Load(this);
    17.        return cachedAsset;
    18.    }
    19. }
    The assetGUID was the actual guid the editor uses to reference assets (AssetDatabase.GUIDFromAssetPath). The editorAssetPath was available in the editor and debug builds, it represents the last known path of the asset in the project. This was useful, because if an asset could not be loaded (anymore), the assetGUID wasn't of great help, but looking at the editorAssetPath gave us an idea what happened to the asset.

    WeakAssetReference was implemented as a ValueType, to avoid additional null-reference checks.

    I implemented a custom Editor to mimic the look and feel of a standard EditorGUI.ObjectField for a WeakAssetReference. A WeakAssetReference feels the same from an usage point of view, whether you use a direct reference or a WeakAssetReference. The only difference it makes for the user is a tiny icon next to the "ObjectField picker" that indicates it's a WeakAssetReference.

    It was important to me to make it feel like a standard reference, to make it easier for everybody on the team to adopt. Everybody knew already how to drag&drop assets in an EditorGUI.ObjectField or how to use the Object Picker.

    Using a WeakAssetReference looks like this:
    Code (CSharp):
    1. class Player : MonoBehaviour
    2. {
    3.    [WeakAssetReferenceAttribute(typeof(GameObject))]
    4.    public WeakAssetReference explosion;
    5.  
    6.    void Awake()
    7.    {
    8.        // async (pre)load the asset
    9.        ContentManager.Preload(explosion);
    10.    }
    11.  
    12.    void Update()
    13.    {
    14.        if (Input.GetButton("Fire")
    15.        {
    16.            // prefab preloaded already, otherwise blocks until loaded
    17.            var prefab = explosion.Load<GameObject>();
    18.  
    19.            // do something with the prefab
    20.        }
    21.    }
    22. }
    The [WeakAssetReferenceAttribute] provided additional information for the custom WeakAssetReference editor. It allowed to specify what asset type the WeakAssetReference expects. This was also very helpful, to avoid that someone perhaps adds a reference to a Mesh, where the code actually expects it to be a GameObject.

    We used Awake() to trigger async-load calls of the specific object during scene loading. The loading-screen is kept alive until all requested assets have been preloaded.

    In order to access the actual asset, one would use WeakAssetRefeerence.Load<Type>() which was a blocking call. If the asset was preloaded already, it was just a value lookup, otherwise the system would load the asset in a blocking fashion.

    The blocking Load call is important to keep the game code simple. The outer-loading-screen makes sure all required assets are loaded already, we didn't want to add more complexity to the game code that is using these assets.


    Addressables

    I'm now going to explain how we achieved something similar as Addressables, I named it "ContentManager".

    The ContentManager is more limited than your Addressables system. We assume assets are always provided via asset bundles and they are located on disk. Downloading asset bundles would be an entirely different code path for us, that is not part of the ContentManager.

    All assets in the project are addressable by default.

    This was very important to us, because we want people without a strong technical background to be able to add and wire assets in the project. For example, if a GameDesigner created a new enemy configuration asset (ScriptableObject), s/he doesn't have to mark that configuration asset to be addressable first, it just works.

    I tested the AddressableAssetSystem_Demo what happens if I create a new prefab in "Assets/SpaceShooter/Prefabs". The Addressable System does not seem to provide the new prefab, it seems I have to add it in the "Addressables" window first, before the AssetReference GUI allows me to select that prefab. I imagine this workflow would not have worked in our team.

    How did we achieve addressables by default?

    We assume to load assets by their "Assets" directory location. If I have a prefab in "Assets/Prefabs/Player.prefab", this asset can be loaded using that path on all platforms, be it in the editor itself, iOS or Windows Standalone.

    With the WeakAssetReference you don't even have to keep that path around. You only need it if you want to by-pass the WeakAssetReference system and it does make sense in a few cases to by-pass it.

    Our addressables system (ContentManager) made this possible by using what I named an "Asset File Table". The AssetFileTable is a ScriptableObject that contains all paths and guids of the project.

    The AssetFileTable was generated by a custom build step. If I remember correctly, I used the generated assetbundle manifest files to build the AssetFileTable.

    When running the game in the editor, it would just contain AssetDatabase.GetAllAssetPaths and their corresponding guids.

    Here is a simplified example of the AssetFileTableEntry:
    Code (CSharp):
    1. struct AssetFileTableEntry
    2. {
    3.    public string assetGUID;
    4.    public string assetPath;
    5.    public string assetBundle;
    6. }
    If an asset is requested, it would call something like this on the ContentManager:
    (this is heavily simplified)
    Code (CSharp):
    1. class ContentManager
    2. {
    3.    AssetFileTable m_FileTable;
    4.  
    5.    T Load<T>(WeakAssetReference addr) where T : UnityEngine.Object
    6.    {
    7.        AssetFileTableEntry entry = m_FileTable.FindEntryByGUID(addr.assetGUID);
    8.        AssetBundle bundle = GetOrOpenAssetBundle(entry.assetBundle);
    9.        T asset = bundle.LoadAsset(entry.assetPath, typeof(T)) as T;
    10.        return asset;
    11.    }
    12.  
    13.    T Load<T>(string assetPath) where T : UnityEngine.Object
    14.    {
    15.        AssetFileTableEntry entry = m_FileTable.FindEntryByPath(assetPath);
    16.        AssetBundle bundle = GetOrOpenAssetBundle(entry.assetBundle);
    17.        T asset = bundle.LoadAsset(entry.assetPath, typeof(T)) as T;
    18.        return asset;
    19.    }
    20. }
    Sometimes it's useful to construct the location in code and request something like "give me all assets in 'Assets/Prefabs/Weapons'" for example. This was also possible via ContentManager:

    Code (CSharp):
    1. class ContentManager
    2. {
    3.    WeakAssetReference[] FindAssets(string directory, string assetType, bool includeSubDirectories)
    4.    {
    5.     // Find all assets that are part of 'directory' in the asset file table
    6.    }
    7.  
    8.    WeakAssetReference[] FindAssets(WeakAssetReference directory, string assetType, bool includeSubDirectories)
    9.    {
    10.        AssetFileTableEntry entry = m_FileTable.FindEntryByPath(assetPath);
    11.        // entry.assetPath would be the directory such as "Assets/Prefabs/Weapons" for example
    12.        return FindAssets(entry.assetPath, assetType, includeSubDirectories);
    13.    }
    14. }
    Game code uses FindAssets like this:
    Code (CSharp):
    1. void LoadAllWeapons()
    2. {
    3.    WeakAssetReference[] weaponAddresses = ContentManager.FindAssets("Assets/Prefabs/Weapons", "prefab", false);
    4.    foreach(var addr in weaponAddresses)
    5.    {
    6.        GameObject prefab = ContentManager.Load<GameObject>(addr);
    7.    }
    8. }
    We tried to avoid hard-coded paths, the preferred approach was to use a WeakAssetReference that represents a directory:
    Code (CSharp):
    1. class Something
    2. {
    3.    // drag&drop "Assets/Prefabs/Weapons" in this field
    4.    WeakAssetReference m_WeaponDirectory;
    5.  
    6.    void LoadAllWeapons()
    7.    {
    8.        WeakAssetReference[] weaponAddresses = ContentManager.FindAssets(m_WeaponDirectory, "prefab", false);
    9.        foreach(var addr in weaponAddresses)
    10.        {
    11.            GameObject prefab = ContentManager.Load<GameObject>(addr);
    12.        }
    13.    }
    14. }
    What was good about our system

    1) Using this system was easy to use and understand for everybody on the team. This was made possible, because it felt no different than working with normal object references. Drag&Drop assets in WeakAssetReference slots felt no other than using standard hard references.

    2) We don't have to maintain an extra addressables list. This was important, because we have tens of thousands of assets. There is no way to maintain such list by hand. Many team members would also not have the technical knowledge to maintain such list, nor do we actually want people spend time on that. We want them to do what they can best, be it art or sounds, but not adding assets to a specific list.

    3) On the programmer side, it was also useful to have as well. The code to load assets was unified, not matter whether the game runs in the editor or on ios. You write loading code once and the same code works everywhere.

    4) Because we use the asset guid's the editor uses itself, even if you move assets around or rename, our system still works.


    What was bad about our system

    1) WeakAssetReference being "type less" was sometimes causing issues. A programmer added a new field, such as:
    Code (CSharp):
    1. WeakAssetReference explosion;
    This was not very descriptive. Is it a sound, prefab, or whatever? We solved this via an attribute:
    Code (CSharp):
    1. [WeakAssetReferenceAttribute(typeof(GameObject))]
    2. WeakAssetReference explosion;
    But sometimes we also missed to use that attribute. Or the type changed during a refactor. So it would have been way better if we could write something like this:
    Code (CSharp):
    1. WeakAssetReference<GameObject> explosion;
    Unity's serialization system wasn't able to handle this, even though Unity can serialize List<string> for example. It would have been the correct solution for us and I believe it's from advantage for this system if you improve Unity's serialize system to support that.


    2) The AssetFileTable was a very memory inefficient beast. Imagine you have only 10000 assets in that table. That's a lot of paths and guids already, which eat up a memory and put further pressure on the GC. The first implementation had about 14 MB just string's in the AssetFileTable. If I would write such system again, I would avoid string for the assetGUID and use Hash128 for example.

    assetGUID string = 32 chars = 64 bytes memory
    Hash128 = 4 ints = 16 bytes memory

    My optimization for assetPaths was to split the paths into directory, filename and file extension:
    • Assets/Weapons/Prefabs/Lasergun.prefab
    would become:
    • Directory = Assets/Weapons/Prefabs/
    • Filename = Lasergun
    • Fileextension = prefab

    The directory and file-extension would be stored in an array and the AssetFileTableEntry does not use a string, but the integer index of that directory in the array. This made the AssetFileTable more memory efficient, but caused its code to get quite complex and indirect. It still was the same from an usage point of few.

    It was also possible to use "keywords" as file extension. I could use "texture" as file extension to get all textures, no matter whether they were png or tga.



    Unity Addressable and ResourceManager

    If you allow me to transfer this knowledge to the new Addressables system, here is what I think should be revised:

    1) Make all assets addressable by default or allow us to do this, like a "Assets/*" rule in the Addressable window.

    2) Use the Addressable window to add exceptions to the "addressable by default" rule.

    3) Don't use a custom AssetReference UI. Mimic the EditorGUI.ObjectField UI and allow to use the standard Object Picker window. The current implementation of the list might work with 20 assets, but if you have that flat list containing 10000 and more assets, that stops being useful, even with the search field. Also, Unity users love drag&drop, don't take it away from us.

    4) Replace string assetGUID in the AssetReference with Hash128 to save memory
    For debug purposes, in development mode builds and editor, keep a string of the last know path of the asset around in the AssetReference. This helped us so many times.

    5) Add functionality to query addressables for its content "give me all prefabs at this address". I guess it's probably supported already, but I didn't find it.

    6) Having only async Instantiate methods for AssetReference will make it difficult to use. You either have to add a lot of support code that loads the stuff at the beginning and then caches the result, or write more complicated game code that is able to handle async instantiation. Both cases not ideal. I recommend to add a blocking Load and Instantiate method. If the asset isn't loaded, it blocks until it's loaded. If the asset does not exist, return null.

    7) If the "catalog_4.json" file is something like our AssetFileTable, I imagine if this file is kept in memory, it's also going to be a huge memory waste in a large project, just like AssetFileTable was in ours.

    8) Get rid of the additional "adressable" indirection, but use the guid instead.

    9) Support AssetReference<GameObject>
     
    Last edited: Jun 29, 2018
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    3,083
    I agree with most of the points. Note that drag-and-drop is supported - in the demos, they dragged assets onto the drop-down, and that assigned the asset. It's not intuitive at all that that's how the field works, and since it's indistinguishable from other drop-downs, it's a bit confusing.

    For "making everything addressable by default"... I don't know. Having things turn addressable on-assign means that all the things you've ever wanted to be addressable are now addressable, and nothing else is. I guess it depends on workflow - if you generate game content based on the contents of folders rather than by explicitly assigning assets to prefabs or scriptableobjects or scenes or whatnot, it'll have to be automatic in some fashion.
     
    laurentlavigne and Peter77 like this.
  3. M_R

    M_R

    Joined:
    Apr 15, 2015
    Posts:
    157
    I noticed that you can mark a folder asset as Addressable. don't know if/how you can load the contents
     
    Peter77 likes this.
  4. rigidbuddy

    rigidbuddy

    Joined:
    Feb 25, 2014
    Posts:
    20
    Generic drawer & serialization f AssetReferenceT<> is highly needed. Because it gives object filter and protects from runtime type errors.

    Agree that Editor UI could mimic ObjectField UI.

    Actually I've seen similar editor in ExposedReference<> (used in Playables) which was super OK for users and already handled by Unity serializer.
     
    Peter77 likes this.
  5. ScottPeal

    ScottPeal

    Joined:
    Jan 14, 2013
    Posts:
    10
    So the title of this forum is "Addressables Feedback" and I provided feedback that it is not working with 2018.2.0b10/11. Then the moderator deleted my post. How is this not feedback on addressables?

    "Your post in the thread Addressables Feedback was deleted. Reason: Please create a new thread rather than derailing an existing topic."
     
  6. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    2,362
    The title of this forum thread (which I created) is "Addressables Feedback", it's not the title of the forum. It's a thread with my very specific feedback regarding the Addressables System targeted to Unity Technologies.

    If you want to join the discussion and add something to my feedback, this it the place to be. If your question/feedback is not related to my post, it's better to create a new forum thread. This helps to keep the forum clean and organized. One thread per topic/issue, rather than everything in one thread, is normally preferred.

    BTW, if you found a bug, it's also from advantage to submit a bug-report and post the bug-report Case number in your forum thread, rather than posting in the forum only. Unity Technologies often asks for a project/example to reproduce the issue and to avoid discussion ping-pong, it's normally easier to submit that bug-report first and then create the forum thread.

    Hope it helps!

    PS: I renamed the thread title to "My Addressables Feedback", to avoid future confusion.
     
  7. _Daniel_

    _Daniel_

    Joined:
    Feb 28, 2007
    Posts:
    2,560
    I agree with the idea of blocking for loading. Async loading code is quite a bit more complex to write in my experience. Being able to load an asset immediately allows for easy to understand code.
     
  8. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    198
    I don't like the idea of synchronous loading at all due to screen freezes.
    If you have a few assets you want to load in order you can easily create a coroutine where you call the Addressable system to load the asset then yield return them all until they are loaded. Super easy and not even complex code required. You can actually hang a loading bar UI on it that way and it won't freeze your screen while loading the assets.
    Who would want a screen freeze from a synchronous call?...
     
  9. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    2,362
    The point I was trying to make here is to use async loading to preload all assets, for example during a loading screen.

    Then later during gameplay, to access those pre-loaded assets, I don't want to handle async loading and instantiation anymore, because I know I did preload those assets. I just want to use Load/Instantiate, which would block if the asset would not have been pre-loaded, but because it was pre-loaded, it's really just a table lookup to return the asset in question.

    This allows to reduce the game code complexity a fair amount, because you don't have to deal with coroutines/async handling at all. That's the reason why I want additional Load/Instantiate methods that are non-async.
     
  10. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    198
    I still don't understand your case completely with the information you gave, but wouldn't want to devote the attention to that either by continue asking for more info. Also code complexity is debatable, I don't find coroutines or asynchronous systems complex.

    But if I were to build a simple async pre-loading system I'd just let all systems(asset containers) pre-load their required assets. Wait for them all to tell me they have completed loading (simple event) and then remove the loading screen if all were to be complete. Whenever an asset is then required to instantiate it has already been loaded, only would need to call the Instantiate method.

    i.e show Loading screen -> Send Event for Loading Systems -> Systems announce themselves to a list -> start loading the linked addressable asset -> On Complete loading -> save asset in a variable for later use -> Send Event that the system has completed loading -> if all systems that were announced have completed loading, remove loading screen.

    No coroutine required. An Awake / Start would suffice to send the event and an event receiver method that checks on complete if the loading queue is empty or not.
     
  11. rastlin

    rastlin

    Joined:
    Jun 5, 2017
    Posts:
    66
    Your approach basically doubles the asset-lookup table size, which already is within addressables core.

    Async code is infinitely more complex than synchronous fetch of the asset, that's not up for debate. With asynchronous fetch you need to handle cancellation and possibility of assets arriving in different order they ware requested, which may or may not be an issue. If you did not had to handle such cases it does not means that more advanced project can function properly without them.

    By the looks of it the Addressables is great... on paper. Based on Berlin talk there is still so much work required for this approach to be production ready I think it will not be in usable state by the end of 2018. Lack of proper variant support (labelling? c'mon...), no possibility to override the asset, all the valid points Peter77 mentioned. I also think single reference-table asset will be a merge-hell, I liked the old approach of per-asset address much better. The blocking fetch is a must before I can start thinking of switching my project to this package.

    They basically laid out nice API and an answer for most of the questions was "You need to code that yourself" ... which we kind of already do with asset bundles. On the one hand they resolved some of the bigger annoyances of the old system, and on the other they took away some of the core strengths.

    Looking forward for Addressables team to prove me wrong in next few months :) .
     
    Peter77 and _Daniel_ like this.
  12. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    198
    Doesn’t rlly, a few scripts that just listen to an event, announce themselves, load their assets which is addressables.loadasset and on complete do an event back that it is ready.

    Thats just a modular way of handling multiple asset loads that need to be done.
    I.e you want your UI system to load, a list of monster prefabs, environment prefabs.
    just a loading script that keeps track if everything has been loaded or not.
    Its dead simple and doesn’t need a synchronous call, just a few events...
    So thats not rlly double asset lookups.

    I rather not have my game freeze over loading assets, in my case they need to be downloaded from server wouldn’t want to let the game freeze while it may be downloading 50-100mb of assets. Ppl will just kill the game thinking it doesn’t respond due to freezing download+loading calls.

    If thats all it has to do it is pretty much no complexity. All you need is a class with a queue, some events on complete / on error handling. If you want to cancel it you call off the queue... so simple. If you find that infinitely more complex then idk.
    You may have a completely different view on the matter at hand than I have or we’re on 2 completely different skill levels.
     
  13. strich

    strich

    Joined:
    Aug 14, 2012
    Posts:
    242
    @Peter77 so we have pretty much written the exact same solution our end about 6 months ago. Though we made one major improvement - We were able to make our ResourceRef<T> be typed, so no need for attributes or whatever and we wrote custom serialization against our own kind of prefab that held the references. We also used our own UI drawer system to completely replicate the functionality of the editor experience. Works really well. The only more recent improvement we made was we used the Odin Inspector plugin to do all our UI drawer rendering.

    I haven't yet had time to dive into the Addressables system but I'm not yet sure if it is better or worse than our solution - Ours has a non-perfect perf profile, but it is still only 2 hashed Dict lookups per asset per session (We cache).