A story of making a 13 kB game in 30 days. “The Wandering Wraith” post mortem.

TulusTul
TulusTul
Oct 4 · 16 min read

Once a year since 2012 an interesting competition is being held. It’s called js13k and the objective is to make a game that runs in a browser and fits in 13 kB (13 312 bytes). You have one month to accomplish this objective. Typically there are over 200 contestants and while there are always lots of mediocre games, there are also a few brilliant ones in each edition. This year I decided to participate and here’s a story of my journey.

The Wandering Wraith is the final result of my efforts.

But let’s start from the beginning.

13th August 2019

The theme for the competition has been announced. It’s “back” and I had no idea what to do with it yet. The time constraints were pretty narrow so I decided to rush for a working game and think about the theme later. I already had a few very general requirements for my game:

  • It should be super easy to learn — there will be over 200 other games. Who has time to learn them all? The game should attract from the first second. Looking at past editions that’s the mistake that a lot of games do.
  • I don’t want blocky look or pixel art — that’s how most of the games will probably look like. I wanted my game to stand out and look differently. I wanted it to be organic with lots of curves, gradients, and animations.
  • The difficulty must be approachable — another sin committed by lots of past games. They are so difficult I can barely beat them. A 13kB game should be a short and fun experience in my opinion.
  • Be forgiving to players — don’t force them to repeat large chunks of the game. The game should be beatable in a single run. Savepoints are welcome.
  • Good enough is good enough — there’s no time to polish things indefinitely. When something is good enough then just leave it and move to the next task.

After some thought, I settled on doing A 2d scrolling platformer as everyone already knows how to play them and they are mostly fun. I’m not a very active player and didn’t play any platformer in a few years so I started searching for inspirations. I cherry-picked two:

  • Celeste for great mechanics and movement feeling (from what people say, I didn’t play it)
  • Limbo for awesome vibe and physics

I also decided to use vector graphics and procedurally generate as many assets as possible to save bytes.

The next day I began implementation.

Day 1

I started coding by taking my old game written for another contest (which I won :D). It is a top-down platformer so my idea was to just add gravity to it and voila — I have a side-scrolling platformer. Sadly this couldn’t be further from the truth. Eventually, pretty much none of the original code was present in the final build.

The code itself was written with Typescript which I love. I don’t see any reason for writing plain js anymore. Typescript gives me two important advantages:

  • quick refactoring — which is crucial since at the beginning I had no idea how the game will finally look so I will iterate and change ideas rapidly.
  • runtime error prevention — especially with strictNullChecks flag turned on. If the code compiles, it mostly runs as intended. Huge efficiency booster.

Day 2–3

On the third day, I had the first working prototype. It was a controllable ball sliding on a curvy terrain. The curves were interpolated using bezier curves. The collision was a simple check between the ball and interpolated line segments.

First working draft

Day 4 (5.78 kB)

Added bundle size check to keep me aware of my constraints so from now on each day will be labeled with bundle size.

Keep in mind that bundle size is measured after zipping.

I started thinking about creating levels. I needed some sort of editor but writing my own would probably take weeks. There was no time for that.

At this moment the level was just a collection of lines and curves so what do I need? A vector graphics editor. I decided to use Inkscape for that purpose. I can just draw some shapes, export it as SVG, then parse the path to interpolate line segments for collision detection using my bezier function. The bonus is that I can just draw that SVG on canvas so terrain rendering was super easy.

That’s how SVG path definition looks like:

M1 287.5l1.5.5c1.7-1.3 3.4-2.4 4.8-.2.9 1.3.8 3 .5 4.1L6 293.1c-4.9-1-4-3.5-5-5.6z

It’s a collection of commands. M says move to followed by x and y coordinates. l is draw line to followed by destination coordinates etc. Imagine that you hold a pencil above a canvas and someones tell you where to move the pencil and when to press it to the canvas. It was pretty straightforward to parse.

I also added sky and hills in the background.

Parallax background and terrain

Day 5 (7.52 kB)

One of my assumptions was to have an organic look of the game. I decided to go quite literal with that and draw some trees in the background. The algorithm is simple: draw a line and at the tip split it into two thinner lines and repeat recursively. In other words, it is a fractal.

I added small pseudorandom variations in angle and size at each split to have more natural results. I wanted the same results in each run but Math.random doesn’t support changing seed so I came with a custom random number generator implementation.

Static trees didn’t look very engaging so I generated 30 frames of animation for each tree.

The frames were generated in offscreen canvas during the start of the game and kept as images for later usage. This led to some performance issues. As I added grass later in the process there were few hundreds of plants simultaneously on the screen. Each was using a different texture because it was in a different stage of animation. Unfortunately, texture swapping is a very expensive operation for GPU and the game could have run slowly on older machines. I had two ideas to fix it:

  • render a single huge texture with the size of the level — actually one for each frame so 30 in total. This eliminated texture swapping and reduced draw calls. A single texture had around 4000x2000 resolution. Unexpectedly for the first few seconds, the framerate was around 2–3 FPS rising slowly over time stabilizing at desired 60FPS. I didn’t have time to investigate the problem and dropped the idea as experience for players was worse than before.
  • a texture atlas — one big texture with all my sprites. That’s an elegant solution for the problem but would require a separate algorithm for packing the sprites into the texture. What a waste of bytes. Worth doing but not when having 13kB constraint.

Eventually, I left the issue unresolved and decided that what I had was good enough. But I still don’t have 60FPS on Firefox (Chrome is ok though).

For plant placement I used raycasting. The rays were cast from top to bottom and the trees were placed where the ray hit the ground.

The result after day 5. Lots of tweaks are still required.

Day 6 (7.96 kB)

I had basic hero controls and platforming.

Basic platforming

I pursued the idea of rigid body simulation with multiple dynamic entities on the screen. The player was also simulated as a rigid, Newtonian body. Ohh boy, it was a trap but more about it later. I had no idea what I was doing and didn’t have any plan for gameplay yet. All of this was ditched later.

Rigid bodies simulation

Day 7–8 (8.3 kB)

Various small improvements.

Day 9 (9.04 kB)

I still didn’t have a vision for a visual style of the game but I was tired of the floating ball so I started experimenting with the player character. Maybe things will clarify themselves if I had something more than placeholders.

First hero design

Day 10 (8.24 kB)

Bundle size-reduction day.

I learned about two topics this day:

Mangling properties names

Mangling works great for things like variables and functions out of the box in most minifiers. It looks like this:

const foo = {bar: 1}; becomes const a = {bar: 1};

As you can see property name stayed untouched. This is often desired behavior because JavaScript is a dynamic language and you may want to do this:

const foo = { bar: 1 }; 
const field = “bar”;
console.log(foo[field]);

In this case, mangling property names will cause runtime errors in the production build. However, having a narrow bundle size limit I wanted to be more aggressive and mangle the properties so:

const foo = {bar: 1}; becomes const a = {b: 1};

Fortunately, Terser plugin for webpack has an option for that but the implementation is rather naive. It has a blacklist of symbols which he doesn’t mangle like canvas or width. All symbols that appear in the browser API are blacklisted. This is a sane default, otherwise, mangling could break using those APIs. But there's a problem. A lot of my properties fell into the blacklist and were not mangled. For me, every byte counts so I had to do something about it. There are two options here:

  • add those symbols to the whitelist
  • or rename the properties

I ended up with the second solution and added _ suffix to the properties so my code is now full of properties like canvas_, offset_ etc. The bundle size is reduced. It is good enough.

The alternative solution would be to use Google Clojure Compiler which can mangle properties in a very intelligent way but I had some troubles integrating it with TypeScript and the results were worse than when using terser so I gave up the idea.

Zipping and the entropy of information

That’s a tricky one. What should one do to keep code small? Avoid repeated code the other might say. It seems natural that if you want to have minimal bundle size you should write as little code as possible. It turned out to be the opposite.

This is because zipping algorithms are not interested in the input size. They care about input entropy.

I’ll show this using an extreme example. Let’s print foo one hundred times.

for (let i = 0; i < 100; i++) {   
console.log("foo");
}

This is a quite complex snippet. Lots of keywords, parentheses, numbers, etc. It has high entropy.

Let’s unroll the loop:

console.log("foo"); 
console.log("foo");
...
console.log("foo");

We just repeated console.log 100 times. The code is much longer. Not a good practice in typical circumstances. But it has lower entropy. It is the same code repeated many times. Internally a simple zipper may store is as

<console.log("foo");>*100

which is much shorter than the example with for loop.

What does it mean for us? It means that to write good code for js13k you have to think differently. You have to think in terms of entropy instead of code length. An example of such thinking is to avoid temporary variables in code. It is better to inline all expressions even if they repeat multiple times. Because of this, my code has lots of multiline if conditions, repeated expressions, very little variables and other crap which I would never merge into a real application.

Day 11 (7.72 kB)

This day I did quite a big refactor which nicely reduced bundle size.

I added grass which is implemented the same way as trees.

First draft of grass

Did a proper procedural character animation. Mostly simple trigonometry.

Final version of character animations

And most importantly I finally settled on esthetics. I went with black and white shades similar to Limbo. I love black and white and playing with lights. It also helps to store some bytes as I don’t have to keep color information anywhere in the code.

Light mood
Dark mood
Final version for comparison

Day 12–15 (7.33 kB)

It was Sunday morning when I started thinking about adding more complex stuff to levels like pickable items, checkpoints, etc. This can be done in a hacky way using Inkscape but it won’t be very pleasant to work with. Also, I wanted a fast iterations. I want to make a change and test it immediately in-game. This was not possible using Inkscape. So I decided to write an editor. I knew it was a horrible idea but I took the gamble. I was ready for failing to deliver the game at all.

Actually, it took me 4 days to implement the editor so I overestimated greatly and oh boy, it was probably the best decision I made.

Having an editor opened whole new possibilities. I added new primitives: platforms. Those are 4 line segments with collision in the shape of a rectangle but stored as a single point which saves a lot of bytes. I used them extensively across all levels.

Another interesting thing is that I save all coordinates divided by 10. This has two benefits: it saves bytes by lowering number precision and gives me auto grid snap so I easily place two perfectly aligned platforms next to each other.

The editor increased bundle size by 0 bytes. Everything related to the editor is bundled only in development mode and completely ignored in the production build. This is thanks to the webpack-conditional-loader.

Editor in action

This is how the definition of the first level in the game looks like. I have 11 levels in total.

400 70 0–2–6l14 1c12 5–19 13–8 34 25 3 25 6 48 1l15–2 0–3c6 0 12 4 32–1 2 4–4 5 0 11dl7 0dc4–8–3–4 0–11 9–4 14 0 23–1–1 8–6 8–3 13d13–5 7 4 21–1d2–5–4–5–1–10 17 4 78–1 81 2l0 13dc19 0 11 18 33 15d48 2 57–6 102 0l0–11c10–1 23 3 43 5l-1 21–406 1czm253 26c10 2 16–6 18–2l0 7 9 0 0 9 10 0 0 10–18 0–19–15czC138 16C314 20C251 29C228 32C184 8C350 24dpv318 43dv309 43v165 24v184 18v350 36V318 31dV309 31dV217 21s158s295s91s347s223

This is very similar to the SVG path definition. It contains some extra commands like draw platform, draw pickable crystal or toggle spikes on the terrain.

Day 16–17 (7.51 kB)

Various improvements.

Day 18 (7.33 kB)

I had basic physics, nice visuals, level editor. Time to make a game out of it. So I started refactoring renderer :). But that went pretty quickly. This day I also:

  • improved character design
  • improved hills rendering
  • added progressing through levels
  • added menus
  • added savepoints
  • added sounds
  • added spikes on terrain and platforms
  • added death with particles effect

The renderer refactor was worth it. Despite so many new features, the bundle is smaller than the day before. The renderer was inherited from my previous game so I rewrote it to precisely meet the new requirements.

For sound effects, I used ZzFX by Frank Force. It is extremely efficient in terms of bytes and I’m quite happy with the results. It saved a lot of headaches I had last year with sounds.

By the way, Frank is also a contestant this year. Check out his entry Bounce Back.

Day 19 (9.21 kB)

The game looks nice so far but only from the outside. In reality, the physics make it unplayable. It appears that physically correct, rigid body simulation is a very bad choice for controlling a platformer hero. There are quite a few downsides of it:

  • going down the slope makes the hero fall for a moment
  • going up the slope increases friction slowing down the hero
  • collision responses were ok for a simple ball but sometimes very strange looking for a hero with legs
  • generally hard to make robust hero controls with a fun feeling

So I decided to throw away all my rigid body physics and start almost from scratch. The new physics I designed had only one purpose: to control player character. The first step was to find contact points with terrain:

The red dot represents the contact point

The player has few different modes of movement, each with separate physics:

  • running — triggered when the contact point is below the hero. In this mode, the hero is glued to the ground. He can move left and right and is not slowed down by high slopes. The stickiness is achieved by finding the nearest ground using raycasting and teleporting hero to that position.
  • climbing — triggered when the contact point is at the side of the hero and he is not running. In this mode, the hero is frozen and cannot move. He can only jump which changes the mode to falling.
  • falling — triggered when the contact point is above the hero or there is no contact point.
  • flying in a bubble — triggered by touching a bubble.
Final platforming experience

The flow of controlling the hero finally felt natural. It was good enough.

Day 20-21 (9.73 kB)

I started working on level design.

I added an objective to the game — collecting crystals. From a time perspective, I should have called them “gems”. It would save me a few bytes.

I also added rain/dust particles in the air which looks cool.

Lots of various improvements.

Day 22 (9.8 kB)

First playtests with colleagues revealed that most of them were falling off platforms. This is because they wanted to jump at the very last moment but were a little bit too late. So I granted them an additional 150ms of time to jump after passing the platform. The annoyance ended, colleagues were happy.

Day 23 (9.97 kB)

Added bubbles mechanic inspired by Celeste. It adds some variety to the gameplay.

Working on the next levels.

Lots of various improvements.

Day 24 (10 kB)

Added a little spring effect when the hero falls on the ground. Adds a nice touch to the game.

Day 25 (11.3 kB)

New mechanic — antigravity crystals.

The biggest issue the game had at the moment was the lack of title and theme. I still didn’t know what the game was about and I had only 5 days left.

My initial idea was that the hero was an alien who crashed on Earth. He would have to find a rocket part that broke off the ship while crashing and then return to the ship and repair it. This would nicely fill the theme as the player would have to go back through all the levels. I ditched the idea because making the levels interesting in both ways was beyond my abilities.

Unfortunately, most people didn’t see an alien in the hero. It looked more like a ghost. So it became a ghost. A wraith. The Wandering Wraith. I added the hangman in the initial location to adapt the game to the new theme. The objective is to guide the wraith back to his grave. This is not a very creative resolution of the competition theme but it is good enough.

The final version of the title screen

I also added the end screen with some statistics.

Who can beat me?

Day 26 (12.4 kB)

Added more levels.

I attempted to make music but resigned after two or three hours. It would take me too much time and consume too many bytes.

Instead, I added wind sound in the background. The sound frequency varies slightly over time which creates a quite natural effect. I also adapted all plant animations to look like the wind is blowing from left to right to make it consistent with dust in the air.

Day 27–30 (13.3 kB, 2 bytes left)

The last days were just polishing, adding more levels and balancing them. I’m very glad I had those few days of stabilization.

The game was playtested by colleagues a lot and I received plenty of constructive feedback. It got much better during that period.

The final game has 13310 bytes which are 2 bytes below the limit.

When unzipped the game is a single index.html file which weights 34.8 kB.

What went right

  • the look and sound are better than I expected. I’m quite proud of it.
  • the editor turned out to be the single best decision I made in the process.
  • the pacing of development was good. I had something playable early, could afford to drop bad ideas and introduce new ones and had time to polish the game.
  • the game is playable I think. Much better than my last attempt.

What went wrong

  • meeting the “back” theme — I introduced the theme late in the development and all I have is a single sentence on the title screen. Some gameplay mechanics related to the theme would be a nice addition.
  • level design — that was the hardest thing during the process. I wish they were more creative, balanced and interesting. Some puzzles here and there would be cool.

Summary

This was great fun. I wish I had more time as there’s still a lot of place for improvements and a few unresolved bugs. Overall I’m happy with the results and look forward to improving the game after the competition.

Thanks for reading. Try out The Wandering Wraith and check the source code if you haven’t already.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade