Yikes! 4 years. Wow. Thanksgiving weekend 2016. I still remember Thanksgiving weekend in 2012, when I decided to try my hand at making a small game to learn Unity. At the time, I thought it would take 3 months (“How hard can it be to make a game!?”), and here I am 4 years later.
For me, Thanksgiving is always a time to reflect on the year. I don’t usually go into personal stuff here, but this past year has been pretty rough.
Past 12 Months
There have been a lot of setbacks with work. I had hoped to finish the game by mid-2016, or at least to be at a point where I’m focusing solely on level and puzzle design. Some of you might recall I had tried working with other programmers to make the game, but for one reason or another, things didn’t work out. I’m back to working by myself on the project now.
Programming has never been my strong suit (or something I particularly enjoy), so taking over the code base after it had been worked on by 3 different people, and which I had not looked at closely for almost a year, was a brutal endeavor. It seemed almost impossible to understand the code that was handling the back end level loading, saving system, and gameplay. The architecture was completely different than what it was when I stopped working on the code to focus on design.
It took about a month of me banging my head against the code, commenting every single line, going back and forth between scripts, before I understood what was going on. I’m past the hardest part though, and now have a clear idea of the underlying tech for the game. It makes fixing bugs much easier, as I can pinpoint what is wrong behind the scenes when I see a problem in the game. It’s also much easier for me to work with the code directly to make my designs happen.
I’ll talk more about the changes I’ve made to level loading and save system later in this post.
Besides work, also a lot of stuff happening in life. In June I went through a break up, and in October, a close family member passed away. During this time, I had also discovered a golf-ball sized tumor near my left collarbone. This was initially diagnosed as a swollen lymph node, which is very worrisome to find in the collarbone area.
In fact, the lymph node by the left collarbone is known as “Virchow’s Node“, and “The finding of an enlarged, hard node has long been regarded as strongly indicative of the presence of cancer in the abdomen”. To make things even worse, a lymph node is considered abnormal if it’s bigger than 2 cm. The tumor was about 4 cm.
As you can imagine, I was not very pleased to learn of this news. I had to have surgery to have the tumor taken out and tested for cancer. In the end, the tumor turned out to be benign, which was a huge relief. The entire experience took 2 months, from first discovering it, to seeing a doctor, then a specialist, then the scheduling, waiting for test results, etc. It was a particularly stressful time.
Anyway, things are looking up now. I’m thankful for the friends and family who have supported me throughout this period. Also thankful to have my health after the scare, and not have cancer. And even though Manifold Garden has taken way longer than I had expected, I’m thankful for the opportunity to work on this project. It’s really a dream come true in so many ways, to be able to immerse myself completely in a world of my imagination. And even though there are a lot of aspects of game development I don’t enjoy (management, low-level programming), everything has been part of a learning journey.
Onto the technical details.
Level Loading – Streaming and Stutter-Free 60 FPS
In Manifold Garden, there are multiple different areas that you can travel to. Each area is a separate scene in Unity. There are loading screens during gameplay, and no level select screens/
Here is a map of how the game world of Manifold Garden is organized. The blue circles represent worlds (main gameplay areas), and the red lines are hallways which connect the worlds via portals.
Here’s what it looks like to go through a portal hallway to get from one world to another:
The hallways serve 2 purposes:
1. It provides visual contrast of going from a narrow hallway to a large open area. Putting two open areas next to another doesn’t feel as interesting. The interior also has strong contrast to make the portal “look” like a portal (not seeing anything behind the portal contrast with the interior wall).
2. It provides time to prepare the next level before the player moves in (this is an old game dev trick)
For a while, the way level loading was handled was by loading all the levels at the beginning, then disabling all the scenes not needed, and then enabling / disabling them accordingly as the player travels throughout the game world.
In this image, everything with a yellow tint has been loaded.
The problem with this was that it had a really long load time at the beginning. Even with only a third of the game scenes loaded, the load time was about 30 – 40 seconds. It clearly was not going to scale for the entire game.
This meant that the levels needed to be loaded in dynamically in the game.
The first implementation of streaming loading used Unity’s async loading. The only control you have over this is choosing the thread priority to be low, below normal, normal, or high.
Every time a level is loaded, it would cause a very noticeable hitch. It was a fraction of a second, but with the game running at 60 FPS, it was very noticeable, and very jarring.
For dynamic loading, only the neighbor levels are loaded.
As you can see here, the player starts in world 1. Before the game begins, we already world 2 and world 3, as well as the hallways connecting them to world 1 loaded in.
Worlds that have a black dot means they have been disabled. So when the player is in world 1, only the hallways connecting to it are enabled. The hallways are staggered the main world physically. The gameplay worlds are always at origin (0, 0, 0). The hallways are far away, like at (600, 0, 0). When players change between gameplay worlds, we are enabling the new one, disabling the old one, and just changing the player camera culling layer.
When player moves int world 2, we load its neighbors. World 1 has already been loaded, so we don’t need to reload it. We load in world 4 and world 6. Note now world 1 is disabled. The only enabled levels are world 2 and its hallways.
I was getting a hitch when loading in new scenes, even using Unity’s async.
At the beginning of November, I went to Unite 2016 (the Unity conference in LA) and spoke to William Armstrong (who worked on Firewatch), as well as devs from Playdead (who made Inside and Limbo).
They both had really great suggestions for smoothing out loading.
Instead of loading the entire level using async load, I load in the colliders separately. In preparing the level, the colliders are separated into groups of 100, and each is made a prefab, then deleted from the scene. During gameplay, the scene (without colliders) is loaded in async during one frame. Then during the next frames, one collider prefab groups is instantiated and moved into the scene it belongs in.
Thus, an entire level is loaded into the group over the course of multiple frames, sometimes up to 20 or 30. This is perfectly fine as the level is fully loaded far before the player is even close to getting to it. This allows me to maintain a smooth frame rate even as new levels are being streamed in.
After I got the stutter free level loading working, I started to work on the save system. This turned out to be much more complicated than I had expected. I’m not even sure why this surprises me anymore. Pretty much everything related to this game has been more complicated than I had expected.
What makes the saving tricky is that Manifold Garden has persistent states in an open world. The levels are loaded in dynamically, so not all the playable objects are available to be referenced. Also, which levels are loaded depends on the current play session, not necessarily the history of the game. For example, if you visit world 4 in one play session, then continue that game in another play session, but don’t ever visit world 4 or its neighbor levels, world 4 will not get loaded. However, the objects in world 4 still have history.
There’s also the case of making sure the save file doesn’t get corrupted. To minimize errors, each save in the game is stored in a separate file on disk (so that corruption of one save doesn’t ruin all the saves). Also, when writing to disk, the game first writes to a temporary file. It then makes a backup of the original, then renames the original, then renames the temp file, then deletes the original.
This way, there is always at least one good copy of the save.
Here the load screen design at the moment (I still need to get the screenshot in save slot working):
Each save slot also lets you restore to an earlier save (basically backups within the save):
I got the idea for the restore from the Talos Principle.
Still a few things left with the save system. Also need to tackle object pooling, and the water mechanic has several problems.
The last remaining tech is a fast travel system, which is using the level loading system underneath. This shouldn’t be too difficult, but I’ve been wrong each time when I thought this.
After the tech, I want to go back into level design and focus on that.
Still need to work out audio design and music.
And then it’s just lots of tiny bugs here and there, and polish everywhere.
Oh, also working on the PS4 port (sigh).
Almost there! Definitely in the second 90% of development. It’s a real sludge to get through, and my productivity has been super low. However, I’ve been tackling things consistently, and definitely still making progress.
You walk over the highest mountain one step at a time, right?