Afterlight Caves

A trailer made from an earlier version of the game with fewer visual effects

Afterlight Caves is a procedurally generated, top-down twinstick shooter game for the web developed by me, Cole Granof, and Matt Puentes. The gameplay is something like a cross between The Binding of Isaac and Geometry Wars, and it runs entirely in a browser.

We developed the game over the course of four months, from November 2019 to March 2020 in our free time. It's made entirely in pure JavaScript, with no external front-end libraries. The scripts run directly in a browser, and we use a Node.js express server solely to serve the static files and keep track of high scores.

We wrote all of the game engine and logic from scratch. For graphics we just use the JavaScript canvas API to draw simple shapes and lines. We run the main game loop and a constant speed so the game always runs at your monitor's refresh rate. We put a lot of work into optimizing our code, and it runs at a stable framerate even on low-powered hardware.

The whole thing is also free and open source software, with source code available on GitHub.

Due to the nature of working in a small team we were all involved with pretty much every aspect of development, but here are a few of the features I focused on.

Controls

The twinstick genre practically requires the use of a controller with two sticks, but we also wanted the game to be playable on a normal computer without any specific hardware. To achieve this we needed the user to be able to naturally switch between using a keyboard or controller on the fly, a surprisingly difficult task in the environment of a web browser.

There is a rather sophisticated gamepad API that works with all modern browsers, but unfortunately the way it's implemented makes it tricky to use alongside keyboard input.

In JavaScript, keyboard input is detected asynchronously through event listeners like this:

element.addEventListener("keydown", event => { if (event.key === "a") console.log("Pressed the 'a' key"); });

The function you pass to addEventListener() gets automatically called whenever a keydown event occurs, which is nice and straightforward and easy to deal with.

However, detecting gamepad input is done synchronously, which means you have to poll the gamepad object to see what buttons are being pressed each game step, like so:

for (const gamepad of navigator.getGamepads()) { if (!gamepad || !gamepad.connected) continue; if (gamepad.buttons[1].value > 0 || gamepad.buttons[1].pressed) { console.log("Gamepad button '1' is pressed"); } }

To combine these two different styles of getting input, I created a buttons module that abstracts all input methods into a single object. You can easily define any number of Buttons (which represent a single keyboard key or gamepad button) and Directionals (which represent a set of four keyboard keys or one gamepad joystick). This also makes it easy to rebind keys so you can customize controls to your liking.

Basically, the buttons object maintains a list of keyboard keys, gamepad buttons, and gamepad joysticks it cares about, and keeps track of their state. For Buttons this means whether the button is pressed during the current game step, and whether it was pressed during the last game step (so you can do things when the button is first pushed or released). For Directionals it keeps track of the vector formed by combining all of its keys or measuring the x and y coordinates of its joystick.

It has event listeners that fire whenever a key is pressed that check whether it's a key we care about and updates the entry for that button. Whenever a gamepad button is pressed it switches to gamepad mode, where it polls the gamepad each game step and overwrites the entries for each button. The next time a keyboard key is pressed it stops overwriting until the next time a gamepad is used.

Using this system you can seamlessly switch back and forth between using a keyboard and gamepad, and even graphically rebind every key through a menu.

The menu of Afterlight Caves, with entries to rebind each control
The controls menu

Beam Shooter Enemies

We all worked on each of the seven enemy types in the game, but the one I spent the most time on is the Beam Shooters. These enemies slowly move in a random cardinal direction and rotate to face the player character when it's in range. Unlike all other enemies in the game, the Beam Shooter fires a continuous beam rather than a physics-based projectile. Implementing this beam required a bit of creative math.

The beam cuts straight through entities (like the player character), dealing damage and maybe inflicting status effects, but it stops at the blocks that make up the walls of the game world.

There can be potentially dozens of these beams on the screen at a time, and each can have a length as long as the game world itself, so the algorithm that calculates a beam's length needs to be efficient.

I wrote a blog post that goes into the design of this algorithm, but the gist is this: start from a point in the world (the origin of the beam) and march in a direction until you hit a wall. The key observation is that the game world is aligned to a grid, so you only need to check for collisions with solid blocks at points where the equation of the beam line intersects a grid line.

This implementation detail ended up being a fun little programming challenge with a practical benefit. Drawing beam projectiles is efficient enough that you can have many of them on screen at a time—with arbitrary length—without a noticeable slowdown.

Powerup System

My favorite part of the project was designing the system of powerups. There are 26 different powerups in the game (one for each letter of the alphabet), each with a magnitude level from one to five. Every powerup is designed to be unique and useful in different ways, and they all interact and synergize with each other in interesting ways.

Synergy between Elastic and Xplode powerups

For example, if you pick up an Xplode powerup it makes your bullets explode into more bullets. If you pick up an Elastic powerup it makes your bullets bounce off walls. If you get both then the elastic effect is also applied to the sub-bullets. The powerups are designed in such a way that this works for every combination, creating unique play styles each time you play.

If you're curious about this system, I wrote another blog post that goes into more detail on the game design and technical decisions that went into its design.

Conclusion

We got to present the game at PAX East 2020 at the WPI booth, which was an amazing opportunity. We got some useful playtesting feedback, and it was great to talk to other game developers and try out their games.

There are many more interesting features I could write about, so perhaps I'll post some more about Afterlight Caves on my blog in the future. It was a fun personal project to work on, and and it ended up being a fun game that runs on pretty much any hardware.

Screenshot of the game, shoing splatter effects, the environment, and the player character