Profiling Memory and Reducing Memory Usage using Addressable Assets

Khalid
Mighty Bear Games
Published in
7 min readMar 29, 2022
Photo by Timur Garifov on Unsplash

Memory usage is always a concern for games that are very graphic-heavy. For Disney Melee Mania, the lowest-end devices that were supported were the iPadMini 4 and iPhone 7. Both of them had 2GB of RAM, which can be easily exceeded given the asset fidelity and amount of content that is in the game. It was important for us to keep track of and ensure that we did not exceed the memory budget.

One of the ways we did that was to profile memory usage bi-weekly and capture the snapshot of the memory. In order to get an accurate profile, the profiling was done on the device itself with settings that were close to the production version of the game. In our case, it meant removing debug-related defines. The only difference from the production version is that it had to be a development build to allow the Unity profiler to connect to it.

Using the Xcode Profiler

Once Unity has generated the Xcode project, the project can be built and deployed to the device. On the left-hand panel in Xcode, the sub-panel Debug Navigator will list memory that is being used.

This view is useful as it highlights that iOS will kill the process once it reaches the red bar, at 1.7GB. So in practice, we have less than the expected 2.0GB to use. The memory graph below shows how the memory grows as we play through the app, especially when levels are being loaded and unloaded. Both of these viewes helped us catch an issue where when going into a match, the menu environment was not unloaded before the match environment was loaded, which caused a spike in memory.

Using the In-Built Unity Profiler

To know what objects are taking up the memory, we use Unity’s in-built memory profiler. Here’s how you do that. First, connect the profiler to the device and select Take Sample.

It’ll show how much memory each asset is taking up. It also shows how each asset is being referenced. That’ll point out why it was loaded, or what prevents it from being unloaded. At a glance, we can figure out if an asset is taking up more memory than it should due to incorrect import settings or whether it was unnecessarily loaded.

We typically take a snapshot of memory usage using both profilers in the menu scene, in an offline game, in an online game, and in the menu after playing 5 games. If the memory usage keeps growing after each game, it’s an indication of a memory leak. This means that a player would only be able to play a limited amount of games before the app exceeds the memory limit and crashes.

Using Addressable Assets

One of the surprising aspects of Unity for people unfamiliar to the engine is that resources are loaded into memory only when they are referenced, and not when they are instantiated.

In Disney Melee Mania, we have skins for each character, and each skin has a unique mesh as well as unique materials and textures. We initially stored references to the skin prefab in a scriptable object (skin item) which is referenced through the pool manager. That meant that all the resources for the skins would be loaded in memory regardless if the skin was needed for the match. But since we had plans to add more skins, we had to find an approach that was more scalable.

Fortunately, we found that we could use Addressable Assets to reference an asset without loading it in memory. It can be downloaded from the package manager. Here’s how it works.

First, we need to convert the asset into an addressable asset. Select the asset and in the inspector, tick the checkbox next to the addressable label.

The text next to the addressable label is the address of the asset. We can then use an AssetReference to link to the prefab.

[SerializeField]
public AssetReferenceGameObject BattleAddressable;

Using the AssetReference, only the address would be serialised. Without a direct reference, the game object’s resources would not be automatically loaded. Once the asset is converted to an addressable asset, it’ll be listed in the Addressable Groups panel.

For assets to be usable in a build, they need to be packaged in an asset bundle. To do that, select the New Build command asshown above. The asset bundles will need to be built for each platform and whenever there are changes to the assets.

Converting an asset to be an addressable asset and changing how assets are referenced and loaded is preferably something that should be setup at the very start of the project to minimise refactoring.

For Disney Melee Mania, to minimise changes to our existing codebase, there’s still a field(BattleCharacter) in the skin item that is used to reference the skin prefab but that field is no longer serialised. Instead, we use an AssetReferenceGameObject to serialise the reference to the skin prefab.

When we do need to use the asset, we ensure that the BattleCharacter field is set correctly by loading the asset using the AssetReference and the Addressable System.

var handle = skinItem.BattleAddressable.LoadAssetAsync();
await handle.Task;
skinItem.BattleCharacter =
((GameObject) skinItem.BattleAddressable.Asset).GetComponent<CharacterView>();

In our case, the loading of our match assets are done during the loading phase of the match scene. In that phase, the server will inform us which skins the player has chosen and so we can choose to load only the skins that will be used. Once the asset is loaded, we can populate the pool manager. When the match is done, the BattleCharacter reference will be reset and the asset released. As a note, this approach is only feasible since the skins to be loaded are already predetermined.

skinItem.BattleAddressable.ReleaseAsset();
skinItem.BattleCharacter = null;
Resources.UnloadUnusedAssets();

While iterating on the solution, we found out that it’s not necessary to build to a device and profile the memory every time, since how the addressable assets are being loaded and unloaded is accurate when using the profiler in the editor.

Using the Unity Memory Profiler Package

Another useful memory tool is Unity’s Memory Profiler package which can be downloaded from the package manager. It generates a nice overview map to see how memory is being used and it can be zoomed it to narrow down on a specific category of assets.

It allows us to capture snapshots of the memory at different points in the game or with different versions of the game and store that in a file.

We used it to detect memory leaks caused by playing a match by taking a snapshot in the menu scene before a match is played and after 5 matches. The diff of the snapshots would let us know which assets lingered in memory after playing a match. In the example below, I grouped the changes by the Diff column, filtered for textures and sorted by memory usage. The New category shows the textures that has been added compared to the base snapshot.

Memory usage can slowly increase throughout development. To show how the memory usage has changed compared to the previous release as a whole, we can store the previous release’s snapshot and compare it with the current version of the game.

Conclusion

Catching any memory issues early on is vital as it takes time to profile and optimise memory usage. You do not want to refactor code and reconfigure assets when a deadline is approaching, as that could cause more bugs to occur.

Currently memory profiling is a manual process and can be time-consuming. But that is something we are thinking of automating. We should be able to take memory snapshots at specific moments of the game and then upload it into repository as part of our Continuous Integration pipeline.

The Addressable Asset system helps to manage memory better by giving us better control on when resources are loaded. However, it does require substantial code changes. We were able to make adjustments to accommodate it this time, but come the next project, we’ll be setting it up right at the start of development.

If you found this article helpful, drop me some claps or give the Mighty Bear Games publication a follow!

--

--