Last year, I entered JS1K for the first time with a game based on the 8-bit classic, Thrust. My entry for 2015 is another retro game — this time inspired by the arcade shooter Defender. Since writing this article, Defender went on to win JS1k 2015!
Squeezing such an iconic game into a kilobyte while keeping true to the original was a real challenge. The 1024 byte limit meant I couldn't include all the original game content but I did try to keep the core of my entry as close to the original as possible. Here are some of the features I managed to implement:
- The AI: Alien landers hunt down and abduct humans. Abducted humans are mutated and return to hunt down the player. Landers and mutants also fire missiles at the player.
- Infinite scrolling landscape and parallax starfield.
- Sprites, lazers, explosions and warp effects.
As with last year, my intention was to write an entry that would fit into 1024 bytes without mangling the source code with a packer (I like to see how things work). I did manage to write a playable version of Defender in 1023 bytes without a packer, but I wasn't entirely happy with the way it looked and I wanted my entry to have more features. To get the most from my 1K limit I was going to need a packer — time to start again.
To make developing with a packer efficient I needed a build tool, so I wrote a simple grunt task to minify the source with Uglify, post-process the result (adding my optimisations) and then pack it with RegPack. The packed output was injected into the JS1k 2015 shim and saved to disk. The grunt task also monitors the source file, triggering the build process whenever changes are saved.
Once the tooling was ready I started by cherry-picking parts of my prototype, reformatting and unrolling code to make it "packer friendly". Next, I incrementally added new features such as colour graphics, mutants and improved AI until I was just over the 1K limit. Once I was happy with my entry I disabled Uglify and, using the minified output as my source, began manually shrinking the code.
Writing code for a packer
Writing compact code is a completely different challenge to writing code that compresses well — with a packer, it's all about repetition.
For example, in the prototype I use a logical flip-flop to control the main
for loop counter, allowing the collision and movement logic to run twice for enemy landers. In the on state, the code is used to check collisions against, and fire at the player. In the off state the same code is used to hunt down and grab humans. In the final packer-friendly version, I dropped the flip-flop in favour of duplicating the entire code block to introduce repetition and produce a smaller output file.
To get the best out of RegPack I needed to understand how it worked with my code so I created a tool to visualise the compression patterns it was using. The tool patches the decompression loop of RegPack (and JSCrush), forcing it to wrap each decompressed substring in a
<span> element. The packer is then run and the resulting string is rendered to the DOM. Finally, a little CSS is used to produce a visual representation of the various strings and how they are nested.
Identifying substrings that compress well allowed me to restructure the source code to exploit these patterns and further reduce file size. Simple changes, such as reordering operators in an expression or ensuring an
=0 assignment preceeds a comma, saved one or two bytes per statement. These small gains soon added up.
One of the biggest repeated patterns is the pixel plotter. It's used to draw the terrain, stars, sprites and the lazer pixels. The same set of statements are used to clip, scale, colour, and draw the pixel rectangle, so they were crafted to have an identical signature to ensure they pack efficiently:
My entry contains 5 single colour sprites, each 7 pixels wide by 7 pixels high. Limiting a sprite to a single colour allows each 7 pixel row of image data to be stored in a single byte, using its individual bits to determine if a pixel should be painted or not. Sprites are purposely 7 pixels wide to avoid touching the upper, 8th bit — setting this bit high will cause the value to be stored as 16 bits.
Storing data this way meant I only used 7 of my 1024 bytes for each sprite, but it did present a problem; some of the image data contained bit combinations that produced unprintable characters when converted to a string. To get around this, I XOR each byte with fixed value of 69 (determined by the sprite generation tool) to ensure that each byte contained enough high bits to produce a printable character.
Once the sprites are encoded, the resulting string looks like this:
Note: The lander sprite is purposely duplicated for the mutant. In the original game the two enemies look very similar. Duplicating the sprite results in a smaller packed file (yet more repetition) and uses less bytes than any logic I could write to share the sprite.
The sprites used in the demo were generated with their row/column data transposed so they appear rotated. I did this to cater for variable width sprites (the original game has a wider player ship) without having to deal with the issues of crossing the 8th bit. Unfortunately, I didn't have enough space left to make use of this.
Drawing the sprites is simply a case of applying the XOR again to remove the mask, shifting bits and painting rectangles. Here's an example of painting the player ship:
The colour of the sprite is determined by setting the
fillStyle property with
hsl(i, 100%, 50%) where the hue
i is the index of the sprite (0-4), multiplied by 99 (another packer friendly value). The 4th sprite produces a hue value greater than the maximum 360, but that's not a problem because hue is an angle so values implicitly wrap.
Warping and explosions
Creating the visual effects such as enemy explosions and warping, or mirroring the player sprite when it changes direction was fairly simple. All the effects are achieved by scaling the coordinates of each sprite pixel as it is painted. Explosions scale both the X and Y values and mirroring is acheived by scaling the X coordinate by
-1. As I'm already painting each sprite pixel, scaling them during render required little effort. Here's an example of how it works:
In the final demo you may have noticed that the sprites are flipped when they explode. This is because the warp factor is a negative value during the explosion and I had to trade-off computing the absolute value to save bytes.
Drawing the background graphics
The background graphics in Defender consist of a rugged undulating terrain and a parallax starfield. While both these elements are purely cosmetic, dropping either of them from my entry made the game feel lifeless so both had to be included.
The terrain is generated from a simple cosine wave. The current loop iteration count is given the bit mask treatment and passed to
Math.cos() and the result is added to the value from the previous iteration, producing an undulating mountain range.
Finding the perfect bit mask was a case of trial and error. The terrain must tile seemlessly because the player can fly in the same direction continuously, therefore any terrain generation formula must produce identical start and end values. I found
Math.cos(iteration / 5 & -11) produced a reasonably rugged mountain range while appearing to wrap infinitely:
The planet width is actually a bit mask (yeah, another one) with the lower 10 bits set high. The width is derrived from
Math.pow(2, 10) - 1, which produces a value of 1023. This mask is used with a logical AND to keep values inside the world. I'm using a bitwise operator over modulus because stripping bits prevents negative values.
The same values calculated for the terrain are also used to draw the parallax starfield. The X position is halved, producing a slower scrolling speed, and the Y value is scaled up and logically ANDed with 1023. Scaling the Y position in this way purposely results in the majority of the stars ending up off canvas, which saved writing logic (and bytes) to distribute them along the X axis.
Here’s the final, packed source for my entry:
...and here's the full annotated source code: