There is a nifty site called Kosmi that allows you to play NES games as a group with up to 4 players using a web-based emulator. As my friends and I have been sheltering to avoid COVID-19, we used it to play a couple of old NES games. One tricky bit is there aren't a lot of great 4-player games out there.
Of course, the NES architecture is well-understood, so I decided to make one. Based on a suggestion from my wife, I made a simple game, where the object is to pet an adorable dog. Along the way, I learned some neat things and was reminded of how fascinatingly ancient the NES architecture is.
For crafting the ROM, I relied heavily on resources put together by Bob Rost in 2004, including his own NES programming language, nbasic. It's an enhancement on the relatively standard nesasm assembly language that allows for some additional "sugar" features at a relatively low cost to program size and efficiency; it saved me a bundle of assembly-language hacking. I forked my own instance of the nbasic project (for reasons I may go into in a later blog post).
For graphics, I used a simple editor called YY-CHR, which is tuned for the features of the NES CHR format (in particular, it respects the 4-color constraint and makes it easy to see how changes map to results in the final product). A simple Makefile to keep everything synchronized, and I was off to the races.
The NES CPU
The NES used a 6502-series microprocessor. This is actually the first micro-language architecture I ever touched (it's the same assembly as used in the old Apple ][ computer line). By modern standards, it's quirky and ancient; the system is limited to three registers (a, x, y), 16-bit addressing, and 8-bit math; the addressing being wider than the math means the architecture isn't capable of considering one entire memory address for pointer arithmetic, so I find the need to write a lot of redundant code; very similar functions end up being separate because all references to memory need to work from different base addresses, coded into the code itself.
While it's quirky, it's enough CPU to run a game.
Editing the CHR file with YY-CHR
The 6502 chip isn't clocked nearly fast enough to push pixels to a screen at a rate that keeps the NTSC or PAL television standards happy. To solve this, the Nintendo used a custom chip called the "PPU," which kept images flowing to the screen using some very regimented rules. The rules are described on the NESDEV wiki, but some brief highlights:
- The exact pixels generated come from tiles stored in a particular ROM (in the cartridge), the pattern table. The pattern table describes tiles of 8-by-8 pixels that can be rendered. The nesasm system can take CHR files and "bake" them directly into the ROM as the pattern table.
- Color of the pixels is determined by palettes. For background, there is one shared "background" color and three colors that can vary in each palette; there are four total background palettes, for variation of up to 13 colors on screen at a time. Foreground has similar rules (4 palettes, 3 colors), with one color reserved as "transparent", which shows the background.
- Background images are rendered by setting values in the "nametable," an array of values indexing into the CHR tiles. So each 8x8 chunk of the background corresponds to one tile. The color of pixels in the tile is controlled by palette selection in the "attribute tables," which select color for 16x16 (i.e. 2-tiles-by-2-tiles) segments of the screen. There are up to 4 nametable / attribute table pairs, allowing for screen scrolling.
- Sprites are configured as X and Y position, the CHR they display (or 2 CHRs, if "8x16 mode" is selected), and some attributes controlling color, flipping the sprite, etc.
The CPU accesses all of this complexity using some "magic addresses" that set values on the PPU. Writing values to address $2006 sets an address latch in the PPU's memory, and bytes written to $2007 are then stored to that address, cursor-style (i.e. if $10, $00 is stored to $2006, then values written to $2007 will be stored at address $1000, $1001, $1002, etc.). One tricky bit is that the $2006 latch is used by the PPU when it's sending colors to the TV, so changing it when the NES is actually drawing can change what colors are output and mess up the screen. This is actually a feature, not a bug; fancy graphics effects (such as scrolling one section of the screen separately from another, like in Legend of Zelda) are implemented by altering the screen scroll mid-draw operation.
Relative to the PPU, controllers are very simple. The controller architecture in the NES is a serial bus; setting a 1 to the $4016 address tells every controller to remember what buttons are currently pressed, and then setting it back to 0 and reading from $4016 (and $4017 for controller 2) gives one button's press state at a time. Since I made a 4-player game, it also needs the button values from controllers 3 and 4; the NES "Four Score" 4-controller adapter did this by adding controller 3 to 1's output and controller 4 to 2's output, essentially making it look like controllers 1 and 2 had twice as many buttons. Not too hard to work with.
Petris is a game where you pet an adorable dog. You score 1 point for every time you pet the dog in its favorite spot, which changes as time goes on. At the end, the player with the most points (and the adorable dog) win!
To try it out, download the NES file and load it into your favorite NES emulator. If your emulator supports 4 controllers, you can play with three friends. Enjoy! :)