The Terror of Mongolia JS13k Post-mortem (part 1)
Jack Le Hamster
Posted on August 29, 2023
I decided to write this post-mortem early, before the game is actually finished, cause I might just forget some details later. So this is part-1, since there's likely going be a follow-up after the game is completely submitted.
For context, this is a game created for JS13k games (https://js13kgames.com/). The jam runs from August 13th to September 13th. We're a few weeks away from the deadline. Since I'm currently busy with other tasks, I'm stashing my work, then I'll have a couple days remaining to work on it right before submission on September 13th.
The theme for this year is:
The 13th Century
I started around Aug 19th (I can't remember exactly but that's the age of my oldest file). I just needed to practice for a JavaScript interview, and thought: Hey, what better way to train for a JavaScript interview than to join a game jam where the challenge is to make the entire game fit in 13k!
Turns out I was wrong, you're better off just practicing solving LeetCode problems.
But anyway, to start, I needed to find some inspiration. What do you find in the 13th century? Medieval castles, peasants, knights... As I kept digging ideas from the js13k theme website, I stumbled upon Genghis Khan. This reminded me of the way I loved using the Keshik in Civ5. So that was decided, I'd be making a game about those.
Early prototype
I already imagined a horde of horse archers riding through the plains of Mongolia, lead by the Great Khan. So I started with prototyping that experience, with just circles following the main characters.
The early prototype looked terrible, in particular, the following aspect. If I make the followers go directly towards the leader, they converge and it looks weird for horses. If I try to introduce collision detection between the horses, I don't know if I can code that in less than 13k and keep it efficient. I stashed that idea on the side, basically commenting out that whole section, and explored a new aspect of the game.
I was still deciding on how to display the horse, and was still debating whether or not I should just having horse emojis jumping around, to save space on graphics. And this would have been my options: 🐎🐴🏇 . Have I done that, the mood of the game would have been totally different!
But instead what I really wanted seemed impossible to fit in 13kb:
A fully animated horse running through the plains.
but tried that anyway. There was this horse in motion experiment dating from 1878, which was perfect to use as a model. With that in mind, I worked on a vector graphics editor that would allow me to trace over the horse sprite sheets.
Turns out, it was quite liberating to work on the tool and not having to worry about fitting that in 13k. I knew this was only for asset creation, but I still tried to make it look very nice to enjoy the art making process. The tool turned out nice, it was easy to draw a shape: Just drag the corners, and drag from a side to create a new corner, put it back to erase that corner. At the bottom, I was able to see and select each frame of each shape, and tag them differently so that I can attribute them to different sprite.
Spending some time on compression
So I came up with a nice method for compressing the data of the vector graphics, and spent quite a bit of time fine tuning it. Turns out though, I get nearly as much saving by compressing the initial JSON into a zip. But hey, it's part of the game so I'm still gonna talk about it!
To store all image, we just need commands to draw paths, and separators between each paths.
- First, we reduce the precision on each point. Basically, dividing all coordinates by 10 and rounding. That seems to be the limit up to which I can degrade the quality and still maintain a nice animation.
- We start at [0,0].
- Each command stores the difference from one point to the next. So [1, 1] means moving to x=1, y=1. Then [2, 1] afterwards would mean moving from x=1,y=1 to x=3,y=2.
- The size of each point is one byte, which means each x, y value has a range of 16 values, from -8 to 7. In order to draw a long line, multiple small lines would be used.
- A command that doesn't move the pen is a separator, indicating to put the pen down or up, or to switch to a new shape.
From there, I managed to make a very compact file of around 3kb, which when compressed, is about 5% smaller so not that useful after all. But I think it still beats zipping the original json by a few bytes.
Well, it's all nice and everything, but I still didn't have a game yet, so here comes...
The gameplay
I was debating on whether to turn the game into a side scrolling shoot-em-up, or an adventure game. I think it was just simpler to make something similar to Magic Survival.
First, starting with the horse motion, the movement had to show some momentum to really make it feel like you're riding a horse. To achieve that, I use the following process:
- The keyboard controls the acceleration (ax, ay).
- The movement, (dx, dy) is altered by the acceleration, but then capped using the brakes.
const da = Math.sqrt(ax*ax + ay * ay);
sprite.dx = (sprite.dx + ax / da * speed) * brake;
sprite.dy = (sprite.dy + ay / da * speed / 2) * brake;
Note that ax
and ay
are divided by da (the length of the acceleration vector) to avoid having the horse run faster diagonally than horizontally.
Next, we want the horse to be centered on the scene, so we need to have the scene follow the horse.
sh[0] += hero.dt / 20
* (hero.x - canvas.width/2 - sh[0] + headStart * 80 )
* .1;
sh[1] += hero.dt / 20
* (hero.y - canvas.height/2 - sh[1] + hero.dy * 50)
* .1;
This weird formula does the following:
- Make sh, the "shift" coordinates, move away from it's own position (
-sh[0]
) and towards the hero's position offset by the center (hero.x - canvas.width / 2
). - Make sure to add some room towards the direction the archer is shooting (
headStart
) and towards the direction the horse is moving (factored intoheadStart
andhero.dy
). - Make the transition smooth, not immediate (
* .1
).
Now there's still a big piece missing. With just the horse on the screen, that smooth scrolling has visibly no effect. What we need is:
Scrolling the decor
In order to show that something in moving, we need to place some decor on the ground. There are two decor elements moving: the trees and the grass. Turns out each is implemented a different way.
Displaying the grass in a grid
A simple line is used to show grass, but to make it look natural, it needs to be a bit random. For that, seeded randomness is used:
- The world is divided into cells. At any point of time, we show 40x30 cells around the rider. Each cell has one single line of grass.
- The grass's (x,y) is offset by a random number. That number is seeded by the cell's (x,y) coordinates, which means it will always be the same offset, even if the cell disappears then comes back. Those coordinates also don't need to be stored anywhere.
- So if the rider travels to the right, then comes back, a cell can be destroyed then recreated, and the line of grass will remain exactly as it was.
Now, this technique lets us created non-repeating patterns, and make sure the decor in one place never alters, even if you leave the scene then come back. While I'm sure nobody would have noticed if the grass position change, that detail is there.
Displaying the trees by recycling them within a sliding window
The trees are implemented using a completely different algorithm. The reason for that is I thought of it later, realized it was much simpler, but now I'm too lazy to re-implement the grass. (I might later if it turns out I run out of space to fit 13k).
Basically, all trees are first placed randomly (this doesn't have to be seeded randomness).
Then the following code is applied on the tree sprite:
const hx = sprite.x - hero.x;
const hy = sprite.y - hero.y;
if (hx > repeatCond * 2) {
sprite.x -= repeatDistance * 2;
sprite.cellX--;
} else if (hx < -repeatCond * 2) {
sprite.x += repeatDistance * 2;
sprite.cellX++;
}
if (hy > repeatCond) {
sprite.y -= repeatDistance;
sprite.cellY--;
} else if (hy < -repeatCond) {
sprite.y += repeatDistance;
sprite.cellY++;
}
If the tree is too far from the hero, it gets moved by "repeatDistance" towards the hero, which puts the tree ahead of the hero. Basically, we're reusing the same tree and moving it when it's outside the sliding window.
While in this situation, sliding windows is preferable to use, both techniques have pros and cons:
-
Sliding window:
- pros: Simpler to implement. Can use pure randomness.
- cons: Causes repeating patterns (Deja-Vu). Requires to store tree locations.
-
Grid cells with seeded randomness:
- pros: Avoid repeating patterns. No data to store.
- cons: More complex. Requires a seeded random function. Adds calculations
Now that we got the decor and hero in an empty world, it's time to add:
The foes
Starting with the mounted warriors, I had them go towards the hero at first. But I ran into the same problem as the one in my early prototype with horses converging, which looks very ugly. So instead, I changed the behavior as follow:
- The foes have a goal. That goal is set at a random position behind the hero. So the foes often try to go through the hero, with some random offset added to make it a bit less predictable.
- If the foes are too far from the hero, or if they reached their goal, reset the goal.
if (sprite.gdist < 100 || hdist > (sprite.soldier ? 500 : 3000)) {
sprite.goal[0] = hero.x + (hero.x - sprite.x) + (Math.random()-.5) * (sprite.soldier ? 200 : 300);
sprite.goal[1] = hero.y + (hero.y - sprite.y) + (Math.random()-.5) * (sprite.soldier ? 200 : 300);
}
The soldiers have the same algorithm, but their parameters are tweaked make them go more directly towards the hero.
Since the foes and the hero have the same display mechanism, I have all foes use the hero itself as a template, and override some property (like the process
property which dictates how the foe will move).
There are some other interesting tricks and tips used for building this game, which I'll outline below:
Dynamic properties
Another technique I found useful when dealing with sprite properties is to have properties that can be either values or functions. I then use an evaluate
function for reading those properties:
function evaluate(value, sprite) {
if (typeof(value) === "object") {
if (Array.isArray(value)) {
return value.map(v => evaluate(v, sprite));
} else {
const o = {};
for (let i in value) {
o[i] = evaluate(value[i], sprite);
}
return o;
}
}
return typeof(value) === "function" ? value(sprite) : value;
}
This makes updating properties a breeze. I often hardcode values, then suddenly I need to make then dynamic. Rather than scratching my head and figuring out where to update a property, I just turn it into a function that can lookup other properties in sprite
to calculate itself.
Sprite caching
While the game was running fine on my computer, I remembered from the days building Flash games that drawing vector graphics repeatedly was costly. A discussion in the Discord channel for JS13k also reminded me of that.
Caching for this game consists of saving a frame of drawn shape into a canvas, then when I need to display a sprite, I copy that canvas into the main one rather than repeating the draw calls on the canvas's context. This is how I'm able to show 100 enemies on the screen without much slowdown.
if (cache) {
const tag = `${animation}-${frame}-${color}-${dir}-${width}-${height}`;
if (!cacheBox[tag]) {
cacheBox[tag] = {
canvas: document.createElement("canvas"),
};
cacheBox[tag].canvas.width = width;
cacheBox[tag].canvas.height = height;
cacheBox[tag].canvas.getContext("2d").lineWidth = 6;
cacheBox[tag].canvas.getContext("2d").strokeStyle = "black";
showFrame(cacheBox[tag].canvas.getContext("2d"),
dir < 0 ? width : 0,
height < 0 ? -height : 0,
width * dir,
height,
frame,
anim,
color, 0, 0);
}
ctx.drawImage(cacheBox[tag].canvas,
x - hotspot[0] * width - sh[0],
y - (height < 0 ? 0 : hotspot[1] * height) - sh[1]+shake);
return;
}
First grab a tag that represents one single image. The tag will depend on the animation, the frame, the color chosen, the direction and the width and height. (direction, width and height might not be needed, I might remove them during refactor).
If the sprite is marked for caching, an offscreen canvas gets allocated, then the sprite is drawn on it the first time it appears. That offscreen canvas is then drawn into the main canvas.
Note that caching is used for foes and trees, but not the main hero. The reason is because the hero's sprite is skewed upwards / downwards when moving vertically.
Well, that's it for part 1 of this post-mortem. Please come back to check part 2, and of course, try out the game once it's released.
Below is a video showing the state it is at right now. I hope I can make it even better before the release:
https://www.youtube.com/watch?v=JBmQgz7r2Hc
Source code:
https://github.com/jacklehamster/khan-js13k/
js13kgames
Part 2 now available:
https://dev.to/jacklehamster/the-terror-of-mongolia-js13k-post-mortem-part-2-3ell
Posted on August 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.