For the last few Septembers, I’ve been following the JS13K game jam. I’ve never competed though, partially because I didn’t have ideas I really liked, but also because mid-September is when classes start picking up. I actually did try last year, but got stuck on trying to squeeze animations into a few hundred KB and never got out. This year, though, I’m proud to present King Longlegs!
It’s a small, moderately challenging platformer set in an bleak, empty world. You control a small spidery creature, avoiding spikes and solving puzzles through nine levels (I originally planned for thirteen, to match with this year’s theme, but ran out of ideas and time).
My main inspirations were Hollow Knight, which I’d finished a few weeks earlier, and Mateusz Tomczyk’s 2022 entry The Wandering Wraith. My original vision was pretty grand, featuring evil ghosts that would chase you, a cool final boss fight, and much cooler level design with statues and pillars, and proper doorways. In keeping with this year’s theme of triskaidekaphobia, I also wanted to have a system where, instead of dying, you’d lose control of your character who would run screaming back to the last savepoint. Sadly, most of this stuff didn’t make it in, and was probably too ambitious for the 13 KB limit.
The art is all vector graphics, rendered with the 2D HTML5 canvas API. The rocky texture of the platforms is procedurally generated, and uses a tiny PRNG so it looks the same everywhere. This means we have to draw many thousands of line segments per frame. I thought this would be nothing for modern computers, but performance was initially pretty bad, especially on Firefox. I’m guessing it’s because the canvas API doesn’t let you render a bunch of points/lines/faces in bulk or stick them in a buffer or something. In the end, I added a bunch of off-screen checks and reduced the number of lines, and performance was ok.
Sound was absolutely a breeze, thanks to Frank Force’s amazing ZzFx library. I spend very little time on sound design, so it’s a little weak, but even a little bit goes a long way.
To create the levels, I wrote a very simple and ugly editor. It was implemented as dev-build-only part of the game, and allowed you to create and edit objects as you were playing. It was held together with spit and duct tape, but without it, the game could never have been completed on time. As it was, my brother Steven and I stayed up until 4 AM (the game jam ended at 7 AM) working on the last levels.
Collision detection is actually pretty easy, at least for sort of shapes you usually have to worry about (triangles, lines, spheres, and points). But determining how to respond to a collison is very challenging. It’s been an on-and-off battle for the last few years, across multiple projects. In high school, I discovered Kasper Fauerby’s Improved Collision Detection and Response, which presents an excellent introduction to swept-sphere collision detection methods in 3D. I was working on a 3D game at the time (long since abandoned), and applied Fauerby’s techniques with limited success. In college, while working on AfterZhev, I used the classic “try to move, if there’s a collision, don’t move” strategy. This is effective and very easy, but struggles with fast-moving objects and angled colliders.
For King Longlegs, I tried applying the swept-sphere approach again. This strategy has a lot of nice properties:
The main problem is floating-point error, which manifests as random fall-through and jittering bugs. I added a lot of hacks and tricks to mostly mitigate these issues.
My favorite part of King Longlegs is the leg animations. They’re not perfect, but I think they really sell the idea of a little spider-thing scurrying about a strange land. It’d be very hard to achieve that feel without them.
Internally, the legs are modeled using as just two points, the hip and foot. (We’ll talk about the knee later.) The hip is fixed while the foot is animated. If the foot is touching the ground, it remains planted to the ground while the rest of the character moves around. However, as in real life, the foot can’t get too far from the hip. Experts call the maximum foot-hip distance the leg length. When that happens, the foot is lifted from the ground and moved forward to find a new resting place. But what if it can’t? Then it dangles helplessly in midair.
This logic forms the core of the leg animations. There is some extra code to handle death, floating in water, and different running speeds. But with a lot of tuning and trial and error, it’s surprisingly simple to create leg animations that look pretty good.
Now let’s talk about the knee joint. Although the legs are modeled as two points, they’re drawn as two lines connecting the hip to knee and knee to foot. This is possible because, if you know the hip-knee and knee-foot distances, and where the hip and knee are, you determine where the knee should be (actually, in 2D, there are usually two solutions, but one involves the knee bending backwards and can be rejected). This is an application of Inverse Kinematics. In general, you use something like FABRIK but our case, has a nice closed-form solution that you can derive using the Law of Sines and Law of Cosines. In the end, the drawing code looks like:
const c = Math.hypot(hipX - footX, hipY - footY)
// Use the Law of Cosines to find the angle between the femur and fibula.
const theta = Math.acos((FIBULA_LENGTH*FIBULA_LENGTH + FEMUR_LENGTH*FEMUR_LENGTH - c*c) / (2 * FEMUR_LENGTH * FIBULA_LENGTH)) || Math.PI
// Use the Law of Sines to find the angle between the long edge and the femur.
const phi = (Math.PI - theta - (Math.asin(FEMUR_LENGTH * Math.sin(theta) / c) || 0)) * walkingDirection
// Calculate the angle between the horizontal and the femur.
const alpha = Math.PI/2 - phi - Math.asin((footX - hipX) / c)
// Calculate the angle between the horizontal and the fibula.
const beta = alpha + (Math.PI - theta) * walkingDirection
// Draw the leg.
ctx.beginPath()
ctx.moveTo(hipX, hipY)
ctx.lineTo(hipX + FEMUR_LENGTH * Math.cos(alpha), hipY + FEMUR_LENGTH * Math.sin(alpha))
ctx.lineTo(hipX + FEMUR_LENGTH * Math.cos(alpha) + FIBULA_LENGTH * Math.cos(beta),
hipY + FEMUR_LENGTH * Math.sin(alpha) + FIBULA_LENGTH * Math.sin(beta));
ctx.stroke()
Thanks for reading! If you haven’t yet, you can play King Longlegs here or on the JS13K website. It ended up doing pretty well, scoring 35/171 in its category, and using 12.6 of the allowed 13 KB. You should also try playing some other entries. People have created some really fun and impressive games.