For the 2019 js13k contest, my goal was to make the top 10, so I decided to go all out and build a full blown FPS with hand crafted levels and a final boss. The project turned out to be one of the most fun and satisfying projects I have ever worked on. In this postmortem, I will go over some of the more interesting aspects of the game which include game design, graphics, and other engineering topics. Let's get started!
The theme for the 2019 js13k contest was "Back". For inspiration, I went online and looked up words that started with "back". After looking around for a bit, I found the word "backroom", and somehow, that made me think of the mysterious back rooms in gas stations that I've always wanted to peek into. After some more thought, I came up with the idea for my game. You are a new employee at a creepy gas station. Your boss calls you, and tells you that he wants you to meet his son Bubba Junior in the back room. You hang up the phone, and make your way there. After walking around for a bit, you discover a giant tunnel in the wall that leads to an underground world full of zombies and 3d puzzles.
To create a 3d game, you generally want to stick with WebGL because your shaders will run directly on the GPU which is optimized specifically for graphics, and thus is much more performant than rendering 3d projections onto a 2d canvas, which would happen in the CPU. You can of course create 3d games in other ways, like using 3d CSS transorms, but this has a ton of limitations and will generally not be very performant. Because the js13k contest is all about creating games that fit inside 13kb, we of course can't use a heavy 3d library like THREE.js. We will need to write the 3d engine from scratch, including the vertext and fragment shaders.
I wanted Bubba's Back Room to truly be a 3d world (not just 2d maps with 3d walls). I wanted the player to be able to run up and down hills, walk up steps, jump on platforms, etc. To do this, I decided to keep the world geometries simple by constructing the entire world out of voxels (cubes). With voxels, you can create all kinds of interesting 3d structures. For the most part, cube geometries are defined by two triangles per face for each of the six faces of the cube. Then you need to define the normals for each face, which are the vectors that point away perpendicularly from each face. This is needed for lighting. Once you have the geometry setup for a cube, you then need to write vertex and fragment shaders. For Bubba's Back Room, I wrote these in GLSL, and my compiler turned them into JS strings which are used to setup the shader programs at execution time.
For game text, I ultimately found that a super small sprite sheet is needed, because essentially you need to encode the placement of a bunch of pixels to form each character. I did some explorations using JSON structures, and even elaborate seeds that could encode pixels for each character, but ultimately just using an ultra tiny sprite sheet used up the least amount of space (just around 100 bytes). I chose to use the smallest dimensions per character that were still legible, requiring them to fit inside a 3px by 5px bounding box. You can see the alphabet for these characters in the above screenshot of the master sprite sheet. At runtime, I took the tiny text sprite sheet, scaled it up, and copied it to the master sprite sheet.
Finally, the textures that are applied to all of the voxels are similarly generated at runtime. For the most part, each texture takes in an array of sample colors, generates a pattern, and then constructs a gl texture out of it. Most of the textures are either fully random, are tiled with highlights and shadows on the edges, or have a brick pattern.
I think the world generation logic for the game was my favorite part to work on. Because the entire world was constructed out of a single primitive (a cube), I had a single addBlock() method that would add a block at a specified position with a given texture. Next, I wrote the addPlane() method which would construct planes or poles by iteratively calling the addBlock() method. I created a method called addSlope() which was used to create stairs and other sloped features in the world. I created an addRing() method which would add a ring of blocks, and addTunnel() which would string together a bunch of rings to create tunnels. I created higher level methods like addRoom() which would construct floors, walls, and ceilings. I also added methods like addPole(), and addTable(). Once all of these methods were setup, I was able to construct the entire world in about one night's work.
Yea I know, the zombies don't look that great. I had originally planned on making cooler models for the zombies (in fact I had plans for different kinds of monsters), but I just honeslty ran out of space. I ended up constructing the zombie models out of cubes, of which I already had buffers and textures working. I used three cubes for the head, one for the neck and body, one for each of the arms and legs, one for each foot, one for each set of fingers.
In terms of rendering, I chose to render each zombie as an individual draw call. This simplified things a lot because for each animation frame, I just needed to iterate through each zombie, translate it to the right position, rotate it, and then render. Because there were never more than a dozen zombies on the screen at a time, the render performance was still pretty good.
Having studied physics in college for several years (I originally wanted to be a scientist!), physics is near and dear to my heart. The physics in Bubba's Back Room are actually pretty simple. Moveable bodies such as the player and zombies can be affected by gravity which acts on them at all times, pulling them down towards the ground. To compute the effects of gravity on a body, you just need to compute how far the body has moved for each animation frame. That equation is d = 1/2 * at^2. d is distance, a is acceleration (gravity), and t is time, which is the time that has passed since the last animation frame. Once we know how far the body should move for the given animation frame, we can use a technique similar to ray tracing to march the body downwards until the final position is reached, or until it hits a voxel and stops.
For jumping, we can simply initiate an upwards vertical velocity on the body when the jump occurs, and then let gravity pull the body back down to the ground. The equation used to compute the distance travelled for a given animation frame is d = vt, where d is distance, v is velocity, and t is the time that has passed since the last animation frame. This applies to both the player and the zombies.
In order for the gun fire to feel realistic, I wanted the bullet trajectory and impact to be instant. I also wanted the bullet impact to occur directly in the middle of the screen where the crosshair is. For this scenario, a hit detection technique called "picking" will work perfectly. Picking works by rendering two canvases. One canvas uses shaders to generate the scene that the player sees on the screen. The other canvas, which is hidden, uses shaders that render solid colors which map to ids. Whenever you fire your weapon, we can grab the x,y coordinate of the crosshair, and then "pick" out the pixel that corresponds to that location from the hit canvas. Once we have that color, we can use it as a key to lookup a data id. For Bubba's Back Room, these ids map to zombie instances. Once we know which zombie the bullet has hit, we can execute a damage routine which causes the zombie to flash and reduces its health.
As you can see in the screenshot above, I am using the blue channel to render the world, and the red channel to render zombies. It's important to render both the zombies and the world because you want to make sure that objects in the world, like columns or walls, will occlude the zombies and thus prevent bullets from traveling through objects to hit them. In the screenshot above, you can see four zombies. Although they all appear to be the same color of red, they are actually different shades of red. The first zombie has a color value of (255, 0, 0). The second zombie has a color value of (255, 0, 1) etc.
Bubba's Back Room was my third js13k attempt (I submitted entries for 2017, 2018, and 2019). For my 2017 and 2018 entries, I used a tool called jsfxr for sound effects. It worked fairly well - the online tool was really outdate, but it worked. The library size however was pretty large, and the information that needed to be encoded for each sound effect was also pretty large, making this library a bit less ideal for js13k contests.
Enter 2019, and I started to notice some chatter on Twitter about a new tool called ZzFX (Zuper Small Zeeded Zound Zynth). I checked it out, and was blown away. Not only is the tool just simply awesome, but the library itself is unimaginably small, and as if it couldn't be even more awesome, the creator of the library also uses seeds to encode information about each sound effect. The seeds are just 5 digit integers, which is also incredibly small. When I switched from jsfxr to ZzFX, I saved hundreds of bytes, enabling me to fit in more game features.
It is my opinion that music can make or break a game. js13k games without music feel like incomplete games. For the js13k competition, we obviously can't pull in WAV files which can be several MB in size. Instead, we should leverage the built in HTML5 Audio API. There are several ways to do that - we could construct oscillators manually, we could string together sounds from a sound effects library like ZzFX, or we could use a music composition library. I took a stab at the first two and wasn't able to create compelling sound tracks. For the sake of time, I decided to use a library called Sonant X, which has an amazing online tool for composiing songs. The main downside of course is that the library plus song JSON can reach up to nearly 2kb, which is massive. I felt like it was a necessary cost though.
Keeping the game below 13kb was a constant battle. It didn't take long before I surpassed 13kb, and I constantly found myself adding a new feature, doing optimizations to get the game below 13kb, adding new features, and repeating. Aside from all kinds of micro optimizations, here are the biggest byte savers:
For the final boss room, I wanted to do something that was unnerving for the player. I decided to place a black hole in the middle of the floor, suggesting that the player takes a leap of faith, quite literally, down into the hole. When you jump in, you fall for awhile, and then land in a pit of lava full of columns. Hopefully, you jumped right down the middle of the hole, or else you'll land in lava and die a slow death. Although I did this because some of the most fun games that I have ever played were also pretty unforgiving, I also left this quirk in for my own amusement. I assume that there were lots of "WTF?!?" moments. Anyways, if you jump perfectly into the center of the hole, you will land safely on a column, and begin the final fight with Bubba Junior. I haven't included a screenshot of Bubba Junior in this article because if you want to see him, you'll have to play the game and see for yourself!
The final results of the contest? I was ranked 47th out of about 250 entries. To be completely honest, I was't thrilled - as I had mentioned, my goal was to make the top 10. Fortunately though, this year, the creator of js13k games used a new scoring system that makes it prety obvious where your game excelled, and where it lacked. As you can see from the screenshot above, my game did pretty well in most of the categories, but did pretty poorly in the "innovation" and "theme" categories. Which I think is fair. From a gameplay perspective, Bubba's Back Room is not particularly innovative, and the theme tie in was a bit weak, I admit. Next year, I will be sure to focus more on those two categories.
I think anyone who has submitted a js13k entry ultimately thinks "I wish I had had more time to do FOO, or more space to do BAR". For me, I ran out of space before I ran out of time. Here are some things that I wanted, but just didn't have the space for: