This post will mostly be a retread/revisiting of a talk I gave to the Albany IGDA chapter earlier in the year. There's some new stuff, and I've chosen to leave out some other stuff.
Pico-8 is what's referred to as a fantasy console: that is, it evokes the feelings of working with an old, 8-bit console (specifically something between an NES and Gameboy) without all of the pain of actually working with that hardware. I ended up with a copy of it because I bought the Voxatron Humble Bundle way back and at the time, buying Voxatron got you Pico-8 for free (this is still true at the time of writing this blog post).
Pico-8 is an interesting piece of software for a couple reasons: it has a lot of functionality in a very small package, it supports a very tight iteration loop, and it has limitations that encourage the user to think creatively and only about the problem at hand rather than getting hung up on abstractions and frameworks. In a lot of ways, Pico-8 is a modern day equivalent to BASIC. The surprisingly rich set of features it has, combined with ease of learning and use make it a very attractive tool to prototype and game jam in.
The set of functionality offered by Pico-8 is everything necessary to write tiny games with up to two local players. It has scripting in a custom subset of Lua, a sprite editor that allows for 8x8 pixel sprites in 16 colors, a map editor, SFX editor, and music tracker. It offers integrated editing facilities for all of these as part of the executable. Finished games can be saved as source files, cart images (these encode the cart data in the alpha channel), or embeddable web players.
I won't go into depth here about the software itself, but instead here's pictures of each of the tools it has:
Dungeon was the first game I made in Pico-8. It's a dungeon-crawler, but specifically a roguelike. Its major features include: procedural map generation, randomized items, two different types of enemies, status effects, combat narration, and of course gold. Additionally, while it is turn-based, you don't have forever to take your turn; a little clock will count down and the next turn will trigger when it hits zero.
My initial goal with Dungeon was to implement procedural map generation. There's nearly forty years worth of research and examples of procedural dungeon generation, dating back to the original Rogue and extending forward to games like Nethack, Diablo, and Torchlight. Of course, I wanted to reinvent everything on my own, make mistakes, and learn along the way. I had some specific requirements for how I wanted the dungeons to look: large, rectangular rooms, directly connected together by doors. Contrast this to how Rogue and Nethack's levels look: rectangular rooms connected by thin, branching passageways.
The algorithm I came up with ended up looking something like the following:
- Generate a room with 1-4 doors at random positions. Add the doors to a queue.
- Randomly decide a target room count.
- While there are doors remaining in the queue, we haven't hit the room budget, and it's possible to generate new rooms:
- Pop the first door off and attempt to generate a room of a minimum size at that door.
- Adjust the room size down until it doesn't overlap any existing room.
- If the room still has area left, then draw it and the door and generate up to four new doors.
Overall, this is a pretty solid algorithm and it will create a dungeon that is a nice, globular cluster of rooms centered around the starting room. In practice, there's a bit of subtlety involved, especially with determining overlap of rooms. My implementation is pretty buggy and I haven't gone back to fix the issues in it. When it generates a nice dungeon, it looks pretty fantastic, but when it fails, it fails spectacularly.
The final map is stored in a Lua table, exploiting the secret, hand-wavey 1MB of memory that the interpreter has available to it. This allows for a dungeon that is 128x128 tiles instead of the much smaller 128x32 available in the map. I could use up to 128x64 of the map, but my preference is for more enemies, objects, and tilesets, so I wanted to leave the second two sprite banks available.
The sparse objects (enemies and stuff the player can interact with), are stored in a Lua list. All of them are lightly object-oriented: they each have proc, update, and on-death functions along with some visual information. Most of the logic involved resides outside of their storage though. The proc, update and on-death functions make all of the business logic in the game uniform (quite possibly the best strength of the system). Proc handles additional on-hit effects, like poison for slimes and bleeding for bats. Update handles AI and per-frame logic. On-death is used for things that happen when an enemy or object is destroyed: potions are objects with 1 HP that apply their effects in their on-death function. The player could also be run using the same logic, and is actually using this system, but I foolishly went and hardcoded special cases for it everywhere.
I'm not sure when, but out of the three prototypes I talk about in this blog post, Dungeon is the one that I'd most like to revisit and fix the bugs in, along with refactoring it and expanding out the gameplay.
Pico Wars is a game inspired by the Advance Wars series of games. It's a tiny, turn-based war game where the objective is to capture the enemy headquarters. In Pico Wars, the player has access to three different units: the infantry, which captures buildings, the tank, a close-range unit, and the artillery, a long-range unit. There are three different types of buildings: the headquarters, which the player must defend, factories, which can produce units, and cities, which produce income every turn. There are also a variety of different terrain tiles that all have different movement costs for each unit, and some of which are impassable.
My goal with Pico Wars was to focus on polish and trying to make a complete game. In the course of trying to do that, I managed to have at least one interesting problem to solve: pathfinding.
My original intuition for pathfinding was to go with something like Manhattan distance since the game world is a grid. This would also be extremely simple and cheap to calculate because it is just the addition of the x and y coordinates of the different of the start and end positions. However, once I started thinking about how to integrate that with the different movement costs that the map tiles have, everything fell apart. At the very least, I'd have to take into account the cost along the path, but the direct route isn't necessarily going to be the cheapest.
The real answer to this problem was that I'd need to implement a traditional breadth-first-search (or a floodfill if that's your thing). The goal here is to generate a map of distances from the unit that wants to move to every grid cell. Once we've got this distance map, we can find paths within it.
The general algorithm for getting this map is as follows:
- Push the starting location onto a queue.
- Initialize the starting distance to 0.
- While the queue has locations in it:
- For each of the four cardinal directions:
- Compute the distance
- If tile hasn't been visited or the distance is less, assign it and push the tile
- For each of the four cardinal directions:
The end result of this is a map with distances for every single tile. It could be optimized by doing an early-out once the distance surpassed the maximum movement distance of the unit, but the map is so small that it doesn't really make a difference. Having this distance map allows us to now calculate every possible path the unit could take. I also use this to shade the tiles that are potential destinations for the unit. You just need to check if the distance is less than or equal to the unit's movement amount and shade the tile. When we finally want to get a path, it's as simple as starting at the destination and then following the decreasing distances back to the unit itself.
Possible Movement Locations
A somewhat minor but very effective tactic I used in Pico Wars to reduce the amount of code I was writing was to use closures. These are a very powerful feature of Lua (and many other languages) where you can make a local function that captures variables from the surrounding scope. I used these in Pico Wars similar to how one might use macros in C or C++, to reduce the amount of copy-pasting of code I was doing. One particularly fruitful use of them was in the pathfinding code to turn 16 long lines of code into a closure and four much shorter lines of code.
H-Blank is a silly prototype of doing Game Boy-style rendering in Pico-8. It was inspired by The Ultimate Game Boy Talk at 33c3, which is a very good introduction to the Game Boy and definitely worth watching.
The general gist of what this prototype does, is implement a framework that gives the user access to an h-blank callback that they can set and a bunch of what are called OAMs (basically sprites). The renderer loops through every vertical line on the screen and checks to see if the callback should be called (the user sets which line to call it on).
Inside of the callback, the user can move the background around and set the callback line. Unlike the actual Game Boy, they can also move OAMs around, which can lead to even more interesting effects. I implemented a very simple background scroller effect like you'd see in old demoscene demos and that's the extent of how much I toyed around with this. Rendering in this way eats up a ton of the virtual CPU that Pico-8 has (around half of it). A future version of this would probably want to be implemented using peek() and poke() to directly set memory, as that would be significantly faster. As it stands, I'm probably not going to touch this again for a long time/ever, so the code is up on itch.io with the demo.
If you want to play these and check out where I ended up, you can use the widgets below to get to web versions of them.