Efficient streaming of assets in and out of memory is a key element of any quality game. As a consultant on our Professional Services team, I’ve been striving to improve the performance of many customer projects. That’s why I’d like to share some tips on how to leverage the Unity Addressable Asset System to enhance your content loading strategy.
Memory is a scarce resource that you must manage carefully, especially when porting a project to a new platform. Using Addressables can improve runtime memory by introducing weak references to prevent unnecessary assets from being loaded. Weak references mean that you have control over when the referenced asset is loaded into and out of memory; the Addressable System will find all of the necessary dependencies and load them, too.
This blog will cover a number of scenarios and issues you can run into when setting up your project to use Unity Addressable Asset System – and explain how to recognize them and promptly fix them.
- We have an InventoryManager script in the scene with references to our three inventory assets: Sword, Boss Sword, Shield prefabs.
- These assets are not needed at all times during gameplay.
You can download the project files for this example on my GitHub.
We’re using the preview package Memory Profiler to view memory at runtime. In Unity 2020 LTS, you must first enable preview packages in Project Settings before installing this package from the Package Manager.
If you’re using Unity 2021.1, select the Add package by name option from the additional menu (+) in the Package Manager window. Use the name “com.unity.memoryprofiler”.
Stage 1: Hard references, no Addressables
Let’s start with the most basic implementation and then work our way toward the best approach for setting up our Addressables content. We will simply apply hard references (direct assignment in the inspector, tracked by GUID) to our prefabs in a MonoBehaviour that exists in our scene.
When the scene is loaded, all objects in the scene are also loaded into memory along with their dependencies. This means that every prefab listed in our InventorySystem will reside in memory, along with all the dependencies of those prefabs (textures, meshes, audio, etc.)
As we create a build and take a snapshot with the Memory Profiler, we can see that the textures for our assets are already stored in memory even though none of them are instantiated.
Problem: There are assets in memory that we do not currently need. In a project with a large number of inventory items, this would result in considerable runtime memory pressure.
Stage 2: Implement Addressables
To avoid loading unwanted assets, we will change our inventory system to use Addressables. Using Asset References instead of direct references prevents these objects from being loaded along with our scene. Let’s move our inventory prefabs to an Addressables Group and change InventorySystem to instantiate and release objects using the Addressables API.
Build the Player and take a snapshot. Notice that none of the assets are in memory yet, which is great because they have not been instantiated.
Instantiate all the items to see them appear correctly with their assets in memory.
Problem: If we instantiate all of our items and despawn the boss sword, we will still see the boss sword’s texture “BossSword_E ” in memory, even though it isn’t in use. The reason for this is that, while you can partially load asset bundles, it’s impossible to automatically partially unload them. This behavior can become particularly problematic for bundles with many assets in them, such as a single AssetBundle that comprises all of our inventory prefabs. None of the assets in the bundle will unload until the entire AssetBundle is no longer needed, or until we call the costly CPU operation Resources.UnloadUnusedAssets().
Stage 3: Smaller Bundles
To fix this problem, we must change the way that we organize our AssetBundles. While we currently have a single Addressable Group that packs all of its assets into one AssetBundle, we can instead create an AssetBundle for each prefab. These more granular AssetBundles alleviate the problem of large bundles retaining assets in memory that we no longer need.
Making this change is easy. Select an Addressable Group, followed by Content Packaging & Loading > Advanced Options > Bundle Mode, and go to Inspector to change the Bundle Mode from Pack Together to Pack Separately.
By using Pack Separately to build this Addressable Group, you can create an AssetBundle for each asset in the Addressable Group.
The assets and bundles will look like this:
Now, returning to our original test: Spawning our three items and then despawning the boss sword no longer leaves unnecessary assets in memory. The boss sword textures are now unloaded because the entire bundle is no longer needed.
Problem: If we spawn all three of our items and take a memory capture, duplicate assets will appear in memory. More specifically, this will lead to multiple copies of the textures “Sword_N” and “Sword_D”. How could this happen if we only change the number of bundles?
Stage 4: Fix duplicate assets
To answer this question, let’s consider everything that goes into the three bundles we created. While we only placed three prefab assets into bundles, there are additional assets implicitly pulled into those bundles as dependencies of the prefabs. For example, the sword prefab asset also has mesh, material, and texture assets that need to be included. If these dependencies are not explicitly included elsewhere in Addressables, then they are automatically added to each bundle that needs them.
Addressables include an analysis window to help diagnose bundle layout. Open Window > Asset Management > Addressables > Analyze and run the rule Bundle Layout Preview. Here, we see that the sword bundle explicitly includes the sword.prefab, but there are many implicit dependencies also pulled into this bundle.
In the same window, run Check Duplicate Bundle Dependencies. This rule highlights the assets included in multiple asset bundles based on our current Addressables layout.
We can prevent the duplication of these assets in two ways:
- Place the Sword, BossSword and Shield prefabs in the same bundle so that they share dependencies, or
- Explicitly include the duplicated assets somewhere in Addressables
We want to avoid putting multiple inventory prefabs in the same bundle to stop unwanted assets from persisting in memory. As such, we will add the duplicated assets to their own bundles (Bundle 4 and Bundle 5).
In addition to analyzing our bundles, the Analyze Rules can automatically fix the offending assets via Fix Selected Rules. Press this button to create a new Addressable Group named “Duplicate Asset Isolation,” which has the four duplicated assets in it. Set this group’s Bundle Mode to Pack Separately to prevent any other assets no longer needed from persisting in memory.
Stage 5: Reduce Asset Bundle metadata size in large projects
Using this AssetBundle strategy can result in problems at scale. For each AssetBundle loaded at a given time, there is memory overhead for AssetBundle metadata. This metadata is likely to consume an unacceptable amount of memory if we scale this current strategy up to hundreds or thousands of inventory items. Read more about AssetBundle metadata in the Addressables docs.
View the current AssetBundle metadata memory cost in the Unity Profiler. Go to the memory module and take a memory snapshot. Look in the category Other > SerializedFile.
There is a SerializedFile entry in memory for each loaded AssetBundle. This memory is AssetBundle metadata rather than the actual assets in the bundles. This metadata includes:
- Two file read buffers
- A type tree listing every unique type included in the bundle
- A table of contents pointing to the assets
Of these three items, file read buffers occupy the most space. These buffers are 64 KB each on PS4, Switch, and Windows RT, and 7 KB on all other platforms. In the above example, 1,819 bundles * 64 KB * 2 buffers = 227 MB just for buffers.
Seeing as the number of buffers scales linearly with the number of AssetBundles, the simple solution to reduce memory is to have fewer bundles loaded at runtime. However, we’ve previously avoided loading large bundles to prevent unwanted assets from persisting in memory. So, how do we reduce the number of bundles while maintaining granularity?
A solid first step would be to group assets together based on their use in the application. If you can make intelligent assumptions based on your application, then you can group assets that you know will always be loaded and unloaded together, such as those group assets based on the gameplay level they are in.
On the other hand, you might be in a situation where you cannot make safe assumptions about when your assets are needed/not needed. If you are creating an open-world game, for example, then you cannot simply group everything from the forest biome into a single asset bundle because your players might grab an item from the forest and carry it between biomes. The entire forest bundle remains in memory because the player still needs one asset from the forest.
Fortunately, there is a way to reduce the number of bundles while maintaining a desired level of granularity. Let’s be smarter about how we deduplicate our bundles.
The built-in deduplication analyze rule that we ran detects all assets that are in multiple bundles and efficiently moves them into a single Addressable Group. By setting that group to Pack Separately, we end up with one asset per bundle. However, there are some duplicated assets we can safely pack together without introducing memory problems. Consider the diagram below:
We know that the textures “Sword_N” and “Sword_D” are dependencies of the same bundles (Bundle 1 and Bundle 2). Because these textures have the same parents, we can safely pack them together without causing memory problems. Both sword textures must always be loaded or unloaded. There is never concern that one of the textures might persist in memory, as there is never a case where we specifically use one texture and not the other.
We can implement this improved deduplication logic in our own Addressables Analyze Rule. We will work from the existing CheckForDupeDependencies.cs rule. You can see the full implementation code in the Inventory System example.
In this simple project, we merely reduced the total number of bundles from seven to five. But imagine a scenario where your application has hundreds, thousands, or even more duplicate assets in Addressables. While working with Unknown Worlds Entertainment on a Professional Services engagement for their game Subnautica, the project initially had a total of 8,718 bundles after using the built-in deduplication analyze rule. We reduced this to 5,199 bundles after applying the custom rule to group deduplicated assets based on their bundle parents. You can learn more about our work with the team in this case story.
That is a 40% reduction in the number of bundles, while still having the same content in them and maintaining the same level of granularity. This 40% reduction in the number of bundles similarly reduced the size of SerializedFile at runtime by 40% (from 311 MB to 184 MB).
Using Addressables can significantly reduce memory consumption. You can get further memory reduction by organizing your AssetBundles to suit your use case. After all, built-in analyze rules are conservative in order to fit all applications. Writing your own analyze rules can automate bundle layout and optimize it for your application. To catch memory problems, continue to profile often and check the Analyze window to see what assets are explicitly and implicitly included in your bundles.
Check out the Addressable Asset System documentation for more best practices, a guide to help you get started, and expanded API documentation.
If you’d like to get more hands-on help to learn how to improve your content management with the Addressable Asset System, contact Sales about a professional training course, Manage Content with the Addressable Asset System. We’re currently offering this training through scheduled live public sessions, though it will be available on-demand as well.