gecko's site

Things I've learned since 'Goodboy Advance'

Goodboy Advance was the original jam version of Goodboy Galaxy, which Rik and I hacked together for Ludum Dare one weekend in December 2018.

It's not that good really... well it was kinda impressive at the time, but it's really missing enemies, characters, and story. If you're looking for a cool GBA homebrew game about a dog in space, you should definitely try Goodboy Galaxy: Chapter Zero instead.

However, if you'd like to know what we used to make the original jam game, and all the things we've learned since that allowed us to go on to make Goodboy Galaxy, then this is the page for you!

How we made it

The game was written in C using the devkitARM1 toolchain, with libraries provided by this excellent free book called Tonc which can teach you almost everything you need to know about GBA programming.

We used Maxmod for music/sfx playback. Music was composed in MilkyTracker and MadTracker. i.e. we have different weapons of choice, but it doesn't matter because both support the XM format which Maxmod can use.

Aseprite was used for graphics/animations (having to be mindful of size and palette restrictions etc.). To convert the graphics into the raw formats which can be used by the hardware, we used Grit which is powerful but quite difficult to work with because there are so many options and sometimes it's hard to tell which ones do what you want2.

And finally, we made the maps in Tiled, but it seemed like parsing XML or JSON in C on such limited hardware would be a terrible experience. So instead we made a Node.js script which goes over the Tiled data and generates C code for a given level. That may sound like a big hack but it was really a life saver!

Learnings

The source code for the jam game can be found on GitHub. This code was born from the ashes of a Super Crate Box clone that I was working on many years ago.

Here are some things it does poorly, which we've fixed in Goodboy Galaxy and/or you may want to do differently if you're aiming to make something bigger than a jam game:

  • All declarations are in 1 header file. This was convenient for the jam but wouldn't cut it for a larger project. (Actually for Galaxy we rewrote everything in Nim which has a decent module system instead of header files)

  • Misuse of VBlank. No separation between update & draw means that sometimes the game will perform visual changes (scrolling the background, fading the palette, modifying sprites) while the PPU is in the middle of updating the display, leading to tearing artifacts.

    • Writing a decent GBA main loop is a topic that deserves its own article, but at the very least: "Game logic" -> "Wait for VBlank" -> "Visual updates" would be a much better structure than what I did here ("Wait for VBlank" -> "Do everything").
  • Tile/map/palette locations are hardcoded or allocated by simple counters. Instead, you might want to use allocators such as the ones presented in Gameboy Advance Resource Management

  • Statically allocated arrays for each type of entity. Consequently there's a maximum of 20 breakable blocks in a level, 8 bullets alive at a time, etc. This is fine for smaller games (keep it simple!) but doesn't scale well: if the current level doesn't have any breakables, that array of 20 breakables is still taking up RAM for no good reason.

    • Consider putting all entities in 1 array (an object pool) and using some mechanism such as dispatch tables to give different behaviours to different entities.

    • On modern hardware, dynamic dispatch can be considered a thing to avoid in the interest of cache-friendliness, but on the GBA it turns out that chasing pointers isn't particularly slow compared to the speed of the processor itself.

    • More generally, if you're running out of memory you can try moving global variables from IWRAM into EWRAM which is slower to access but more plentiful.
      e.g. EWRAM_DATA u16 myarray[100];
    • It's also OK to do dynamic allocation via malloc, but try to only use it for data that sticks around for a while but isn't needed permanently, e.g. the state of a boss fight on a particular level. The only trouble is, the default malloc implementation from newlib is pretty wasteful (>1KiB static IWRAM usage), so you might want to consider replacing it with a version that's tailored to the GBA, such as the one from ACSL.

  • The wrapping level mechanic introduces some nasty bugs. When the player goes beyond the right side of the level, they are teleported to the left side. This can cause objects to vanish or appear out of nowhere. It can also lead to getting stuck in walls or ceiling depending on placement.

    For the jam, we designed the levels to minimise the problem. In Galaxy, we fixed it with more thorough checking and a proper spawn system (which ties into the '1 array of entities' mentioned above).

  • Streaming larger level maps (bigger than 2x2 screenblocks) - this code works but it's not that good. It uses 4 whole screenblocks which are copied every frame even if nothing changed. Probably need to be smarter about that...

    • The way to do this properly is to take advantage of the fact that a single screenblock (tile map region) on the GBA is 2 tiles wider than the size of the GBA screen (and plenty of tiles higher). This means as the camera scrolls you can copy in new rows & columns of map data, to give you an infinitely large map from just a single screenblock. The Tonc bigmap example demonstrates this. There's also an annotated version of the same code by ColonelSalt which is easier to read.
    • You may eventually run into the problem that there are too many unique tile graphics in your level. The maximum is 1024 (as tile IDs in the map are 10-bit). But you might run into problems before then, because 1024 tiles takes up a lot of VRAM! To solve this, you'll need a means of dynamically loading tiles as they scroll onto the screen, and unloading them as they scroll off. Once again, GBA Resource Management explains how to do this.
  • Too much DMA - the game occasionally crashes on screen transitions on real hardware. I suspect this might be due to me using DMA copies to load absolutely everything. As Tonc says, “don't wear it out”. Perhaps this would cause the game to miss interrupts or fire them too late (Maxmod in particular is picky about VBlank IRQ timing). I'd recommend using memcpy32 for most things instead. (If somebody knows more about this, I'd really like to know!)

  • Tooling: We have a script that converts Tiled level data into C code. Besides that, we used grit for all our image data, including the level maps themselves. In Galaxy we realised we need more control, so we ditched grit and ended up making a lot more scripts that generate all kinds of data from various config files. It takes time to get this stuff working, but it makes adding content to the game much quicker and less error-prone!

    tl;dr Don't be afraid to write code that spits out more code.

  • Polish: The game has some niceties such as muzzle flash / impact fx / screenshake, but the position-locked camera and the static background let things down a little. In Galaxy we use HBlank DMA to get some gorgeous multi-layer parallax from just a single background.

A gif showing gameplay footage of Goodboy Advance.
[1]
devkitARM can be installed from here but nowadays I'd recommend something like sdk-seven , gba-toolchain or gba-bootstrap
[2]
SuperFamiconv is often recommended as a more user-friendly and less buggy alternative to grit. I'd recommend trying that first.