Post-mortem: Exo #JS13K
Posted on 17/09/2018
Hot off the heels of hosting and participating in #LOWREZJAM, I decided to pile more pressure on myself and enter #JS13K. I wasn't overly pleased with my entry to #LOWREZJAM, because mid-way into the jam my PC decided to have several meltdowns, killing my HDD and GPU. So with a feeling of something to prove, mainly to myself, a fresh start with Linux and a greater jam challenge than the usual I decided this game had to be something interesting.I asked my friend ai Doge if he wanted to enter with me, and he agreed. It was his idea to make a tower-defence style game, and this quickly evolved into the towers actually being satellites, orbiting planets, orbiting a star. On paper it seemed simple, so rather little design went into it from the start and instead things were decided based on how it all fitted together in practice. So sorry if this post-mortem focuses entirely on technical challenges, just the design process was organic and, really, I should have written it up while it was happening. I guess that's just another take-away from this experience!
We used only basic Canvas features, it was a personal challenge to try and support IE as well, so not even all of them! One challenge that presented itself was with drawing "orbit" lines. For whatever reason, drawing large shapes like lines, but particularly curves, is INCREDIBLY slow. The solution to this was to cache each orbit line on another canvas(es), while this did actually render faster, it used up a lot of memory for what is essentially empty space. The next step was to minimize the number of canvases, and their size. This was simpler that expected. Firstly, all orbit lines (at least groups of them, if they share the same target/center) can be drawn onto the same cache canvas, the only requirement being that the cache canvas fits the largest orbit line. Secondly, all our orbits are "perfect", they can all be represented by circles with a single center point. This means they're also symetrical, both vertically and horizontally. So the next step was only drawing one corner of the orbit line. This meant the cache canvas could be 1/4 of the size of the actual orbit. All that was required for the "illusion" was to draw the cache canvas 4 times at 90 degree angles.
Contrary to the orbit lines though, which are rather large and visible, we also have a lot of lasers represented by lots of tiny drawn lines. However, HUNDREDS of tiny lines were barely noticed when profiling (in Chrome) compared to a handfull of large lines being really slow. I don't claim to know exactly what's going on here. But it's clear fillrate is far more consquential than the number of calls to drawing. This is contrary to a lot of resources I've read on line. Maybe it's not universally true, but I'm glad it's something I've found and can keep in mind for the next time I'm doing something like this.
We also used the Speech Synthesis API in our game. Now I wish this was something with an ounce of predictability. On first testing it in Chrome, I tweaked it to a nice robitic sound. However, putting the same thing in another browser can yeild VASTLY different results. In terms of accent being far from your language hinting, and some settings not actually being able to be pushed as far such as pitch and speed. This led me to mostly stripping it back to barely any tweaking and just pointing out on the main game screen that quality will vary! A cop-out, but I found this one area where it seemed pointless to impossible to work as desired.
Something I had much better luck with was the Music and SFX. I chose to use the SoundBox tool and player library because it looked like a tool other people I know might be able to make sense of! I have a friend, Francis Fonye, who managed to make some original music for the game at rather short notice near the end. But, while SoundBox is primarily a music tracker, I managed to get some SFX out of it myself. The main one of these was the laser SFX. SoundBox exports as JS code, so the laser SFX is actually a single sound. What I do in the game is generate many more slightly varying sounds simply by changing the note of the exported code. I made enough of these varying notes that I didn't even have to handle the creation of multiple instances of the same sound. I saw this as a double benefit!
Most of what I learned from this experience were concepts around minification and compression, in particular using Google's Closure Compiler. Our code started off as the kind that "just does the job". Which is no bad thing, on looking at the minified code one can see what's working and what isn't. So there was a lot of rewriting in the mid-phase of development, where we were still finding the right style. A couple tricks we found helped:
Using Closures. A closure is basically a scope that is shared by anything defined within it, like functions, even if they're exported and used somewhere else. Now why is this helpful? One aspect is keeping certain variables private, but that's not important to the mission. Another is not populating the global scope with your variables. But what it really does, that's important to minification/compression, is remove a lot of dot notation. Say you have an array that's referenced a lot by a certain system, like instances of an object. Rather than having the array as a property of the object, like so:
function Object() {
this.something = "else";
Object.instances.push(this);
}
Object.instances = [];
var newInstance = new Object();
We can wrap the thing in an IIFE (our closure) and only export what we need.
var Object = (function() {
var instances = [];
return function() {
this.something = "else";
instances.push(this);
}
})();
var newInstance = new Object();
This is obviously a small example, the usefulness increases with the number of things your system needs that aren't actually required outside of it.
Another trick is using constants/enums. So say you have something that can be of several types, in our case it was towers ("laser", "beam", "lightning", and so on), these strings can't be minified and they'll take up a little more space than needed even when compressed. Closure Compiler can do the same thing with constants and enums, but I used enums because they're basically grouped constants. The intention here is to, instead of writing "laser", write TYPE.LASER. From a readability perspective it's the same, if not better already. But the important thing to note is that that constant doesn't need to be a string, it can be a number. At no point do we actually have to call it "laser". Like so:
/**
* @enum {number}
*/
var TYPE = {
LASER: 0,
BEAM: 1,
LIGHTNING: 2
}
Now, in our code, we can still write something that clearly denotes a laser type. But using enums, Closure Compiler can replace any use of it directly with the value. No string, no extra code in the compiled build. As an example comparisson:
// Using string.
BuildTower(x, y, "laser"); // What our code says.
g(x,y,"laser"); // What CC might put out.
// Using enum (as seen above).
BuildTower(x, y, TYPE.LASER); // What our code says.
g(x,y,0); // What CC might put out.
Again, small example, but the more this practice it used the more space is saved!
Anyway, sorry this post-portem is a bit slap-dash. Like I said at the start, next time I take on a long jam project like this I'll note the challenges as I deal with them. That way, at least the next one might be good! :)