If It Ain’t Broke, Don’t Break It: Part 2

This is the second article in a series about cultivating good habits so you don’t have to fix the problems that bad habits will cause. The goal here is to anticipate the problems you may need to solve and incorporate the solutions into your design. See Part 1.

Heavy lifting goes behind the curtain

Don’t look!

What is heavy? Instantiation, destruction, and garbage collection. Those are the big ones in most games. We want those to happen when the user isn’t looking, or at least not when they expect our game to be performing at its best.

Have you ever wondered why the characters in movies never take bathroom breaks? I’m no Hitchcock, but I suppose the waiting around might not be a great user experience for the filmgoer. Likewise, you want to keep the disruption to your users behind the scenes. This is what loading screens are for. Downloading and streaming from disk are the obvious use cases for loading screens. But your work isn’t done there.

Instantiation: deserialization

We have to worry about two things in instantiation, deserialization and memory allocation. Serialization and deserialization are a big topic. Fortunately, Unity insulates you from a lot of it. Not all of it, but that is a topic for another time.

Put simply: serialization means turning an object in memory into data that can be saved and transmitted. That’s what happens when you take a GameObject from the scene window and drag it into the Asset Browser. The file on disk represents the serialized data.

Try this: In a Unity project, go to Edit->Project Settings->Editor. Change the Asset Serialization option to “Force Text.” Now open one of your prefabs in a text editor. Look something like this?

This is the serialized data of the prefab. It is in a format called YAML, but there are others you may encounter, like JSON or XML. Consider that all programs have output, and most of them can save that output to files. They can subsequently load those files and turn them into copies of the original objects they had in memory. That is what serialization formats are for.

So what happens when you call GameObject.Instantiate() on a prefab? This is your loading process. If you tried the above experiment, you’ll see that even for a default object like a plain sphere prefab, quite a lot of data is serialized.

Same thing the other way. Deserialization takes time. Whether blocking (nothing else runs while it’s working) or non-blocking, your game will be held up until critical objects exist. Even having non-critical objects pop into existence all of a sudden is usually jarring; imagine a desolate moonscape suddenly turning into a colorful cliffside Mediterranean village. Kinda spoils the mood.

Fortunately, a lot of architectural patterns converge on one thing – for games, it’s best to get all your objects into existence at the beginning of your scene. What happens before the beginning of your scene? The loading screen! Gamers are accustomed to seeing these. If your loading process is blocking, and nothing else can happen while it runs, players will accept the minimal animations of a loading screen juddering a bit.

Katamari Damacy had one of my favorite “loading” screens. So much happens beyond just loading!

Instantiate() is indeed a blocking operation. So is AssetBundle.LoadAsset(). AssetBundle.LoadAssetAsync() is not – and you may want to use it for that reason. The rest of the game will run while it loads, but be careful not to spoil the illusion by interrupting the player’s experience with the result. Keep it behind the curtain, whether that’s the loading screen or just out of range of the camera.

Now things can get a bit more complicated. What if you have a big open world game? Or a really complex level with a lot of density of detail – such that you need most of your system’s resources to render and reason about just what is in front of the player? You might want to explore schemes for hot swapping your objects, then. That is outside the scope of today’s article. However, even there, you don’t want your characters to take a bathroom break while the players are waiting – you have them do it off-screen. That’s a tough balancing act, but necessary for a professional-looking experience.

Notice the hitch in this video when I tick the box – this instantiates 99,999 copies of an empty game object. This is on a powerful development PC, and the prefab is empty. Granted, that’s a lot of copies. But if your prefab has much complexity at all, that will more than make up for it. A real game object might be 20-100x as complex in serialized form.

Instantiation: memory allocation

But maybe you noticed another thing: After the deserialization is over and the objects are in memory, the game goes back to running smoothly. Again, yes, powerful device, no processing happening in those prefabs… but we still seem OK.

While the deserialization process might use memory itself, it’s really a performance hit we’re worried about when we instantiate. Hopefully, when you plan your memory usage for your target devices, you have already budgeted and anticipated having the actual objects in memory themselves. If you try to use too much – well, depending on the platform, the game may crash. At Tricky Fast, this has been our biggest constraint on WebGL so far, for example. Otherwise, it may run painfully slowly as memory is swapped around.

So with memory, a good place to start is simply to stay within budget. There’s more to it than that, but you won’t see the most drastic problems unless you’re over. So, fine, assuming we’re within budget, why do I care about allocating memory behind the curtain as well? Given no extra processing burden, the game runs the same.

Simply put, we want similar objects, especially those stored in collections (Lists, arrays, etc.) to be physically close together. This is tied into hardware – hardware is optimized for accessing memory in sequence when the elements of a collection are close together.

So how do we get them to be close together? We allocate them at the same time. For the most part, if our memory isn’t too fragmented, they will be plopped into the memory heap, one after the other. Fortunately, in C# our garbage collector keeps us pretty un-fragmented. That is a mixed blessing.

Imagine you are making a game of whack-a-mole. A naive approach might be:

  • Instantiate the board with 9 holes.
  • Wait until a timer says it’s time for a mole to appear
  • Instantiate the mole
  • Wait for the player to whack him, or miss and let it retreat into the hole
  • Destroy the mole

