This is our object pooling in action: as the player is completes the 1st maze of the game the 3rd one begins pooling, which finishes right as the player lands in maze #2.
Goal: This blog post aims to give a high level overview of not only the tech behind this pooling but the editor tools we use to make it all happen.
The core tech is a simple implementation of standard object pooling. We specify which objects (prefabs) we want pooled pre-compile time (in our case pretty much everything except the Goal objects and UI stuff). Objects are instantiated and recycled (put into a cache) or destroyed if recycling doesn’t work for any reason.
Pooling is a really easy way to massively improve not only performance but more importantly persistence. Sky Labyrinth has one loading screen for the whole game; all 30+ mazes are inside one Unity Scene thanks to pooling. This is especially important for our Sprint mode of gameplay (which challenges the player to beat the entire game with 1 life).
This standard pooling isn’t enough make everything happen though. You need to tell the pooling what to recycle, what to instantie, where to do it, when to do it, with what related parameters, etc. This is where our custom MazeObjectPooling script comes into play.
Maze Object Pooling
Once a maze is completed, this script is called with the target maze to be instantiated (we always do 2 mazes ahead). The process occurs in phases:
- MazeParts – Walls, Pillars, Tiles
- MazeItems – Pickups, Powerups
- MazePuzzles – Trampolines (we cut our other puzzle from the game for now)
- MazeObstacles – WallSaws, FloorSaws, Goop, etc.
- Recycle the previous maze
In the GIF above, you might notice that each object instantiated has a slight delay between the next one; this is intentionally programmed in, not due to lag or performance issues. We had implemented a instant instantiation (see here) which worked great on desktop, but on a 2015 Android device we saw significant framerate drops. We introduced delaye and phased pooling to distribute the performance load over a number of separate frames instead of all at once.
Any programmers reading that previous sentence might exclaim “Hey! Isn’t that what multithreading is for?!” – yes multithreading our instantiation would be a far more performant and elegant solution. Unfortunately Unity’s API is not thread-safe, so we cannot use C#’s System.Threading with things like gameObject instantiation, setting Transform parameters, setting gameObject names, configuring custom component parameters, aka: everything that our pooling script does.
Recycling however, is not phased because it’s far more performant than instantiation. Though there is a significant delay before a maze is recycled to ensure the player isn’t still inside it!
Our MazeObjectPooling needs to know lots of data about what it’s instantiating. At a minimum it needs an ID, name, and position. For some objects we need rotation as well, and for a few special cases we need lots more data specific to that object (such as enemies and their pathing node’s data).
How does one deal with telling the pooling script all this data? Automated editor tools of course!
This is a port of MazeObjectPooling into an Editor-only (and Editor-friendly) script. I say editor friendly because (through some arduous trails) we learned that sometimes Unity will make prefabs just *poof* disappear if you don’t make your scripts Editor-friendly with things like PrefabUtility and isDirty.
WriteMazePoolingData – Writes all poolable object data out to JSON. The maze seen here (8×8 fairly simple) has 40KB of JSON data; our largest (20×20) most complex maze is currently only ~232KB, so in total our entire game’s maze data should take around 3-5MB of space.
DeleteAllPoolableObjects – As the name suggests, destroys all poolable objects in the scene, since it has to be empty before we publish builds to reduce initial loading time (and make pooling actually useful). I can feel hundreds of level designers cringe at the idea of deleting their work, but worry not! We have our JSON data and the below editor tool to rely on. Our JSON is also in source control, so in a worst case scenario of a local disk failure, we only lose our most recent uncommitted design changes.
ReinstantiatePoolableObjects – As the name suggests, reinstantiates all poolable objects in the Scene from JSON; all those level designers breathe a sigh of relief! You might notice this instantiates a lot faster than runtime pooling, because it’s not phased or delayed.
Anti-Stupidity Functionality – As the name suggests, this stops stupid people from overwriting the JSON data with blank mazes. In case you the read is wondering, yes, I did in fact find out we needed this feature the hard way.
First time I overwrite all the JSON data with blank mazes, shame on me. Second time I overwrite all the JSON data, I add a feature to prevent myself from ever doing it again (also shame on me again for not doing it the first time).