Devlog: Inside Runners' Grave


Introduction

I built this game in Phaser, a versatile Javascript game engine.

I was first introduced to Phaser a few years ago in a college game dev course. I was really impressed by the amount of features and solid documentation in a web-based engine, so I started using it to build my own solo projects and game jam games.

Phaser's Arcade physics was the perfect fit for this game. Tilemap system allowed me to load both procedurally-generated levels and static ones made in Tiled. I also took heavy advantage of visual features like particles and setTintFill() to give the game's elements custom and randomly-generated color schemes. Overall, I had a lot of fun developing this game, and I really appreciate the work of Photonstorm and the Phaser community in building and documenting the engine's extensive features. If you want to make a cross-platform 2d game, consider Phaser!


How to make a random maze

Central to the game it its' procedurally generated mazes. I started from a YouTube tutorial I can no longer find, but the basis is a three-step process. 

First, the maze itself is generated using a depth-first search (backtracker) algorithm. The algorithm itself can be found on Wikipedia, but in short, if we treat a maze as a grid of little rooms separated by walls, we can create a maze by going through and removing walls starting from one of the rooms. If we continue without destroying a wall to enter a room we already visited, and going back if we find a dead end, we can create a maze such that there is only one path between any two spots in the maze. I implemented this algorithm with a stack that contains the next node to visit. It traverses a 2D array of integers which represent the state of the room's walls with bit flags. (0001 for above, 0010 for right, etc.) For each room, we choose a random unvisited "neighbor" to "visit" by destroying the wall between us and it (removing the bit flags). Then we mark that room as visited (|= 1000 0000), add it to the stack, and continue until stack is empty. Now we have what I call the maze "key".

An in-game demo showing a maze being created by deleting walls.

This visual demo shows the algorithm in action, generating a 5x5 maze. The cells/rooms marked "Visited" are shown with an "X", and the current cell we are searching from is shown as Toby. Making this was a surprisingly complicated because I actually had to edit the tilemap while the maze was generating. You can access this demo by typing "demo" on the hub level.


To translate the "key" into an array of actual walls, I create a 2d array double the height and width of the key (+1), since we have a tile for each empty "room" and for each wall in between. We then iterate through the key and add walls to the new array. In a final pass, the walls are converted to properly line up with adjacent walls. For example, a wall tile with walls on the right and left of it should be a flat horizontal wall: (), but one with walls on each side should be a  shape. I decided to encode the wall sprites in a similar manner to the WallStates, so each wall tile's code is just a bit array of it's adjacent walls. If there is a wall above, we add 1 (0001) to it, and if there is one to the right, we add 2 (0010), and so on. Not sure if there is any huge efficiency gain for doing it this way, but I sure find it neat. Lastly, I chose a random cell in the bottom right quadrant of the maze to contain the exit. One could have an exit on any of the outer walls, but I decided it should be somewhere on the inside.

A 4x4 grid of tiles, each one a different shape of wall or corner

The enlarged spritesheet for the wall tiles. From left to right and top to bottom, each tile is indexed 0-15 (0000 - 1111) Can you see the pattern?

I use this array to create a Phaser Tilemap layer. With the tilemap, I can add random enemies, make random walls bulletproof using simplex noise, recolor the walls, and ultimately create physics sprites for each wall tile. In the end I remove the Tilemap itself, as it is not needed once the level is fully generated.

How to make one Scene for (almost) all Levels

In Runner's grave, there are two kinds of levels: Procedurally-generated maze levels and pre-made tilemaps made in Tiled. The hub menu level, weapon tutorials, and final level, are all the latter. Previously, I had one Phaser Scene class that could run the generated maze levels, and and had to make distinct level Scene classes for each premade level. The data for the scene (like maze size, weapon loadout, enemies, color, etc) was loaded from a JSON file, which has the data for each level in a list.

In the latest update, I wanted to make a Scene that could build both the procedurally-generated levels, and new levels made in Tiled. So, a large amount of the time spent on this update was refactoring the main scene code to make this possible. To make it happen, I extracted the code to build level objects from (walls, entities, etc) from the maze generation module to include in the Level scene. This method, buildObjectsFromLayer, became the backbone of this scene. 

The key to it was to build the procedurally generated tilemaps the same way as the Tiled ones, with one layer each for bulletproof walls, hollow walls, and entities. Then, I could just call buildObjectsFromLayer with each one, for both kinds of levels. In the end, I still had to perform a check to see which kind of level was being made, not only to know if I needed to generate or load a tilemap, but also because I had to create blank layers before loading the tiles on them to get the generated maps working quite the same. But I still was able to reduce a large amount of code that was otherwise duplicated and inconsistent, which feels like a huge victory even though I only made one pre-made level this way. In preparation for that scene (the pistol tutorial), I also added code that supported attack-activated switches capable of opening doors. With the new Level scene, I could add as many as I wanted simply by adding the right point objects in tiled, but I only ended up adding one anyway. Maybe I'll do more someday once I learn how to actually design levels.

On Art

looping gif of a pixel art black goopy creature with jagged teeth shrinking and growing

spritesheet showing each frame of the goopy black creature's animation

This creature is internally called the "gloopie".


I know this is supposed to be a tech discussion post, but I have some quick thoughts about the art (and sound) of the game. 

All of the art I made myself in Aseprite. The inspiration behind my original vision was a combination of the Atari-era horror of FAITH, and the unintentional terror of the Ski Free yeti. (I didn't end up adding a monster that chases you around the maze, though :-( ). I also chose this style and kind of low-color, low-resolution pixel art, to be honest, because I thought it would be easy. And to continue being honest, I think it mostly was. Don't get me wrong, I am fully aware that pixel art is a real style for skilled artists, and that anyone with even a touch more experience could undoubtedly make something much better looking that what I have made in probably half the resolution. But as someone who has little artistic skill, I did not want to put so much time into learning to make visual art that lives up to my imagination and standards. And using free assets made by others, while viable, feels very restrictive when it comes to making something with a unified style. So, going with this style was a way for me to make art that seemed coherent and original to me without feeling embarrassingly bad. I feel that this feeling is common among not just solo devs who don't know art, but anyone who has great respect and passion for a medium, without the skill level to create with pride. This gap between taste and skill level; I'm not sure where I first heard about it, but Google suggests it originates from a quote from Ira Glass. I think this gap, and the associated fear of making things that stink, has held me back and continues to hold me back from many things. The inner critic can be the worst of all, I find. Most harsh, least forgiving, and typically not very constructive. But it is also true that the best way to improve is often to create, receive critique, and learn from it. That is what I hope to do with this game, and hopefully with future projects as well.

That being said, it was a lot of fun making the sprites. Will I be devoting much time going forward to improve my art skills? Probably not. But who knows?

For the sound effects, I used a combination of free sounds I bitcrushed with Krush in Audacity, and 8-bit effects made with Bfxr. All very enjoyable and satisfying to use.

The music was generously crafted by my talented friend Macrae. He provided me with three tracks that build on each other, and I used in-engine tools to dynamically slow down or speed up the tracks to give the game a touch of suspense.


Thank you for reading this post! If you liked this post, or the game, or both, please tell me! I would love to hear your thoughts.

Leave a comment

Log in with itch.io to leave a comment.