When creating a game, a solid content loading strategy is a key part of the development process.
Starting the game fast, streaming assets in and out of memory,
all of this has a fundamental role in providing the player with a smooth playing experience.
In this 4th devlog,
we’re going to take a look at the techniques we employed to package the content of “Chop Chop”,
an open-source action-adventure game demo we are making as part of the Open Projects initiative.
As usual, the whole game is available for you to explore on the Github repository,
and you will find the link in the description of this video.
Before we talk about the way we packaged content for Chop Chop,
let’s take a small step back to understand the problem we were trying to solve.
In Unity you structure a game by breaking it down into scenes.
When not using AssetBundles, scenes are then listed one by one in the Build Settings
which defines what ends up being packed into the build.
If a Prefab, a texture, or a material is referenced into one of those scenes, it will end up in the game build.
We call these “direct references”.
This doesn’t give much control over memory
since when we load a scene, we also load all of its direct references.
In addition to that, all assets that sit in the folders named Resources are also included
in one big compressed file included in the build.
Even though we decide when to load these assets, we have limited control on when to unload them.
In addition to this, this file cannot be bigger than 4GB.
Moreover, there can be a hiccup at the beginning of the game when the information about this big archive is first loaded.
This might be imperceptible on a PC,
but on a console it can make a difference between succeeding or being rejected during a certification process.
On mobile, it could mean a frustrated player who uninstalls the game - and a bad review!
For all these reasons, we decided to use AssetBundles to pack our content
and load it at runtime, only when needed.
On top of that, we use the Addressable Assets package to easily configure, pack, build, and load these bundles.
So the first step is defining the content strategy.
In Chop Chop, the game begins with a main menu scene.
From there, we can reach the gameplay phase which happens in scenes that we call Locations,
always loaded one at a time.
In addition to these, there are special scenes that we use for global managers,
in charge of running cross-scene operations like saving and loading progress,
playing music, and loading other scenes.
So we decided that every scene mentioned so far is going to be packed into bundles.
This way, we ensure that the initial loading of the game is very short.
To do this, we simply have to mark scenes and their assets as Addressable and make sure they are in the right group.
Groups are a functionality of Addressables that allows to divide assets into logical units,
to be then packed into AssetBundles when building content.
One important note:
assets in the same Group can be built into multiple AssetBundles by choosing Pack Separately,
so that they are as independent as possible from each other.
However, Unity always requires at least one scene in the build, which is designated as
the entry point of the whole game.
For this reason, we introduced a new, almost-empty scene called Initialisation which,
as you can notice, is the only scene in the Build Settings.
Initialisation is a one-use scene.
Its only function is to load another scene called PersistentManagers,
which is in charge of loading other scenes such as the main menu or Locations.
At this point, Initialisation can unload itself and PersistentManagers takes over.
From here, we’re in AssetBundle territory.
Every new scene loading request will automatically find and load the correct AssetBundle
that contains the scene and all of its dependencies.
To reference these scenes we use a tool coming from the Addressable Assets package
called “AssetReference”.
AssetReference is a special type of field, which looks a lot like a direct reference,
but it’s more like… the promise of a reference.
This way we can have a reference to any asset which can be fulfilled at a later stage,
when we actually need that asset.
You can think of AssetReference fields as weak links that are not immediately connected.
We have control over when to connect them, and when we need to,
the Addressable system will find all of the necessary dependencies and load them too - if they’re not already loaded.
It’s quite convenient.
When you remove these links by unloading references, the asset and its dependencies can be unloaded, freeing memory up.
Now let’s talk about an important topic that can be confusing at first: asset duplication.
While dividing the content into bundles,
we ran into a potential issue - as noted by some of the project’s contributors.
Our game heavily relies on some ScriptableObjects to carry messages across-scenes.
If we packed our content the wrong way, these ScriptableObjects would get duplicated in two or more different bundles.
If that happened the messages wouldn’t travel anymore,
because at runtime the sender and the receiver would be effectively communicating over different objects in memory!
What happens here is that these ScriptableObjects are being packed in the build implicitly,
meaning that while not initially marked as Addressables, they were pulled into bundles due to them being a dependency.
This can cause duplication if multiple objects in different bundles are referencing these ScriptableObjects.
So the solution in this case was to just explicitly declare these assets as Addressables.
The Addressable system will track the dependencies,
realise that the needed assets are already in a bundle, and not pack them again.
This avoids the duplication, and solves the problem with our event system.
Just a side note.
Duplication might be wanted in some instances.
Consider the case of two characters using the same gradient texture,
and let’s assume these two characters never appear in the game at the same time.
In this case, it might be OK to pack the texture twice in the same bundles as the characters,
so that they are available without having to load an additional, shared, AssetBundle.
So ultimately, look at the structure and gameplay of your game,
and design the packing and loading strategy that makes sense for you.
As for us, the next step is to better structure the Addressable Groups
in a way that allows us to load a level, and its dependencies, without pulling in unnecessary assets.
This might be something we will do closer to the end of the project,
since - luckily - the Addressables system doesn’t care where an asset is located,
so we can move assets between Groups later on and we won’t need to change anything in our code.
The development of Chop Chop continues, and it’s not too late to join us on this journey.
To get up to speed, check out the forums and the wiki on the Github repository, linked below.
Thanks for watching, and we hope you are enjoying following the development of Open Projects!