Switch Hitter Development Log

Behold as I pretend that I am a game developer.

Sprint #14: "Bending--okay, breaking--the rules."

October 21st, 2018

After spending some magic number of years writing code, you start to pick some of software development's lore. One of those nuggets shows up in Michael A. Jackson's Principles of Program Design, wherein he says that his teams follow two rules "in the matter of optimization. Rule 1: Don't do it. Rule 2 (for experts only): Don't do it yet, that is, not until you have a perfectly clear and unoptimized solution." Broadly speaking, code that performs well--and it's worth defining that performant code either doesn't take long to run, doesn't need much memory to run, or some combination of both--is at odds with code that reads well. It's often faster to make many assumptions but it's also often far more verbose. Jackson, and many other extremely smart people, insist that being able to read and understand code is of paramount importance during the development process while having that code perform well is secondary until it's absolutely necessary. Well, I sort of broke the rules this sprint and decided to indulge my performance instincts a little bit.

That's okay, right?

Though I wouldn't really say that getting Ensue to perform was absolutely necessary, I've definitely noticed that it's slowing down significantly in the bigger levels that I've been trying to produce lately. The main reason I decided to make this sprint performance-themed was that I figured there were a lot of easy things to change that would make big changes in performance. I was sort of right but also sort of wrong: the various performance profiling tools I've used certainly indicate that my numbers are better but, honestly, the frame rate feels about the same to me.

In any case, I thought it'd be good to give back a little bit of my knowledge, so here are the parameters. All of these optimizations were made in JavaScript. Although I'm very familiar with JavaScript through a career spent as a web developer, it dawned on me in these last couple of weeks that I've never been all that concerned with the actual performance of the code (generally because the time that HTTP requests need dwarf even the most sluggish of client-side scripts). As such, many of these tips and tricks were new to me. Maybe they'll be new to you, too.

For each forEach

One of the first things you learn in programming is the concept of the for loop, the notion of being able to do something a defined (but still dynamic) number of times.

var list = [1, 2, 3, 4, 5];

for (var i = 0; i < list.length; i++) {
  var item = list[i]
  console.log(item);
}

In modern JavaScript, though, the much more usual way of writing a for-style loop that iterates over an array is to use Array.prototype.forEach(). This snippet accomplishes exactly the same thing as the last.

var list = [1, 2, 3, 4, 5];

list.forEach(function(item) {
  console.log(item);
});

I happen to think the latter forEach() version is a lot more readable and straightforward than the somewhat archaic-looking classic for loop but what I didn't know is that it's definitely much slower. I don't really know the specifics here but I believe it has a lot to do with repeatedly calling an anonymous function, along with having to create that anonymous function it the first place. Regardless, in video games, you're constantly running all sorts of loops to make sure every entity in the game gets its chance to handle the input the player is giving you, update its state based on that input (and myriad other factors), and then eventually get drawn to the screen. If you're slowing everything down with something that ultimately just boils down to using a more convenient syntax, well, that's a pretty easy win to get. Converting my code that looked like that latter into code that looks like the former was the vast majority of the performance benefit I saw.

We can do better

Even the humble for loop has a problem, though. It's all about that list.length thing. length is a property on the list object (and, indeed, on all Arrays). Again, I'm not super-familiar with the specifics here but it was made very clear to me that it takes a little bit of extra time to access an object property than it does to reference, say, a variable that has the same value as that property. Specifically, there ended up being a bunch of places where I was able to do something like this and see clear performance improvement:

var list = [1, 2, 3, 4, 5];
var listLength = list.length;

for (var i = 0; i < listLength; i++) {
  var item = list[i];
  console.log(item);
}

This technique represents the very basic concept of caching. When list.length shows up in the for loop's condition, it gets referenced once per iteration, so it can be significantly faster to cache its value in this way. Instead of constantly asking a question--in this case, "How many elements are in the array called list?"--you ask the question once and remember the answer.

Thrashing around

In JavaScript, as well as Java, memory is sort of managed for you. You get to just use however much memory you need and, periodically, the engine will run a garbage collector that frees up all the memory you're no longer using. This is generally pretty convenient, not having to meticulously ensure that every malloc() is paired with a free(), the way you need to in C, or every new with a delete, as in C++. It's not necessarily preferable in video games, though.

When the garbage collector runs, you definitely know it, especially when you're working on, you know, just for instance, an action-platformer that you want to be running silky smooth all the time. You don't really have any control over when the garbage collector runs so the best you can do is to try to minimize the amount of memory you allocate and subsequently throw away. This concept many, many faces that go well beyond the scope of a humble devlog but I wanted to give at least a few examples of how I was able to significantly reduce Ensue's memory footprint and, thus, prevent it from invoking the garbage collector as often as it had been.

