Bootstrapping your Unity game with Addressables
A quick method to ensure you can 'Play From Anywhere' and code with confidence!
In this post, we’ll explore a straightforward way to assert control over your Unity game, keep your scenes clean, and lay the foundations of a flexible, extendable bootstrapper that leans on Unity’s Addressables system.
Read on for some background and reasoning about why such a system is beneficial, or, if you’re already convinced, simply skip ahead to the good stuff!
The Grim Fandango
At some point during your game’s development, you’ll likely begin to encounter friction between different systems. Managing relationships and dependencies between different areas of your game can become a challenge, and the further you are in development, the more fragile this process can become.
Failure to reign in complexity early on can result in a project collapsing under its own weight. The cost of rewriting, refactoring, or replacing systems to restore sanity can very quickly become unbearable, especially for solo devs and small teams.
Over time, as the game grows and things become more complex, it’s common for one or more of the following problems to appear:
Scenes become complex and cluttered with GameObjects that provide system-level behaviour
Entering Playmode requires some kind of arcane ritual to ensure that the right configuration is applied first
The evolution of some ‘blessed path’ that developers must go through in order to test their latest work
A common pattern developers use to overcome these issues is to create multiple scenes which are intended to be loaded simultaneously. One scene might contain the game’s current level - gameplay elements, terrain, enemy characters etc - whilst another may contain various managers and utilities which glue the game together, coordinate different systems, and provide debugging tools and information.
This solution works, but it means that scene management becomes more complex, and often requires the development of additional tools to ensure that the right scenes get loaded at the right time. Developers are required to understand a potentially complex scene configuration, and in the absence of custom tooling to manage things automatically, friction may increase when testing, and new potential points of failure are added each time your scene configuration needs to change.
In a mid-to-large-sized team, it’s very easy for developers to step on each other’s toes, or for complex configuration requirements to be misunderstood or miscommunicated.
Command then Conquer
Over time, I’ve come to believe that the most basic requirement of a well-architected Unity game is that the Play button should always do exactly what its name suggests. If you can’t hit Play and dive into the game from wherever you are, without any additional thought, then there’s a good chance you’ve got a few time bombs waiting to go off, and your system could be improved.
If I need to worry about loading ‘the right scenes’, tweaking some values in the scene, manually dropping in a prefab or doing anything other than pressing that lovely little Play button, then I’m not happy. I want to make changes and test, as quickly as possible, with no fuss. So do your designers and artists, and pretty much anybody else who is hands-on with the project.
To that end, I’ve come to favour the following basic rules:
Your system should load what it needs when it matters
Scenes should only contain things that are strictly relevant to that scene
Play Means Play
Though I don’t claim that these rules are always applicable or appropriate for every project, I’ve found that they’re decent guidelines that you can easily refer back to in order to keep things running smoothly.
To my mind, the main implication of the above rules is that we should build some kind of boot-strapping system which allows us to define what to load, and do it automatically when required.
Defining The Problem
By default, Unity will call Awake on all active GameObjects when a Scene is loaded. This is usually the first point in time when you can do any useful work, but there’s no guarantee that your GameObjects will receive their Awake call in any particular order.
In the above scenario, and owing to the non-deterministic ordering of Awake calls, it’s easy to end up in a situation where you’re managing multiple singletons, sanitising your initial state, and otherwise trying to ensure that everything you need is loaded and ready before you can get on with things. It’s a frequent source of difficulty for new Unity developers and can lead to real frustration and difficulty if left unaddressed.
Common advice is to treat Awake as a ‘self-initialisation’ point, and the subsequent Start call as the first point in time when Components can safely communicate with one another.
Even taking the above into account, this still feels messy to me. I prefer, as much as possible, to keep scenes clean: free of system-level or non-scene-specific GameObjects. Worrying about whether things are loaded, available, or initialised feels like a chore because it is a chore, and if you don’t solve the problem at the source, it becomes more and more difficult to manage as your project grows.
A better solution overall is to build a system that injects what we need as we enter play mode, significantly reducing the amount of work we need to do to make sure things are where they’re supposed to be:
By injecting our dependencies as early as possible, we can avoid the fuss we might otherwise be forced to engage with.
Defining The Solution
The bootstrapping system we’ll build is comprised of four main parts:
A custom Project Settings file and editor.
A ScriptableObject representing a ‘Custom Boot’ context (Editor Runtime / Build Runtime)
Some light integration with Unity’s Addressables system
An initialisation script, which does all of the heavy lifting at runtime.
Though there’s plenty of code to chew through here, I won’t be including every single line - if you’re familiar enough with the APIs I mention here you’ll probably be able to plug any gaps yourself, but the code for this demonstration is available on GitHub, so feel free to peruse at your pleasure!
Custom Project Settings
One of Unity’s great strengths is the ease with which extensions and editor tools can be built, allowing a great deal of flexibility over your workflow and toolkit.
Since Unity already provides a Project Settings window, it’d be nice if we could add our own custom project settings there. Thankfully, we can! This can be achieved by implementing a SettingsProvider that will be loaded whenever the user opens their Project Settings.
The code required is fairly boilerplate, but Riley Bolen provides a great tutorial explaining each part of the process. It’s worth checking out Riley’s guide before attempting this, as my own SettingsProvider is almost identical.
The entry point to our SettingsProvider looks like this:
We perform some quick checks to ensure our settings asset is available, before returning an instance of CustomBootSettingsProvider.
If we need to create our settings file, we call GetOrCreateSettings inside CustomBootSettingsUtil:
Rather than saving our settings under the Assets folder, I prefer to store it alongside other project settings, in the aptly-named ProjectSettings folder. Note the use of InternalEditorUtility here: this is what allows us to easily serialise our ScriptableObject outside of the Assets folder1.
The CustomBootProjectSettings object stores two asset references - RuntimeSettings and EditorSettings:
The AssetReference fields here are our first link to the Addressables system. Though we don’t strictly require the settings file to store AssetReferences, future extensions to this example could benefit from it doing so, so I’m leaving it as-is.
Creating those AssetReferences requires a few additional steps. First, we need to create the assets we’re interested in tracking in the usual way:
The CustomBootSettings object in this example is a ScriptableObject which stores an array of prefabs. In a real-world scenario, you probably want something a little more complex than this, but it’ll do for the sake of illustration:
Next, we need to add the assets we’ve created to the Addressables system. Addressable assets are assigned to Groups, and we have the freedom to manipulate groups as we see fit.
For the sake of keeping things organised, we’ll create two groups:
CustomBoot_Runtime - for assets which need to be loaded in the editor’s play mode, as well as in standalone builds. This group is configured so that it’s included whenever you build your game.
CustomBoot_Editor - for assets we only care about loading during editor play mode. This could be handy for debugging tools, helper scripts, or other development-related tools we might need. This group is configured so that it’s not included in builds.
Now that we’ve created our Addressable Groups, we can add our assets to them:
The API to manage all of this is fairly simple, and you’ll notice that after we’ve added an asset to Addressables, we end up with a new AddressableAssetEntry. Referring back to GetOrCreateSettings, you’ll see that the asset entry’s GUID is what we use to finally create our AssetReference objects, which we serialize inside our project settings file:
All that’s left at this stage is to make sure our project settings window is rendered properly. You can use either the old-school IMGUI API, or the new VisualElements API. In this instance, I opted for VisualElements, but the choice is yours. It doesn’t make for particularly interesting code, but feel free to check it out on GitHub.
Putting it all together, we can now click Edit → Project Settings and view our new settings pane in all its splendour:
Ok, it could be prettier - but it does the job. What we’re looking at here is basically just a window that lets you edit the prefab arrays of our CustomBootSettings objects. Unity will take care of ensuring the asset states are saved if we modify them via this editor, so let’s cross this stage off our list and move on.
Runtime Initialisation
The whole point of implementing a boot-strapping system is to ensure that we load what we need as early as possible and avoid any irritating dependency-related problems elsewhere.
Unity provides a simple mechanism for hooking into the startup process:
The RuntimeInitializeOnLoadMethod attribute is the key here, and it doesn’t require much explanation. Include this on a static method anywhere in your runtime code and it’ll get called when the Unity runtime initialises.
Next, we need to decide what to do when our Initialise method is called. Our plan is to load our Addressable assets and then instantiate the prefabs referenced within.
This is a simple task via the Addressables API:
Notice the use of the asynchronous API here. More on this later, but if you’re not familiar with async / await, then it’s well worth reading up on it.
The handle variable is important - and we need to keep hold of it for cleanup purposes later, but first, we want to do something with the asset we’ve just loaded.
The Result property on our handle is a reference to the CustomBootSettings asset we created earlier, which means we can now call Initialise on it.
Here’s what that looks like:
This is fairly typical prefab-loading code, but the thing to note is that we first create a container object and mark it as DontDestroyOnLoad so that it survives scene changes. We then loop over our list of prefabs and instantiate them one by one, assigning our container object as the parent. We also keep track of each instance we create by adding them to an Instances array, so we can explicitly clean them up later.
The Cleanup code is bog-standard object destruction:
All we need to do now is make sure our initialisation and cleanup code is executed appropriately, so let’s dig into that.
The very first thing we want to do when Unity calls our initialisation method is to set up a listener so we can clean things up again when we’re exiting the runtime:
The Application.quitting event is a fairly reliable way to detect a normal shutdown process, but there are caveats on certain platforms2. It’ll work fine in the editor and on standalone builds, however.
To ensure we clean things up nicely within the Addressables system, we need to call Addressables.Release and pass it our handle from earlier:
We also call the Cleanup method on our CustomBootSettings object, ensuring that any instantiated prefabs are destroyed.
Now that we know how to load Addressables and clean up after ourselves, all that’s left to do is flesh out our Initialise method and we have a basic but functional boot-strapping system ready to roll!
Just one more thing…
If you cast your mind back to the beginning of this tutorial, you’ll remember I made the following wild assertion:
The Play button should always do exactly what its name suggests
I also mentioned the use of async / await, which lets us do things asynchronously such as loading things from disk or over the network without blocking other processes.
In an actual build, the player isn’t usually unceremoniously dumped into a scene requiring a ton of dependencies to function. Normally, a splash or loading screen allows the game to load whatever it needs before the player can do anything. In that scenario, asynchronous dependency loading is exactly what you want: minimise the time it takes to load everything, and only continue when things are in the right state.
During development, you’ll want to enter play mode from anywhere. If we use asynchronous loading in this context, there’s no guarantee things will be ready by the time the current scene starts up properly, which defeats the purpose of our bootstrapping system!
In the GitHub repository for this demonstration, the SampleScene contains a sphere which is coloured depending on the initialisation state of our bootstrapper when the sphere receives its Awake call:
Red if the boot-strapper isn’t initialised
Yellow if the boot-strapper only becomes initialised after the sphere’s Awake call
Green if the boot-strapper is initialised before the sphere’s Awake call
Let’s take our boot-strapper for a test run and see what happens…
That’s no good! We want everything ready before Awake gets called on our scene objects. Our asynchronous boot-strapping code is executing as we expect, but it’s not finishing in time. To make matters worse, the more stuff we do during our boot-strap phase, the longer it’s going to take for the boot-strapper to consider itself initialised.
Under the hood, things are looking like this:
To tackle this, we need to make our bootstraper check whether we’re in the editor, and if so, load everything synchronously so that it’s finished before the scene starts. Whatever small overhead there may be in synchronising our boot-strap initialisation is a small price to pay for the gains made elsewhere.
This is straightforward enough: check if we’re in the editor, and if we are, then we need to call synchronous versions of our initialisation methods, which is really just a case of not using async / await for the most part. When it comes to loading the addressable asset, we can call WaitForCompletion on the handle object in order to ensure synchronous execution:
Now that we’ve added synchronous behaviour when we’re in the editor, let’s try out our magical sphere again and see what happens:
Success! Our boot-strapper has done all of its work before the scene objects received the Awake call, and we can now get on with our game’s development with full confidence and a nice way to keep our system dependencies separate from our scenes.
Let’s try it out in earnest: the demo project has a few prefabs we want to load during our boot-strap process: some UI elements, an EventSystem, a simple 3rd-person character controller with a camera, and a ‘debug’ button which doesn’t actually do anything, but acts as our ‘editor-only system dependency’ for illustration purposes:
Let’s set those up in our Project Settings window, and see what happens when we hit Play. Here’s a quick screen recording:
Very satisfying! Our scene contains nothing but the bare minimum, and everything we need to play our game is loaded dynamically as soon as we need it.
All that’s left now is to ensure our ‘real’ game’s loading scene waits for the boot-strapper to finish before continuing - so that in a build, we can assert full control over the player’s experience.
Conclusion
We’ve seen why a boot-strapper is a beneficial addition to your game, and mentioned some of the problems it solves. With a little work up-front, we can simplify our development process and make sure that we don’t spend time fighting the wrong battles.
Unity provides great flexibility in your game’s architecture, but it’s always worth ensuring you control what gets loaded and when. Non-deterministic timing of critical lifecycle calls can, if you’re not careful, lead to all kinds of difficulties, so it’s worth taking some time to avoid the worst of it as soon as you can.
Please feel free to check out this demo project on GitHub. With a little extra work, you can turn this example boot-strapper into a powerful tool in your arsenal and add all manner of extensions. Personally, I’m keen to add support for bootstrap profiles, so I can modify the bootstrap process and the dependencies that are loaded based on whatever workflow I’m currently engaged in.
Let me know what kind of customisations and additions you make!
If you’ve enjoyed this post, and want to help support me in producing more Unity tips and tutorials, then please like this post, consider a subscription and share Not A Robot!
For a limited time, I’m offering a 20% lifetime discount for new paid subscribers, so sign up today to ensure you don’t miss out!
If we wanted our settings file to be modified, we’d need a more robust solution. For this demonstration, we never actually change the settings file itself once it’s been created: it’s just a pointer to the assets we do modify.
Mobile and Universal Windows Platform behave differently, so check out the Unity documentation for more information about what to do to ensure a fully production-ready cleanup process.