Making a Game Boy Game! (Part 1)
Русский перевод: https://habr.com/ru/post/436330/
Over the past few weeks I've decided to work on a Game Boy game that I'm having a bit of fun making. The working title of it is "Aqua and Ashes". It's open source, hosted at https://github.com/InvisibleUp/AquaAndAshes. In this first part I'll discuss how I came up with the idea, share some of my art, and go over how I managed to initialize the Game Boy hardware and draw a background to the screen.
Coming up with the idea
Recently I got an internship doing back-end PHP and Python stuff for a website for my university. It's a really nice job that I find fun and I'm thankful to have. But... at the same time, doing all this high-level web dev stuff has given me a big itch I need to scratch. That itch being the fun of low level bit fiddling.
itch.io's weekly game jam email came in my inbox and it announced the Mini Jam 4. It was a 48 hour (well, a bit more actually) jam where the restriction was to have graphics like a Game Boy. My perfectly logical and sound reaction to this was to make a Game Boy homebrew, because that seemed neat to do. The themes were "seasons" and "flames".
After a bit of musing about plot and mechanics that I could pull off in 48 hours that fit the theme restrictions, I came up with a
rip-off reinterpretation of a level from the 1993 SNES game Tiny Toon Adventures: Buster Busts Loose! where you, as Buster, play a round of American football.
I always sort of enjoyed how this stage took the incredibly complex sport of football, stripped out all the plays and positions and a lot of the strategic elements, and still ended up with a perfectly fun and easy to pick up game. Obviously this sort of simplified overview of football doesn't replace Madden in the same way that NBA Jam (same sort of idea; only 4 players on a much smaller court with much more streamlined gameplay than the real deal) doesn't replace the 2K series. But the idea has a certain charm to it, if NBA Jam's sales numbers are any indication.
What the heck does that have to do with anything? Well, my thought was to take that football level and build upon it until it was fresh albeit original. First, I stripped it down to only 4 players, one offense and one defense per team. This was due to hardware limitations mostly, but it also would allow me to play around with smarter AI than the "run left and randomly jump" of the SNES game.
To fit the theme, I change the goalposts to burning pillars or campfires or something (I haven't quite decided that yet) and the football to torches and buckets of water. This way the winner is the team that controls both fireplaces, which is an easy concept to write a simple plot around. The seasons came into play as I decided that the time of year would continually tick every turn, so that the pro-fire team got a buff during the summer and the anti-fire team got a buff during the winter. This buff would come in the form of on-field obstacles like snow piles that only effect other team.
Of course, I then had to make the two teams each some kind of animal that does and does not like fire. My first thought was fire ants and some sort of water bug or praying mantis or something, but doing some research I couldn't find any bugs that were active during the winter, so I switched it over to arctic foxes and geckos instead. Arctic foxes like snow, geckos like laying in the sun, it makes sense probably. It's a Game Boy game, it's fine.
Also, in case it wasn't obvious, I didn't get anywhere near done before the jam was over. Meh. I'm still having fun.
Getting the Game Boy up and running
First off, I had to decide on requirements. I decided to stick with just the DMG mostly to keep with that game jam's requirements, but also because I wanted to. I personally have never owned a DMG game (but I have a few Game Boy Color games, weirdly enough) but I find the 2-bit aesthetic kinda nice and a fun limitation to play with. I might add optional color for the SGB and the CGB, but I'm not ready to consider it yet.
I've also decided to stick with a 32K ROM + no RAM cart just in case I ever want to make physical copies of it. CatSkull, who published a few Game Boy games such as Sheep it Up!, has a really cheap 32K flash cart for sale that would be perfect for me. This is a bit of an additional challenge, but I don't see myself exceeding 32K with a game this simple any time soon. The graphics would be the trickiest part, really, and if worst comes to worst I can try compressing the graphics.
As for actually making the Game Boy do things, that was mildly difficult. Honestly though, as far as retro game consoles go the Game Boy is the nicest I've ever worked with. I started out with an excellent tutorial (well, an excellent start to one, as it was never finished) by "AssemblyDigest". I knew that using ASM was the best way to go, as painful as it can be sometimes, because the Game Boy hardware is not meant for C and I did not have any confidence in that hotshot "Wiz" language mentioned being usable in the long term. Plus, well, I'm doing this mostly so that I can work in ASM.
Follow along with commit 8c0a4ea
The first order of business was to get the Game Boy to boot. Unless the Nintendo logo is present at the offset $104 and the rest of the header is set up correctly, the Game Boy hardware will assume the game cart isn't inserted properly and refuse to boot. Doing this was easy enough, as basically everyone on the planet had a tutorial for it. Here's my take on the header. Nothing super noteworthy here.
The harder part is doing anything meaningful after it boots. An easy thing to do is to have the system throw itself into an infinite busy-loop, where it executes the same line of code over and over. Code execution starts at the main label (as the jump at $100 in the header points to), so at there I need to place some simple code like
main: .loop: halt jr .loop
which would do literally nothing besides wait for an interrupt happens to be fired and then go back to the .loop label. (From this point forward I'm going to skim over exactly how the ASM works. If you get lost refer to the docs for the assembler I'm using.) If you're wondering why I'm not just jumping straight back to the main label, that's because I want to have everything before the .loop label be program initialization, and everything afterwards what happens every frame. That way I'm not forced to loop over loading data from the cart and clearing memory every single frame.
Let's step it up a little bit. The assembler suite I'm using, RGBDS, has an image converter. Because at this point I hadn't drawn any assets for the game yet, I decided to use the monochromatic button from my About page as a test bitmap. I converted that to the Game Boy format using RGBGFX and used the .incbin assembler command to include it after my main function.
To display this on the screen, I needed to do the following:
- Turn off the LCD display
- Set the palette
- Set the scroll position
- Clear the VRAM
- Load the tile graphics into VRAM
- Load the background tilemap into VRAM
- Turn the LCD back on
Turning off the LCD
This was the biggest roadblock to getting started. On the orginal Game Boy, you can't just write data to VRAM (video RAM) whenever. You have to wait until the system isn't drawing anything. Emulating the phosphor beam of an old tube TV, this period between every frame the VRAM is open is known as Vertical-Blank, or VBlank. (There also exists HBlank, between every line on the display, but it's really short.) However, we can sideskirt this by turning off the LCD display, which means that we can write to the VRAM no matter where the "phosphor beam" of the LCD is.
If you're lost, this overview should explain a lot. It's from an SNES perspective, so keep in mind that there is no electron beam and that the numbers are off, but otherwise the vast majority applies. We're effectively attempting to set the "FBlank" flag.
The catch about the Game Boy, though, is that you can only turn off the LCD during VBlank. This means we have to wait for VBlank. To do this, we need to use interrupts. Interrupts are signals the Game Boy hardware sends to the CPU. If an interrupt handler is defined, the CPU stops what it was doing and calls the handler. There are 5 interrupts the Game Boy supports, and one of them fires when VBlank starts.
There are two ways to handle interrupts. The first, and most common, way is to set up an interrupt handler, which works as I've described above. However, we can enable a certain interrupt and disable all the handlers by setting the interrupt enable flag for that interrupt and using the di opcode. This usually doesn't do anything, but it lets us break out of a HALT opcode, which stops the CPU until an interrupt occurs. (This would occur if handlers were enabled as well, which allows us to break out of the HALT loop in main.) In case you were wondering, we will eventually make a VBlank handler, but the stuff in there will depend on certain memory addresses having certain values. As we don't yet have anything in RAM set up, attempting to call the VBlank handler may crash the system.
To do this, we have to send commands to the Game Boy's hardware registers. These are special memory addresses that are directly connected to various bits of hardware, in this case the CPU, and lets you alter how it operates. We're specifically interested in addresses $FFFF (interrupt enable bitfield), $FF0F (activated but unserviced interrupt bitfield), and $FF40 (LCD control). You can find a listing of these registers in the pages linked in the "Documentation" section on the Awesome Game Boy Development list.
To turn the LCD off, we enable only the VBlank interrupt by setting $FFFF to $01, HALT until $FF0F == $01, and then set bit 7 of $FF40 to 0.
Setting palette and scroll position
This is easy. Now that the LCD is off, we don't have to worry about VBlank anymore. Setting the scroll position is as easy as setting the respective X and Y registers to 0. The palette is marginally trickier. With the Game Boy, you can set the 1st through 4th shades of your graphics to any of the 4 shades of grey (or pea-soup green, if that's more your jam), which is useful for transitions and stuff. I set it to a simple gradient, defined as the bit list %11100100.
Clearing VRAM and loading tile graphics
On startup, the graphics data and the background map will be that of the scrolling Nintendo logo you see when the system boots up. The sprites, when I turn them on (they default to being turned off) will be garbage strewn across the screen. I want to clear the video RAM so I have a clean slate.
To do this, I needed a function like memset in C. (I'm also going to need memcpy as well to copy the graphics data over.) memset sets a given chunk of memory so that is all equals a certain byte. This would be easy to implement myself, but the AssemblyDigest tutorial had these functions already so I just used those.
At this point I could clear the VRAM using memset with $00 (although this first commit used $FF, which works just as well) and then load the tile graphics into VRAM using memcpy. I specifically need to copy into to address $9000, as those are the tiles used solely by the background graphics. ($8000-$87FF are sprite-only tiles and $8800-$8FFF are shared by both.)
Setting the tile map
The Game Boy has a single background layer, split into 8x8 tiles. The background layer itself takes up about 32x32 tiles for a total size of 256x256. (The screen, for comparison, is 160x144.) I needed to manually specify the tiles that made up my image, row by row. Thankfully, all the tiles were in order, so I just needed to fill each row with N*11 through N*11 + 10 where N is the row number, filling the other 22 tile entries with $FF.
Turning the LCD back on
You don't need to wait for VBlank here, as the screen won't turn back on until VBlank anyways, so I just wrote to the LCD control register again. I also made sure to turn on the background and sprite layers and set the tilemap and tile graphics locations to the right spot. With that, here's the results. I also re-enabled interrupt handlers using the ei opcode as well.
At this point, for a little extra fun, I wrote a very simple interrupt handler for VBlank. By adding a jump opcode at address $40, I can set the handler to be any function I want. In this case, I just made a simple function that scrolls the screen up and to the left.
Here's the final results. [Edit: Just realized that the GIF loops improperly. It's supposed to wrap around all the way.]
Nothing exciting yet, but it's still neat that I can, in theory, get out my old Game Boy Color and see my own code running on it.
Next time I'll talk some about art. That'll have to wait until I have access to my scanner, though, so I'm not sure when that's coming.