We're back with another code review video, in
this series I show you some actual code I write for actual video game productions, and today we're
looking at scene management, I made a cool system to help me manage scenes in Unity, and I think
it's pretty neat we're going to check it out! So Unity relies heavily on the concept of scenes,
whether you use them at stages or regions in the game you will switch between scenes using the
Unity SceneManager. You can also load multiple scenes at once using Additive mode, this way you
can have the UI in a separate scene for instance, because it's common to all scenes.
Our game has a pretty standard setup: we have multiple scenes for multiple regions, the home of the
player, the village, the docks, other areas of the world, and we have a Common scene with all
the UI, important systems, audio stuff, whatever is common to all scenes. However since we have
gorgeous 2D Graphics as you can see, every scene is hhheavy, and when switching scenes loading
times can be quite long. Which as the player is annoying not gonna lie, so we'll want to
try and avoid that as much as possible. The way we do this is we load every scene in Additive
mode, this way once the scene is loaded and we exit the area, we simply disable the contents of
the scene, and when we re-enter it later we enable it again. As a result the initialization logic
of a scene happens in the OnEnabled method as the behavior must be the same whether you're
enabling a scene or loading it fresh. To keep track of scenes our own SceneManager takes
care of loading loing the scenes, calling the Unity manager under the hood, and keeps track
of what scenes have been loaded, this way when we want to switch scenes the manager can choose
to either load a scene or simply enable it if it was already loaded. Scenes are referenced using
a handy-dandy scriptable object called SceneAsset. A scene asset represents a scene and
that's how we reference it when we want to, say, teleport the player to a scene. So what's in the
SceneAsset? Well first of all a reference to the actual Unity scene, I use this very useful script
(link in the description) that creates a scene reference field and tells you whether it's in
the build or not. The one I use isn't maintained anymore but it still works, and there are plenty
others on GitHub as well. Along with that we have a weight amount, we'll get to that in a bit and
a list of neighbor scenes, for instance from the Docks you can go to the Village from the Village
you can also go to the Docks or to the Forest or the Player's home etc etc and we'll get to see how
that's useful in a sec as well. Basically think of the scene asset as the metadata of the scene,
it's useful to handle scenes and metadata without actually having to load them to know what's inside.
In the future more data will be added such as for instance the sound banks used by the scene. So in
order to enable or disable an entire scene at once every scene has a single root object, this object
is always active but when enabling or disabling a scene who will toggle its children. And this
object has a SceneRoot component which helps with various things, such as loading the common
scene if it's not loaded, this way I can start the game in the editor from any scene without
worrying about loading the common scene. So how does loading actually happen? Well when we want
to switch to a new scene we will load the target scene first obviously, the loading function itself
is in the scene asset, which first checks if the scene is pre-loading (we'll get to that later) and
then checks if the scene is already loaded by checking if it is aware of any scene root object
being present for this scene. If it isn't loaded, we'll call the Unity LoadScene function and wait
for loading to complete, then return the scene root. Back to our scene manager we just have
to disable the scene we just exited and enable the new scene, whether it was just loaded or was
already in cache. Now as I said we load scenes and disable them when we don't need them this way they
are already loaded next time we want to visit them but obviously if we never unload scenes, at some
point we will run out of memory, especially on lower end hardware. That's where the Budget System
kicks in. I mentioned every scene has a Weight field, this is actually a rough estimate of the
weight of the scene in RAM in megabytes, in the scene manager we set a total budget for scenes and
we will keep loading scenes additively as long as we are within budget once we reach the limit if we
want to load a new scene we will first unload as many scenes as needed to free up enough space for
the new scene to load. The simple way to know which scene to unload is to maintain a history of
what scenes have been used every time we enable a scene, we move it to the front of the queue and
when loading we remove the oldest scenes first starting from the back of the queue. This heuristic
is not optimal imagine a case where the player leaves home, travels through multiple regions, then
wants to teleport back home. By then the home scene might have been unloaded. What would be better
would be to define a sort of priority or rating system based on frequency of use to make sure we
keep the most frequently visited scenes in cache. But anyway that's how the budget system works and
it has additional benefits for instance the total budget limit can be set to different values based
on the platform as there isn't the same amount of RAM on say the Nintendo Switch than on more recent
consoles. We could even imagine setting the budget dynamically on PC based on the player's hardware.
An other thing we can do is to preload scenes as I said every scene asset has a list of neighboring
scenes so we could preload them as long as we stay within budget this way by the time we actually
get to the door to the neighbor scene it might already be loaded or almost loaded. Preloading is
very cool or rather it would be but this feature is disabled right now. Let's go on a little tangent
to explain why. So when loading a scene there are a few spikes that freeze the game like maybe three
or four frames that take around 50 milliseconds and it's not a problem while switching scenes
at worst the loading spinning icon thing just stutters but preloading happens during gameplay and that's unacceptable investigating that led to discovering the CPU is waiting on the
GPU to render the frame which is weird because there's nothing to render yet as the new scene is
disabled so I checked the render thread and it's actually uploading textures to the GPU using Gfx.UploadTextureData.
since we have huge background textures and they haven't been optimized at all
that's no surprise. There is a feature in Unity to avoid this, the solution is to set the async asset
upload settings in the quality settings. You can set the size of the upload buffer (but really
you should set it to the max texture size you can because if Unity encounters a texture larger
than the config it will reallocate the buffer). You can also set async upload time slice which is the
number of milliseconds we allow the uploading of textures to take per frame. So that's cool right?
Wrong. Setting it to a low value (the default is 2ms) did not change anything. My theory is
this setting is here to allow multiple uploads per frame, like after a first upload we'll check if we
still have time and start a second upload, but if a single texture takes 50ms to load, it
will take 50ms and it's not going to be split over multiple frames. So we're kind of
stuck actually, if we can't split the upload of a single texture and we have very large textures
they will each create a spikes. And that sucks, the only thing we can do is optimize the textures. Right
now since we're in development and I don't really care about stutters or loading times, all images
are uncompressed and don't have a Max Size, in the future I will need to bring them down in size
while compromising the least on quality, and for this we have a bunch of ways to optimize a texture
but that is planned for another episode of Code Review All That Remains is a way to measure
the weight of a scene, the way I do this is by building a project with just an empty scene and
the scene we want to measure, in the empty scene I take a snapshot of memory usage using the memory
profiler, then switch to the Target scene and take a second snapshot. The difference between the two
is the weight of the scene. In the future I hope I can automate this process by having a CI Pipeline
that loops on every scene, builds the measuring build, launches it, takes the snapshots,
reads the difference and sets the value of the scene asset weight automatically, but that is a very
low priority feature considering the other stuff that needs to be done for the game. Now of
course there is more to RAM than just loading scenes, and the total budget mustn't be equal to
the total amount of RAM available, to account for dynamic objects that will be created or instanced.
So during loading time the game fades to black, right, and it fades back in when the scene
is loaded, but the scene being loaded on the Unity side doesn't necessarily mean it's initialized
on our side, there is some additional stuff we want to do like run time navmesh generation or
making sure other resources are ready before proceeding. So in the awake function objects
in the scene can register themselves in the SceneRoot as being objects that take a while
to initialize. The scene root keeps a list of those initialization observers and during a
scene switch we will wait for all of them to be ready before removing the loading screen.
That's it I think for scene management let me know what you think in the comments and
how you handle scenes and loading times. In the future I will make a similar system for
addressable assets with a budget of its own as a way to manually keep track of assets and
to load a bundle exactly once with a reference counter to know when it's safe to unload it
You can grab the source code for the scene management tool on the patreon and use
it in your project there's also another neat little tool for navigating scenes as a patreon
exclusive. See you soon and have a good one!