A very basic syntax to avoid is this one:

[1, 2, 3, 4, 5].forEach(function(item) {
  console.log(item);
});

I've already talked about the virtues of moving away from forEach() but it turns out that it's also useful to just store a static array somewhere (even if globally) if you know that you're going to be using it a lot. Unfortunately, you forfeit the cool and hip look of defining arrays in place but each of those definitions is a new memory allocation and then an immediate tossing of its reference. I was guilty of liberally using this syntax.

Another easy but wasteful habit to fall into in JavaScript is writing a function that returns a new object. For example:

function arithmetic(x, y) {
  return { plus: x + y, minus: x - y, times: x * y, dividedBy: x / y };
}

This (admittedly very contrived) function returns the results of the big four arithmetic operators when given two numbers x and y. Specifically, every time it's called, it creates an entirely new object to pass back to the caller. You can certainly avoid this by breaking the one function into several and just insisting that the caller doesn't get all four in one:

function plus(x, y) {
  return x + y;
}

function minus(x, y) {
  return x - y;
}

function times(x, y) {
  return x * y;
}

function dividedBy(x, y) {
  return x / y;
}

Alternatively, though, you can have the caller pass an argument that's a reference to an object that they're responsible for making sure is already allocated and ready to be modified. The function could then look like this:

function arithmetic(x, y, result) {
  result.plus = x + y;
  result.minus = x - y;
  result.times = x * y;
  result.dividedBy = x / y;
}

In this scheme, you have to trust the caller to manage the result object in a smart way such that it never gets its reference tossed but the pay-off is that you're not creating a brand new object every time arithmetic() is called. In video games, this technique can get very dicey because you sometimes want, or even need, to call a function like this multiple times per frame and it can be tough to keep track of which values were what at which times. If you're able to cache data and reuse objects like this, though, it can be a major improvement in terms of your game's memory usage.

The elephant in the room

All these code snippets aside, though, the thing that is probably making your game run slow is the time you spend rendering everything to the screen. In mine, even after making a ton of the optimizations I've described, my big fat draw() method was still the main bottleneck. It turns out that it's quite expensive to put something on the screen, which is a concept that I was well aware of from the web ecosystem. In that land, if you're constantly making updates to a page's markup, you eventually learn to start operating on a copy of that markup that you then replace the full DOM with to avoid forcing the web browser to redraw the screen for every single change. Redrawing one change is just as expensive as redrawing all of them at once, both of which are way less expensive that redrawing a bunch of changes in sequence.

In the video game world, this manifests as going out of your way to not redraw anything that doesn't have to be redrawn. A common technique (that I have yet to implement) involves having two layers: one for your background and one for everything else. If you don't need your background to scroll as the player moves around the world, you get to draw it only once! Everything else on the other (mostly transparent) layer will check to see if it needs to be redrawn and, if so, it'll undraw its former state and draw its new one.

I bring this up mostly to underscore a common trap that developers fall into when optimizing their code: they often aren't optimizing the most relevant things. In my case, I sort of knowingly gave myself carte blanche to just work on whatever optimization seemed interesting to me at the time and it didn't take long for me to know that I wasn't attacking the big behemoth. Memory management and saving cycles are important, for sure, but my profilers were consistently telling me that I was simply drawing too much to the screen too often. I'd already long ago made the enhancement where you only draw things if they're within some magic number of pixels of the camera--which, of course, is a huge win if you're otherwise just always drawing everything to the screen, visible or not--but the next level was to start using multiple canvas layers to much more deliberate manage everything's visual representation. I didn't get there this sprint but it's becoming pretty clear that I'll have to get there at some point.

Closing thoughts

Throughout these last two weeks, there were a few resources that I found to be very helpful. No matter what stack you're working with, the most helpful tool is the one that will help you identify where your code is slow and/or bloated. In my universe, that's the performance tool that both Firefox and Chrome make available as a standard feature. Once you know where your code is slow, you need to know the best ways to make it faster. Though there was no single link that that me everything I needed to know about JavaScript optimization, there are many, many things out there that are just a simple search away. Specifically in the realm of video games, though, I found this thread on the HTML5 Game Devs Forum to be very useful, particularly the original post from rafinskipg and the reply by Sawamara. Hopefully, all of this will help somebody else who's looking to squeeze a few more frames per second out of their game!

Next sprint

It feels like it's time for another asset sprint. I'm going to be focusing on things that I've been putting off: background art, sound effects, new tiles, and level design. If nothing else, I'll actually have some thing to, you know, show in my next devlog post. Oh, also! I'm going to try to make it a weekly thing to do a Friday dev stream over at my personal Twitch account. I did a test run last Friday which I thought went pretty well. Tune in for that! I should be starting around 4pm ET or so.