Search Unity

Danger: AssetBundle.LoadAllAssets<T>() loads everything.

Discussion in 'Asset Bundles' started by LightStriker, Sep 9, 2019.

  1. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I should have seen it coming, as Resources has the same issue.

    To be able to figure out the type of an Asset, Unity loads it. It's dumb. That's what headers are for (or manifests), but clearly not in this case.

    So if you have an AssetBundle comprising many assets of many types, trying to retrieve only a specific type will make Unity load everything. The solution is to have AssetBundle "per-type" instead.
     
  2. Ryanc_unity

    Ryanc_unity

    Unity Technologies

    Joined:
    Jul 22, 2015
    Posts:
    332
    Actual behavior depends on the type of T passed in. If it's a scripting type such as a MonoBehavior or ScriptableObject, it requires loading of that object to know it's specific scripting type. If it's a native type such as Texture2D, it can determine the type without loading the object and thus will not load it if it is not that type or does not derive from that type.
     
    Last edited: Sep 10, 2019
  3. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Honestly, that should be a bold red warning in the documentation. Having a 250Mb+ AssetBundle and finding out it just loads everything is not a pleasant surprise ;)

    Honestly, I keep saying it, but there should be more data in the headers, such as typing.
     
  4. Ryanc_unity

    Ryanc_unity

    Unity Technologies

    Joined:
    Jul 22, 2015
    Posts:
    332
    Type information is already contained in the headers. The exception is scripting types due to how script instance data is stored. The only case where everything is loaded by LoadAllAssets<T> is if every asset in that bundle is a ScriptableObject. Looking at the source, I think we can add a few more checks to reduce this further, such as if T is not a scripting type. I've added a note to also improve the documentation for the Load* asset bundle pages.
     
  5. nsmith1024

    nsmith1024

    Joined:
    Mar 18, 2014
    Posts:
    870
    Hello Ryanc_unity,

    At run-time i download an asset bundle, I want to extract everything so they all appear as if they are in the resource folder of the running unity app, for example if the asset bundle contained a prefab called "monster" normally i would have to do this

    var prefab=bundle.LoadAsset("monster");
    Instantiate(prefab);

    Instead of doing that, i want to load everything in the downloaded asset bundle so that everything in there acts as if its in the resource folder, so that i can do this directly and not have to the bundle.LoadAsset("monster") step, like this

    Instantate("monster")

    Is this possible?

    Thanks
     
  6. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I don't understand the limitation here.
    Scripting types are referenced by GUIDs. They could also be referenced by an Assembly qualified name, if someone wanted more flexibility, with say third-party libraries.
    There's literally zero reason to have to fully load an object to know what type it is or what it contains if the headers have the proper information.
     
  7. Deleted User

    Deleted User

    Guest

    We ran into some pretty severe memory usage issues in our game due to this issue. It is not clear from the documentation that this method behaves in this way, and it was certainly not expected. Considering the fact that using ScriptableObjects is the way to organize data in Unity, I don't understand how this issue hasn't been taken seriously. Many games only load ScriptableObjects from asset bundles.

    I have brought this issue up with the Unity team through our account manager, but I'd like to give this some public visibility as well, for anyone else who runs into the same problem.

    We managed to find a workaround for this issue by building our own asset manifest files that contain full type information on all the assets contained in a bundle. This allows us to load all assets of a given type without using the LoadAllAssets<T> method, resorting instead to using LoadAsset() to retrieve all assets of a given type, in series. The downside is that we have to maintain a complete registry of the paths of all available assets, keyed by their type, in the app's memory. On the other hand, this is nothing compared to the excess memory consumed by having almost all ScriptableObjects loaded.

    It should be noted that there are some pitfalls to be aware of when taking this approach:

    To build a manifest with complete type information, you'll likely have to rely on the method AssetDatabase.
    GetMainAssetTypeAtPath to figure out what types of asset goes into a bundle. You then have to keep in mind that this method may give you a type that will not be understood by the built player. For instance, some assets in the UnityEngine namespace are actually instances of a type from the UnityEditor namespace when you run in the editor. One example of this is the AudioMixer: in the editor these assets are actually of type UnityEditor.Audio.AudioMixerController, which derives from UnityEngine.Audio.AudioMixer. The built player does not include the UnityEditor assembly and can therefore only deserialize these assets as the more base class AudioMixer. To account for this issue you can either include the full type tree (the type and all of its base types) for each asset type in the custom manifest, or you can substitute editor-only types for a more base type that is not editor-only. The first solution is more practical since it's difficult to know exactly which types are not going to be included in a built player.

    It may be challenging to account for assets that are implicitly included (not directly assigned) to bundles, since the AssetDatabase methods for dealing with asset bundles can only tell you which assets have been explicitly assigned to any given bundle. You might get around this by parsing Unity's own manifest file, which (I think?) includes the full list of all included assets. You can also use the asset dependency methods in AssetDatabase to figure out which assets will be implicitly included. In our case, we just ensure everything we wish to load is assigned to a bundle.

    It's also probably a good idea to not include a custom manifest of this kind inside of the asset bundle, since this may break the caching between builds and therefore result in much longer build times.

    I'm uncertain whether AssetBundle.LoadAllAssets<T> is capable of finding and loading nested assets (i.e. a ScriptableObject attached to another ScriptableObject), but it will definitely not be possible if you implement the solution outlined above, since it relies on AssetDatabase.GetMainAssetTypeAtPath which only cares about the main asset. It may be possible to get around this by using AssetDatabase.GetTypeFromPathAndFileID to build a more complete manifest, including the types of sub-assets. In our case we just don't load nested assets.

    Apart from that, there's the obvious memory overhead of keeping information about all available assets in memory, but this is likely preferable to loading a bunch of assets you don't need.