What to make?
The first question I had was what to make for the competition? Because I'm focusing most of my effort on Halcyon these days, I wanted to think of a game that I could develop really quickly, but still be fun. Treating it like a short game jam, I wanted something that I could take from start to finish in a week's worth of evenings, but still be a reasonably fun game. Honestly, this was the hardest part of the process, which I've been mulling in my brain for months. I have a few ideas (some of which I still might save for next year), but all of them are things that either don't sound fun right now, or would take just a little more work than I'm prepared to invest.
But about 2 weeks ago, it suddenly dawned on me that a NES port of the Gravitron would be tons of fun to play (at least in my opinion), really easy to write, and light on assets (ie I wouldn't need to spend ages fussing with finding new graphics). The only real problem was IP -- what happens if Terry Cavanagh finds out and shuts down my effort? I debated whether to get permission ahead of time, or to wait and ask forgiveness. But decided that regardless, I'd start on the project and see what I could come up with.
Research
Step 1 is probably the easiest part. Play the game a bunch, and take some notes, to see if it would be as simple as I remembered. There's some Youtube videos out there of people playing the gravitron, which is helpful, and there's a free dedicated Gravitron-only app for mobile. So after spending 20 minutes looking at how the game worked, I was more convinced than ever that I could pull it off.
The most important part of this research bit was thinking through how to make the game match the NES restrictions. The important bits (the top and bottom borders and the timers at the top) could easily be made with background tiles, and the character and obstacles are obviously sprites. There's not too many colors, and not too many sprites on any given horizontal line, which are normally two of the major things that are hard to convert to the NES.
A screenshot of the Super Gravitron from my phone |
Getting Started: the easy stuff
Now the race has begun, and I wanted to see how quickly I could get this thing off the ground. Because the goal was speed more than cleanliness, I started by copying my entire git repository from Super Homebrew War, and then going through and doing mass amounts of deleting unnecessary code. Within 30 minutes or so, I had a good skeleton of a project, which would start-and-do-nothing without crashing.I loaded a screenshot of the original game into GIMP and extracted the player and obstacle graphics, and fairly quickly coded up the majority of the main character's behavior in C. Because the player does nothing but move left and right, and uncontrollably bounce, this was another simple process. I pulled in some existing bitmap fonts (stolen from another old NES game, which, as it turns out, is legal in the US, because bitmapped fonts aren't eligible for copyright!) Using NES Screen tool, I sketched out the general layout of the screen, and had the very basics of the game going without a hitch.
The easy parts are done! Now I have to start using my brain.... |
Next: Obstacles
I've been doing enough NES game development recently that this type of thing is fresh in my brain and easy enough to churn out. The concept of a handful of obstacles, which merely march across the screen with a simple animation, was really simple. The harder part is figuring out the spawn patterns of the enemies. Back to research mode!
I watched a few videos of the game, and made some notes. The enemies clearly come out in some pre-defined patterns, which seems to randomly selected. That's enough information for now.
But now I actually have to start using my brain. How do I want to store and process the spawn patterns? I decided on each pattern being a list of pairs of bytes, with each byte being a list that tells enemies spawning that frame, and a timer until the next frame:
pattern1:
.byte %10010000, 10 ;First bit is left or right side
.byte %10001000, 10 ;bits 2-6 are the Y position of which
.byte %10000100, 10 ;enemy to spawn. So this will spawn
.byte %10000010, 40 ;4 enemies in a diagonal line, 10 frames
.byte $ff ;apart. The $ff marks end of the pattern
This is the first part of the process that took any real debugging and work, making sure my bit shifts and comparisons were correct for each byte in the pattern array. But with that in place, I had enemies spawning. Using the famous fast rectangle collision routine from AtariAge user Supercat, collisions against the player were simple and fast. Although I only had a couple of test patterns, and the timing and speed of everything was way off, and hitboxes needed to be adjusted, I had the bones of the game in place, after 4 or 5 hours.
Fans of the real game will notice that I have the sprite falling backward (head-first). I didn't notice until significantly later in the process. |
Backgrounds
Now things start to get interesting. It's time to see what I can do about the background. First is the timers at the top of the screen. It's easy to put the "current time" and "best time" text as part of the background graphics, as well as the times. But it was also time to think about the animated backdrop. I did a lot of thinking about this during the rest of development. The big issue is that the NES can only change a handful of background tiles per frame, because you (generally) can only write to the background safely during vblank. There's not enough to time to rewrite each background tile. And really, using basic 8x8 pixel background tiles is messy for doing a big animated backdrop like the one in the gravitron.
Eventually I decided that I needed to take advantage of the CHR-RAM and redraw the actual tiles every frame.
For those non-NESdev folks reading this, the NES handles graphics by taking pre-made 8x8 tiles, and arranging them on the screen. A lot of games use CHR-ROM, which means the actual tile graphics are on a ROM chip in the cartridge, and can't be changed. Others use a RAM chip for the graphics, and copy the graphics from the game's main program ROM to the video ram at run time. There are advantages and disadvantages of both methods, but by using CHR-RAM, I can, on every single frame, redraw the handful of 8x8 tiles that make up the backdrop.
I sketched it out and determined that I'd need to redraw 16 different backdrop tiles in order to animate this properly. That's 256 bytes of data that would need to be written during vblank, (about 1800 free cycles after OAM DMA finishes) That's doable, but would definitely require me to optimize things. First, I decided that the millisecond part of the timers (At the top) could be sprites, so I wouldn't have to update them as background elements, saving a little time. (I could cheat for the seconds and minutes, and just skip updating the backdrop when I update them, which wouldn't be noticeable at one missing frame every second)
Then, I needed to optimize and unroll all the background tile updated during vblank. A naive way of re-drawing a tile during vblank might use a loop like this:
;first tell the NES what video ram address you
;want to write to.
lda #TILE_VIDEO_RAM_ADDRESS_HI_BYTE ;2
sta PPUADDR ;4
lda # TILE_VIDEO_RAM_ADDRESS_LO_BYTE ;2
sta PPUADDR ;4
;then do a loop to write from a ram buffer to the tile
ldx #16 ;2
top:
lda buffer,x ;4
sta PPUDATA ;4
dex ;2
bpl top ;3
That's 13 cycles per byte, plus 14 cycles of setup (minus 1 cycle for the last bpl). If I did my math right, that's 221 cycles per tile, or over 3500 cycles. About double what I have available. But hardcoding addresses and unrolling things saves a ton of time:
lda #TILE_VIDEO_RAM_ADDRESS_HI_BYTE ;2
sta PPUADDR ;4
lda # TILE_VIDEO_RAM_ADDRESS_LO_BYTE ;2
sta PPUADDR ;4
lda buffer+0 ;4
sta PPUDATA ;4
lda buffer+1 ;4
sta PPUDATA ;4
lda buffer+2 ;4
sta PPUDATA
;etc, 13 more times
Using this method, it's 140 cycles per tile. That still comes to 2240, which is too much, but we're getting there. Luckily, due to the pattern of how the backdrop updates, a few of our tiles are the same thing repeated vertically, so we can optmimize them further:
lda #TILE_VIDEO_RAM_ADDRESS_HI_BYTE ;2
sta PPUADDR ;4
lda # TILE_VIDEO_RAM_ADDRESS_LO_BYTE ;2
sta PPUADDR ;4
lda buffer ;4
sta PPUDATA ;4
sta PPUDATA ;4
sta PPUDATA ;4
sta PPUDATA ;4
sta PPUDATA ;4
;etc, 11 more times
That's only 56 cycles! It's enough to balance things out, and let us fit into the limited time during vblank.
To verify, I used Mesen's event viewer to see if all of my writes to video memory occurred during vblank, and it looks good!
All those dots at the bottom are writes. And they're all in that big blank space at the bottom, where they belong. |
Now let's see what the tiles look like when animated:
Trippy! |
So the finished effect, which probably took more time than any other single element of the game, at about 3 hours, looks like this:
Making it match the real thing
At this point, I felt like I had all the features in place, so it was time to play it back to back with the real thing and see what needed to be adjusted. I knew my speeds/timings were all off, and I had none of the real patterns from the game.
So I opened up a youtube video of someone playing the game, resize it to match my emulator size, and started adjusting things to match the speed. After a few minutes, I had the movement speeds all close enough.
There were a few other minor things that I had forgotten (the enemies change color, and the top and bottom trampoline borders changed color when you hit them). These were fairly simple changes based on how the NES uses palettes.
The one important thing that I had forgotten was warnings about upcoming enemies. These are small arrows that appear on the sides, and warn when an enemy is about to spawn there. They're crucial to surviving, particularly if you take advantage of wrapping around the screen to dodge obstacles.
Unfortunately, adding these warnings wasn't quite as straightforward as everything else in the process. Because they appear about half a second before the actual obstacle, they could be part of the same pattern as currently-spawning obstacle, or the next pattern, or even (for very short patterns), 2 patterns ahead of the current obstacle.
Previously, I had been only tracking the "current" pattern, and when that pattern finished, I'd randomly pick a new one. But now I had to keep a short queue of patterns. The pattern a half second in the future, the currently spawning pattern, and any patterns in between. So I basically have to have a few things:
- A queue of pattern ids
- A pointer to where in the queue we're at for the "warnings".
- A pointer to where in the queue we're at for the "current"
- A pointer to where within the "warning" pattern we're curretly at
- A pointer to where within the "current" pattern we're curretly at
It wasn't overly complicated, but was just enough logic and fiddly indirection that it actually took some real debugging, as opposed to most of the rest of the project.
After I finally had that working, I sat down with a piece of paper and the youtube video (set to 1/4 speed), and wrote down all the enemy patterns that spawned. To this day, I'm still not sure whether every single pattern was pre-generated, or if some of them were completely randomized. But to simplify the game, I limited it to a handful of the patterns that I saw during the first few minutes of the video. Transcribing them to the source code took another hour, but after that, the game was in pretty good playable shape! At this point (around 9-10 hours), I was confident that I had a viable entry for the competition.
This was also the point where I decided that I should probably check about getting actual permission for this project. With much fear and trembling, I send Terry Cavanagh a message (on twitter) with a screenshot, telling him that I had made the game, and asking if I could release the rom. His response: "Sure!" WHEW. That worry was resolved.
Title Screen
Next up was a title screen. I lucked out by the fact that I had (by complete accident) included my font in just the right place in video memory so that the tile id of each glyph was the same as its ASCII code. That meant that I could actually use C strings for text! I whipped together some routines in C to write text to the screen (which were horribly slow, but because they run during couple frames of black screen before the title appears, nobody would notice). With those, I threw together a primitive title screen, but it was ugly and needed something.
The title screen for the mobile Super Gravitron had these cool-looking little rectangles flying around in the background, so I stole that idea. Using sprites, I generated a bunch of them to randomly fly around. And because they have no logic other than flying across the screen before reappearing randomly somewhere else, they were painless to create.
Multiplayer
Now really what I had been hoping to add all along (but not sure if I'd have time) was multiplayer. I'm a huge fan of 4-player simultaneous games (which are sorely lacking on the NES). And the chaos of this game would be perfect for multiplayer madness.
For the most part, adding extra player characters was straightforward, but two things are worth mentioning.
First, I did something goofy for the main character logic -- instead of handling each character's input and updates in a loop, I copied the player update logic 4 times. Why? Becuase I wrote it in C, but the 6502 C compiler is notoriously bad about handling loops where you loop through elements repeatedly in arrays. Instead of storing the loop counter in a register (x or y), it stores it in a variable, then reloads it to x every time you try to look up a value in an array.
So to keep the performance from being stupid, I really needed to either rewrite the routine in assembly, or copy/paste the routine 4 times and let it hard code the addresses. I chose copy/paste because I was at around 11 hours and wanted to be finished.
The other painful thing was handling player deaths. When a player dies, everything pauses, the player changes color for half a second, then the remaining enemies do a cool extra-fast flyaway, then it starts over. Somehow, even though it seems really simple, I had a difficult time getting the logic right to handle deaths for all 4 players. I just kept introducing bugs when trying to manage the "make the player stop moving and change color while dying" state for some players but not all players were dead. I don't know why I had so much trouble with it (the logic isn't complicated!) but I wasted a good hour on it.
Finished?
Now everything was done. Except one small thing. There's a sound effect that occurs when you die, and I wanted it in the game. And I wanted it to sound somewhat like the real thing. And I'm terrible at making sound effects.
There's a great program out there, FamiTracker, for working with NES audio. But I'm not very good at using it. And certainly not good at trying to reproduce an existing sound. But there was no other way to do it, so I used my last hour fiddling with instrument settings in FamiTracker until I got this sound reasonably close to the real thing.
Finished!
So, after about 12 hours of work (spread out throughout the week), I was done! I loaded it onto a cart (which is always scary, as it usually never works right on hardware), and (thanks to the accuracy of Mesen), it worked on the first try! I gathered my family around and forced them to test it out with me. Our family average was somewhere around 3 seconds per life. Success!
All in all, I'm pretty happy with how it turned out. There's definitely some things that could be slightly closer to the real thing (better analysis of how the patterns were generated, making sure speeds matched the real thing, making fonts match, etc), but I feel like it captures the fun energy of the original game and successfully adds a multiplayer element. And I managed to pull off a reasonable competition entry in a week.
You can download the game here. If you try it, let me know your best time!
1 comment:
My best score: 16, 08 it's so funny
Post a Comment