Making a GB Game, Part 2: Drawing Sprites
Published 2018-07-25
Русский перевод: 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 second part I'll talk about how I drew the sprites for the game and got them on the screen. Here is a link to the previous part.
Fun with Graphing Paper
In order to render things to the screen, I naturally needed some sprites to render. Looking at the Game Boy PPU, I had my choice of 8x8 or 8x16 sprites. I'd probably want the latter, but just to get a sense of scale I drew a quick game screenshot in 1:8 scale on some graph paper.
I wanted to dedicate the top to the HUD. It felt more natural than having it on bottom, because with it on top, if the characters needed to temporarily overlap the HUD ala Super Mario Bros, they could. This game doesn't have any complex platforming or, really, any level design at all, so I don't need to have a really zoomed out view of the field. The position of the characters on the screen and maybe the occasional obstacle is all that really mattered. So, I could afford to have (and it would really wasteful to not have) fairly large sprites.
So, if each square was one 8x8 tile, one sprite would not be sufficient no mater what size I chose. This is especially true given the fact that there is little vertical movement besides jumping over things. So I decided to just build my sprites out of 4 8x16 sprites, with the exception of the fox's tail, which would take 2 8x16 sprites. Doing some back-of-the-envelope math, 2 foxes and 2 geckos on screen makes me use 20 out of my 40 sprites, leaving me plenty of room for additional sprites. (8x8 would put me at the limit, which I don't want to do so early in development.)
At that point, there was nothing do it but to draw the sprites. Here's the raw graph-paper mockups. There was an idle sprite, a "thinking" sprite for deciding whether to pass or run ala the SNES game... and that was it. The plan was also to have running sprites, jumping sprites, and sprites for getting tackled. I only drew the idle and thinking sprites to begin with, just to keep it simple. Still haven't drawn the other ones. I should do that.
Yes, I know I'm not too great at drawing. Perspective is hard. (And the arctic fox's snout, ouch.) But I think this is serviceable. The character designs are kind of generic, really, but for the original purpose of a game jam it worked just fine. I did, of course, reference of actual geckos and snow foxes. Like, see?
Basically the same. (For the record, I literally just realized while looking up those pictures again that geckos and lizards are wildly different things. Not sure what to do about that besides feel kind of dumb...) I guess you can say that the arctic fox character's head is slightly inspired by Blaze the Cat from the Sonic series. Can't say that the gecko has anything like that, apart from a healthy dose of consummate v's.
I originally wanted to have the offense be a different gender than the defense for each team, so it would be easier to tell them apart. (I was going to also be nice and let the player pick which gender they wanted to be, because as much as I'm okay with anthro stereotypes, gender stereotypes are dumb.) However, that would have taken too much time to draw. Instead I opted for male geckos and female foxes.
Lastly, I went and drew a title screen because I had the room for it on the sheet of graph paper I was using.
I, yeah, action poses aren't anything I'm good at. The snow fox is supposed to be mortified and also running, and the gecko is supposed to look, ya know, threatening. The defense snow fox in the background is a fun callout to the Doom boxart, though.
Digitizing Sprites
Now was the part where I turned the on-paper drawings into sprites. I used a program called GraphicsGale to do this, which was recently made free. (I know asesprite was also an option, but I liked GraphicsGale better.) Doing the spritework was a bit more challenging than I had anticipated. Each of those squares in the sprites above makes up 4 pixels, in a 2x2 square. There was often a LOT more than 4 pixels of detail in those pixels. I had to throw out a lot of detail that the graph paper had, as a result. Adhering to the basic shape, even, was a bit difficult because I needed to give some more room to have, say, eyes, or a nose. But I think it still turned out okay, if radically different.
The fox's eyes lost it's more almond shape in favor of just a 2 pixel tall line. The gecko's eyes kept some roundness, though. The gecko's head had to grow, getting rid of the whole broad shoulders thing, while any curves the snow fox may have had were severely toned down. Honestly, though, these subtle changes aren't that bad. I have a hard time deciding which variation I like more.
GraphicsGale had a neat layering and animation feature, as well. This meant that I could animate the snow fox's tail separately from the body. This was great for saving precious VRAM space, as I wouldn't need to duplicate the tail across every frame. Additionally, it meant that I can wave the tail back and forth at a variable speed, going slower when standing and faster when running. However, this makes the programming a bit more difficult. I'm up to the challenge, though. I decided on 4 frames, as that seemed like a decent number without being absurd.
You'll notice that the snow fox is using the lightest 3 shades of grey, while the gecko is using the darkest 3. This is totally allowed on the GameBoy, because even though it only allows 3 colors per sprite, it lets you set two palettes. I set the snow fox to use palette 0 and the gecko to use palette 1. This does exhaust all of my palettes, but it's not like I really need any more.
I also needed to take care of the background. I didn't bother to mock these up, as I planned on having them be solid colors or simple geometric patterns. In case you were wondering, I didn't digitize the title screen yet, as I didn't need it at the time.
Loading the sprites into the game
Follow along with commit be99d97.
With every individual frame of character graphics saved as a PNG, I was ready to convert them for use on the GameBoy. As it turns out, RGBDS comes with a really nice utility for doing this, name RGBGFX. You call it like rgbgfx -h -o output.bin input.png and it gives you a nice GameBoy-compatible tile set. (The -h is to set the tile mode to be 8x16 compatible, going from top to bottom instead of left to right.) It does not, however, provide mappings. And I couldn't catch duplicate tiles with every frame being a different picture. But that is a problem for a later date.
With the output .bin files generated, all I had to do was include them with `incbin "output.bin"` in the assembler. To keep everything together, I kept a dedicated "gfxinclude.z80" file to hold all the graphics includes.
That being said, it was getting really tedious to manually regenerate all the graphics when I changed something. So I edited my "build.bat" file to include the line for %%f in (gfx/*.png) do rgbds\rgbgfx -h -o gfx/bin/%%f.bin gfx/%%f, which converts every .png file in my gfx/ directory to a bin and saves it in gfx/bin. This made life a lot easier for me.
For the background graphics, I took a much, much lazier approach. RGBASM comes with a dw ` directive. This, followed by a row of 8 vales from 0 to 4, equals one line of pixel data. Because the background sprites were so simplistic, it was easy just to copy and paste a simple geometric pattern over and over to make a solid, striped, or checker-boarded pattern. For instance, here's the dirt tile.
bg\dirt:
dw \`00110011
dw \`00000000
dw \`01100110
dw \`00000000
dw \`11001100
dw \`00000000
dw \`10011001
dw \`00000000
This creates a series of staggered dashes that appear to have perspective. It's simple, but it's clever. The grass was a bit wackier, though. It started as a bunch of 2 pixel tall horizontal lines, but I manually threw some pixels around to give it a more noisy quality that I feel works a lot better. It looks like this:
bg\grass:
dw \`12121112
dw \`12121212
dw \`22112211
dw \`11121212
dw \`22112211
dw \`21212121
dw \`12121212
dw \`12211222
Rendering the graphics
Sprites in the GameBoy's memory are stored in a section called OAM, or Object Attribute Memory. This contains only attributes (direction, palette, and priority), position, and the tile number. All I had to do was populate this section of memory and I'd have sprites on the screen.
Well, a few caveats in that. First off, I still had to load the graphics from ROM to VRAM. The GameBoy can only render tiles that are stored in a special section of memory known as VRAM. This, thankfully, was as simple as doing a memcpy from ROM to VRAM during the program init phase. Doing so revealed that I was already using up a quarter of the sprite section of VRAM with only 6 character sprites and 4 tail sprites. (VRAM is typically split up into a background and a sprite section, with a 128 byte overlap between the two.)
Additionally, the OAM could only be accessed during VBlank. I started off just waiting for VBlank before doing the sprite calculations, but I ran into problems as the sprite calculations spilled over the allotted VBlank time and were unable to be finished. The solution here was to write to a different section of memory outside of VBlank and just copy that into OAM during VBlank.
As it turns out, the GameBoy has a special hardware copy routine (a form of DMA, or Direct Memory Access) that did exactly that. By writing into a specific register and jumping to a busy loop in HiRAM (because during DMA the ROM is inaccessible) I could copy things from RAM to OAM much quicker than I could using my memcpy function. The juicy details are here if you care.
At this point, all that was left was the routine that determined what would eventually be written to DMA. To do this, I needed to keep, somewhere else, the state of the objects. Specifically, at a minimum,
- Type (gecko, snow fox, or either team's carried item?)
- Direction
- X position
- Y position
- Animation frame
- Animation timer
The first, sloppy solution I went with was to test the object type, and depending on that jump to a routine that drew that specific type of object sprite by sprite. The snow fox, for instance, would take the X position, add or subtract 16 depending on direction, add the two tail sprites, and then move up and down across the main sprite.
Here's a screenshot of what the sprite looked like in VRAM when drawn on screen. The left half are the individual sprites, where the hex numbers next to it are the vertical pos, horizontal pos, tile, and attribute flags from top to bottom. On the right you can see what it looked like assembled.
The animation on the tail sprite was a bit tricker. On my first attempt, I just incremented the animation timer every frame, and and'd that by %11 to get the frame number. From there I could just add 4 * the frame number to the first tail tile in VRAM (each animation frame is 4 tiles each) to get the 4 different frames as stored in VRAM. It worked (the tail tile lookup bit especially), but the tail flapped about insanely quickly. I needed a way to slow it down
My second, slightly more refined approach, was to increment a global timer every frame, and when that and whatever power of 2 I chose equaled 0, then increment the object timer. This way every individual object could tick it's animation timer at whatever speed it wanted. This worked just fine, and let me slow down the tail to reasonable levels.
Complications
If only it were that, easy, though. Remember, I'm manipulating all of this in code, using a different subroutine for every object, and if I were to have continued, every frame. I had to manually specify by manipulating registers how to move to the next sprite as well as what tile went in it.
This was not sustainable in the slightest. It took a considerable amount of register juggling and CPU time to draw one specific frame. Trying to add support for different frames would be nigh on impossible, and even if I did manage it, it would be a pain to maintain. Trust me, it was a mess. I needed something where the code for rendering sprites could be generic and dumb, instead of the rat's nest of conditionals, register juggling, and math operators it was now.
How did I fix it? Next time on InvisibleUp. Don't worry, I'll be a bit quicker this time, now that I don't have to worry about the scanner anymore. (Hah ha, lies.) Because I'm in the mood to scan everything now, I'll leave you with a quick and dirty drawing I drew of my OC playing a GameBoy. Why? Why not.