At first glance this might seem efficient, because you only have the mole when you need him, and you have free memory when you don’t. However:

  • Memory requirements are not diminished. Even when the mole isn’t in memory, you need that memory available in case he needs to appear.
  • There is a performance hit from the call to Instantiate() which the player may notice.
  • Besides the mole-shaped hole in the ground, there is now a mole-sized hole in your memory utilization.
  • Now a couple of things could happen. The free space on the right is smaller because the hole is taking up room. Maybe automatic garbage collection will be called to collapse the used memory together. That could cause a noticeable hitch.
  • If GC is not called: Maybe some of that space gets filled before the next mole is instantiated. Now we have exacerbated the problem because the mole won’t fit in its old slot, so it will be placed further down into free memory, potentially making a new hole at next whack.

With anywhere from 0 to 9 moles on screen at a time, these problems can add up. So what do we do? How about this approach:

  • Instantiate the board, with 9 holes
  • Instantiate 9 moles
  • Turn them off, so their Update() loops are not running. Their memory is reserved, but there is no processing burden. mole[i].gameObject.SetActive(false);
  • Wait until a timer says it’s time for a mole to appear
  • Activate the mole
  • Wait for the player to whack him, or miss and let it retreat into the hole
  • Deactivate the mole again.

What are the advantages here?

  • Since we are allocating memory for all the moles at once, they are more likely to occupy physically adjacent memory. That means addressing them in sequence will be faster – for example if we want to loop through them all and make them all dance when the player loses.
  • All the allocations happen at the beginning of the game. So, the noticeable performance hit from calling Instantiate() happens behind the loading screen.
  • Gameplay doesn’t cause memory to fragment. That means garbage collection will be called less often, which means fewer hitches.
  • Even though more memory is technically in use, the memory requirement is the same – we might need that same amount to populate 9 moles at any time, anyway. Measuring memory usage is more straightforward, because we don’t need to consider memory that is technically free but possibly needed.

By the way: this is a very simple variation on the Object Pool pattern.

Destruction

Eventually, we will probably need to destroy these objects – say the user wants to move on to a different minigame, which has less to do with moles and violence. We’ve already touched on the basic reasons to avoid destroying the moles during gameplay:

  • Deleting objects on the fly causes fragmentation
    • Fragmentation causes slower memory access because it makes it less likely that objects will be adjacent.
    • It encourages automatic garbage collection, which the user may notice as a hitch.
  • It also makes it harder to analyze our memory usage because deleted objects may appear free in memory, even though their spots in memory are still required by active gameplay.

There are a few more reasons, too:

  • You risk destroying objects that are referenced by other objects. You’ll get NullReferenceExceptions, which will be harder to track down than those that happen in an environment where nothing is deleted on the fly.
  • Destroy() is not as heavy as deserialization, but it is a blocking operation. That gives it the potential to be slow if you have a lot to do.

One last thing to mention, which should be obvious but may have slipped through the cracks: Destroy old stuff before instantiating new stuff. Empty the bucket before trying to fill it again. Maybe you have enough memory that this doesn’t matter. Still better to cultivate good habits.

Caveats?

If you need the space, you need the space. Go ahead and delete if you have to. But consider rethinking your scene boundaries so that you can take care of this unpleasantness behind – you guessed it – a loading screen. Even if, in the story of your game, you are still in the same scene, you may decide to put up a mini-cutscene while you clean up the junk from a finished part of the level.

Garbage collection

Speaking of garbage collection! That is a very complicated topic. It would certainly behoove the ambitious developer to get familiar with it. But there is one quick gimme that I would like to pass along here.

Garbage collection is automatic in C#. It will happen when the environment decides it needs to happen, and the user may notice that. C# language documentation will tell you in many places: while you can call garbage collection directly, you shouldn’t. This is because it is optimized and probably smarter than you, blah blah blah.

There is one exception, and this isn’t documented in many places: games. In games, you will want to call garbage collection directly, because by doing so you may preempt it from being called automatically. You can make performance hitch when you want, so it doesn’t happen when you don’t want. You could do this when the player pauses, or, of course, behind the loading screen. The code is easy:
System.GC.Collect();

Make sure it happens after you do all your loading, instantiating, and destroying. Putting it last, before dismissing the loading screen, will ensure your memory is as orderly as possible before jumping back into gameplay.

There is a similar bit of cleanup you might choose to do at this opportune moment: Resources.UnloadUnusedAssets() – just make sure to read the docs about it and understand the consequences.

Takeaway: What’s the habit I need to learn?

There a couple of things that every game scene does which are heavy and which can be grouped together. Do group them together, and do it behind the loading screen so the players don’t feel their game is being interrupted by yucky hitching. This will keep your memory tidy too.

Anything that needs to be destroyed, gets destroyed behind the loading screen. Then, anything that needs to be instantiated, gets instantiated behind the loading screen. Invoke garbage collection last, and then dismiss the loading screen.

About Tricky Fast Studios

Tricky Fast Studios is a US-based game studio featuring long-time industry veterans. We provide a full spectrum of game development services including bug fixing, feature development, porting, temporary staffing, and complete development. Our recent work includes The Walking Dead: March To War for Disruptor Beam, Poptropica Worlds for StoryArc Media, the Star Trek: Timelines Facebook and Steam ports for Disruptor Beam, and Wheel of Fortune Slots Casino for The Game Show Network. We’re here to build your story!


Join Our Mailing List

No comments yet.

Leave Your Reply