The Co-Founder of The Sixth Hammer Dimitar Popov has returned to tell us about designing a big seamless 2D world without loading screens in Unity.
I am Dimitar from The Sixth Hammer – a small independent Bulgarian game studio, currently focused on 2D game development using the Unity3D engine. Our largest game – Moo Lander, required us to find creative solutions to a lot of difficult problems, typically arising in more complex games. We wanted Moo Lander not to include loading screens and instead be one big open 2D world. So we needed to find a solution to the problem of performantly streaming content during gameplay.
In Unity we have the concept of Scenes, so we just needed to find a way to load those in the background while the gamers play the game. Simple enough, right? Wrong!
- How to know when to unload a scene e.g. when even distant background objects are not visible and nothing from the scene is needed anymore?
- How to keep track of which scenes need to be loaded when multiple players can be in different places in the game world?
- How to load/unload scenes in the background without any spikes in performance?
These are some of the questions I will try to answer in this article. Come on, let’s dive deep into the topic!
The Current Out-Of-the-Box Features Unity Offers
You can imagine the Scenes in Unity as “bundles” of Game objects. Unity offers two types of loading a scene – Additively and Non-Additively. Non-additive loading replaces the current scenes with the loaded one, while additive loading keeps the already loaded scenes – in our case we needed additive loading.
Unity also offers two ways of loading each of the types – Synchronously (that basically pauses everything else until the loading is done) and Asynchronously (in the background). For our game – Additive Asynchronous loading was the type we used.
We’ve had to build a whole system of game objects and helper classes for the scene loading in order to tackle the aforementioned issues that arose during development.
We thought long on what is the best solution for loading/unloading a scene at the right moment (when the action is minimal, when the scene itself is not visible and when the scene is not needed anymore anywhere else). In the end, we came up with the idea of a Scene Switcher.
Each Scene Switcher holds as children two “Gate” Game Objects. Each of those has a 2D physical trigger collider (that is usually rectangular, but may be shaped in any way if needed to fit a scene layout). The Scene Switchers also have a field for the Scene Name, that it needs to load/unload. It then keeps track of the interaction with its Gates. An interaction with a player in the way of 1 => 2 should load the scene, while interaction of 2 => 1 should unload the scene.
The white rectangles with tiny “1” and “2” texts on them are Scene Switchers:
Scene Switchers may know a player’s intention to load or unload a scene. But we’ve separated the actual process of loading a scene to another singleton system class, that keeps track of all of the player’s intentions as well as which scenes are already loaded.
It does that by doing a few things:
- It keeps a Dictionary with the Needed Scenes where the Scene Name is the key, and a List of players that need that scene is the value. It then provides a method UpdateGameObjectNeededScenes(GameObject GO, string sceneName, bool shouldLoad) for updating the dictionary.
- It keeps a Dictionary with the currently Loaded Scenes (Scene Name as a key, Loading State as a value) that it keeps up to date by subscribing to the Unity’s built-in Scene management events.
- It runs a function in the game’s Update loop, that checks if the loaded scenes and the currently needed scenes match, and if not, it initiates load/unload of scenes.
Of course, the actual implementation includes a dozen of helper functions and other variables, but this is the main idea. With that architecture, we are solving the first two issues (mentioned at the beginning of the article) gracefully.
On the other hand, the third issue about the performance deserves a separate page of its own. So let’s see what were the issues and challenges there and how we solved them.
Improving Additive Loading Performance
The way Unity loads a scene in the background works pretty well while the scene is loading. But then on the last frame Unity creates and activates all of its game objects (which is normal behavior, because the engine cannot know the dependencies between them) which causes a pretty substantial lag.
The severity of the lag mostly depends on two things:
- The count of the Game objects in a scene with their respective used assets and resources counts.
- The count of MonoBehaviour components, attached to them (especially valid if they have logic that executes in the Awake or Start methods)
So based on that, we have a couple of different approaches to improve the performance:
- We can make our Scenes smaller e.g. reduce the count of game objects in them. That could improve performance but will make our game management much harder with more frequent Scene Switchers and loadings happening throughout the whole game.
- We can reduce the MonoBehaviours count, which is a valid approach to improving the performance. But in our case, it was simply not possible, because of the complex behaviors that most of the game objects have in Moo Lander.
- We found a way to somehow not activate all game objects in the scene simultaneously – this was the best approach for us because we did not have to cut complexity that way and achieved a tremendous performance boost.
Delayed Activation of Game Objects
In order to do that, we first need to prevent Unity from doing this automatically on scene load. Unfortunately, we did not have a built-in way to do that, so we just went ahead and:
- Parented all of our scene game objects to a single Scene game object.
- Moved all of our physical interactable game objects into an Interactables game object that is a direct child of the Scene game object.
- Wrote a tool that disables all of the Scene game objects when making game builds. That way when unity loads a scene, it is loaded in a disabled state.
We then extended the scene system that was responsible for the loading/unloading of our scenes with the following behavior for after a scene is loaded:
- It first checks if the loaded scene has the disabled Scene object at the top.
- If it has one, then it means that we want to do a delayed activation of the child game objects, so it loops all of the multi-level children and disables them one by one.
- It enables back the parent Scene object. At this point, nothing is still visible, because all of the children are now disabled.
- It starts a coroutine that loops all of the children and activates a certain amount of them each frame. A very important note is that this coroutine must skip the Interactables holder – this is because if we enable physics objects over time, we could potentially have a broken behavior (like enabling an enemy before the ground below will make the enemy fall through).
- It then enables all of the Interactables game objects at once (which still causes a small performance lag, but this is orders of magnitude smaller because we are enabling only tens instead of thousands of objects like in the beginning)
With that, we improve the quality of the player experience by removing the nasty performance spikes when additively and asynchronously loading scenes with Unity.
Bonus tip – unloading a scene does not actually unload all of the used textures and other assets that the scene uses. This is why you should also call the built-in UnloadUnusedAssets method from time to time during gameplay in moments where there is not much action. That causes a small spike in performance. But We still have not gotten to the point, where we would need to optimize that Unity behavior.
Also, check out that link for other insights on improving the async loading of scenes.
As you see, there is a lot happening in order to ensure the smooth loading of everything the players need throughout a game. And after we finish with Moo Lander, we will package all of that into a plugin that would save a lot of time for future fellow developers.
Of course, there is much more happening to our game scenes and objects (like persistent objects that can move between scenes, etc..), but I will share insights on that with you in future articles. For now, I hope this will give you enough of a direction to build your own great solutions to the additive scene loading problem.