DEV Community

Attilio Carotenuto
Attilio Carotenuto

Posted on • Edited on

Unity Asset Bundles, Tips and Pitfalls

Note: This blogpost is also available on Unity's official blog: https://unity.com/blog/engine-platform/unity-asset-bundles-tips-pitfalls

Asset Bundles are archive files containing assets for your game. They are used to split your game into logical blocks, allowing you to deliver and update content on demand while making your game build smaller. They’re also commonly used to deliver patches and DLCs for your game. Asset Bundles can contain all sorts of assets, such as Prefabs, Materials, Textures, Audio Clips, Scenes, and more, but they can’t include scripts.

Previously, it was necessary to build Asset Bundles manually, marking each asset accordingly, then tracking and resolving dependencies by yourself at runtime. Nowadays, all of this is taken care of by the Addressables system, which will build Asset Bundles for you based on the Asset Groups you define, as well as loading and handling dependencies transparently.

While there are a lot of guides on how Asset Bundles work, I’d like to cover some lesser-known aspects of the system, with a focus on game performance, memory runtime usage, and general compatibility.

Loading and Unloading Assets

Whenever you attempt to use an asset contained within a bundle, Unity ensures the corresponding bundle is loaded into memory, then in turn loads the asset in memory.

While it’s possible to partially load specific assets within an Asset Bundle, the opposite is not allowed. This means that as soon as an asset within an asset bundle is loaded, it can only be unloaded if the entire group of assets is no longer needed.

While technically Resources.UnloadUnusedAssets could be used, this is discouraged especially when using Addressables. The Addressables system keeps a reference count for the bundles, and each asset loaded, and calling this method can result in objects being unloaded that the Addressables system is still tracking, potentially leading to crashes when they are then loaded again.

As a result, if your bundle structure is not ideal, you will often see increasing runtime memory usage as the game goes on, leading to deteriorating performance and potential crashes. For this reason, it’s best to avoid bundles with a large amount of assets in it, as it will end up taking up a lot of runtime memory and turn into a bottleneck for your game. Instead, aim to pack assets based on how frequently they are going to be loaded and used together.

Engine Versions Compatibility

Asset Bundles are generally forward compatible, so bundles built with older versions of Unity will in most cases work on games built on newer versions of Unity (assuming you do not strip the TypeTree info, as covered later). The opposite is not true, so bundles built on a version of Unity that’s newer than the one used for your game build are unlikely to load correctly.

As the difference in version between the bundle and the engine used for the game build increases, compatibility becomes less likely. There are also cases where the bundle might still be loaded, but the objects contained within the bundle cannot be loaded correctly in the new version of Unity, likely due to a change in the way the objects are serialized, thus creating issues. In that case, you’ll need to rebuild your bundles to maintain compatibility.

There’s also a performance cost in loading bundles from a different version of Unity, as covered in the TypeTree section below.

For these reasons, it’s recommended to test thoroughly whenever you update the Unity version of your game build against existing Asset Bundles, and to also update them whenever possible.

Cross-Platform compatibility

Asset Bundles do not generally offer cross-platform support. While in the Editor, you will be able to load bundles from another target platform, however on-device this will fail.

This is still true for bundles that contain assets that are not necessarily platform-specific.

The reason for this limitation is that data might be optimized or compressed in ways that only work for the target platform. Also, bundles can contain platform-specific data that should not be shared between different platforms, so this prevents leaking content that is not intended for another platform.

Loading Cache

The Loading cache is a shared pool of pages where Unity stores recently accessed data for your Asset Bundles. This is global, so it’s shared between all Asset Bundles within your game.

This has been introduced fairly recently, I believe on Unity 2021.3, then backported to 2019.4. Before this, Unity relied on separate caches for each Asset Bundle, which resulted in significantly higher runtime memory usage (covered below in “Serialized File Buffers”).

By default this is set to 1MB, but can be changed by setting AssetBundle.memoryBudgetKB.

The default cache size should be enough in most cases, although there are some scenarios where changing it might bring benefits to your game. For example, if you have bundles with a lot of small objects contained within, increasing the cache size might lead to more cache hits, improving performance for your game.

Additional Internal Data

Along with your game assets, Asset Bundles include a bunch of extra information and headers, used by Unity to know which assets to load and how, as well as a dedicated cache (depending on the Unity version you are using).

Table of Content

A map of the assets in a bundle. It’s what allows you to lookup and load each individual asset in the bundle by name. Its size in memory is normally not a concern, unless you have exceptionally large asset bundles containing thousands of objects.

Preload Table

The Preload Table lists the dependencies of each asset contained within your bundle. It’s used by Unity to correctly load and construct assets.

This can become quite large if the assets contained in your bundle have a lot of explicit and implicit dependencies, as well as cascading dependencies coming from other bundles. For this reason (and many others), it’s a good idea to design your bundles to minimize the dependency chain.

TypeTrees

TypeTrees define the serialized layout of the objects contained in the Asset Bundles.

Their size depends on how many different types of objects are contained within the bundle. For this reason, it’s a good idea to avoid large bundles where objects of many different types are mixed together.

TypeTrees are necessary to maintain compatibility when upgrading the Unity version of your game build while still trying to load Asset Bundles built on older versions of the engine. For example, if the format or the structure of the object have changed, they allow you to do a Safe Binary read so Unity can attempt to load it regardless. This has a performance cost, so in general it’s recommended to update bundles whenever possible when you update the engine.

It can optionally be disabled, by setting the BuildAssetBundleOptions.DisableWriteTypeTree flag when building your bundles. This will make your bundles and the related memory overhead smaller, but it also means that you’ll need to rebuild all your bundles whenever you update the engine version of your game build. This is especially painful if you rely on bundles built from your players for user-generated content, so unless you have a very strong reason to do so, it’s recommended to keep TypeTrees enabled.

One case where TypeTrees can normally be safely disabled is for bundles included directly in your game build. In this case, upgrading the engine would require making a new game build and new Asset Bundles anyway, so its retrocompatibility aspect isn’t relevant.

Each bundle has their own TypeTrees, so having multiple small bundles containing the same type of objects will slightly increase the total size on disk. On the other hand, when loaded, TypeTrees are stored in a global cache in memory, so you won’t incur a higher runtime memory cost if multiple asset bundles are storing the same type of objects.

Serialized File Buffers

Note: Since Unity 2019.4, this has been replaced by a global, shared Loading cache, as described above.

When an Asset Bundle is loaded, Unity allocates internal buffers to store their serialized files into memory.

Regular Asset Bundles contain one serialized file, while Streaming Scene Asset Bundles contain up to two files for each scene contained in that bundle. The size of these buffers depends on the platform. On Switch, PlayStation, and Windows RT it will be 128KB, while all other platforms have 14KB buffers.

For this reason, it’s best to avoid having a large amount of very small asset bundles, since the memory occupied by these buffers might become significant compared to the assets they actually provide.

CRC Integrity Checks

A CRC (Cyclic Redundancy Check) is used to do checksum validation of your Asset Bundles, ensuring the content delivered to your game is exactly what you expect. CRCs are calculated based on the uncompressed content of the bundle.

On consoles, Asset Bundles are normally included as part of the title installation on local storage or downloaded as DLCs, which makes CRC checks unnecessary. On other platforms, such as PC or Mobile, it’s important to do CRC checks on bundles downloaded from a CDN. This is to ensure the file is not corrupted or truncated, leading to potential crashes, and also to avoid potential tampering.

CRC checks are fairly expensive in terms of CPU usage, especially on consoles and mobile. For these reasons, it’s normally a good compromise to disable CRC checks on local and on cached bundles, enabling them only on non-cached remote bundles.

Reduced overhead on Asset Lookup

By default, Unity offers three ways to lookup assets within bundles:

  • Project Relative Path (Assets/Prefabs/Characters/Hero.prefab)
  • Asset Filename (Hero)
  • Asset Filename with Extension (Hero.prefab)

While this is convenient, it comes at a cost. In order to support the last two methods, Unity needs to build lookup tables, which can consume a significant amount of memory for large bundles.

In addition, loading assets using a different method than Project Relative Path will incur a performance cost, again because of the table lookup required.

For these reasons, it’s recommended to avoid using those methods. You can even disable them when the Asset Bundles are built, which will improve loading performance for your asset bundles, and runtime memory usage.

To do that, you can set these two flags when building your bundles:
BuildAssetBundleOptions.DisableLoadAssetByFileName
BuildAssetBundleOptions.DisableLoadAssetByFileNameWithExtension

Note that, when using Addressables and the Scriptable Build Pipeline, this won’t be relevant as these flags are set by default.

References

(1) https://blog.unity.com/engine-platform/behind-the-scenes-speeding-up-unity-workflows
(2) https://docs.unity3d.com/Packages/com.unity.addressables@1.20/manual/MemoryManagement.html
(3) https://docs.unity3d.com/2021.3/Documentation/ScriptReference/AssetBundle-memoryBudgetKB.html

Top comments (0)