summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2014-10-19 19:53:54 -0400
committerJesse Luehrs <doy@tozt.net>2014-10-19 19:53:54 -0400
commit87b21d8f73edfbc66ab55db5738ac29579892154 (patch)
tree861830011a8567b1606cd960c011ce11aa6a9892
parentfd5e8fd3eb467395b1356910f799db2b466ca42f (diff)
downloadblog.tozt.net-87b21d8f73edfbc66ab55db5738ac29579892154.tar.gz
blog.tozt.net-87b21d8f73edfbc66ab55db5738ac29579892154.zip
part 4
-rw-r--r--media/sprites.chrbin0 -> 8192 bytes
-rw-r--r--media/sprites.s197
-rw-r--r--posts/writing-an-nes-game-part-4.md186
3 files changed, 383 insertions, 0 deletions
diff --git a/media/sprites.chr b/media/sprites.chr
new file mode 100644
index 0000000..3713c9c
--- /dev/null
+++ b/media/sprites.chr
Binary files differ
diff --git a/media/sprites.s b/media/sprites.s
new file mode 100644
index 0000000..5f0b3d9
--- /dev/null
+++ b/media/sprites.s
@@ -0,0 +1,197 @@
+.ROMBANKMAP
+BANKSTOTAL 2
+BANKSIZE $4000
+BANKS 1
+BANKSIZE $2000
+BANKS 1
+.ENDRO
+
+.MEMORYMAP
+DEFAULTSLOT 0
+SLOTSIZE $4000
+SLOT 0 $C000
+SLOTSIZE $2000
+SLOT 1 $0000
+.ENDME
+
+.ENUM $00
+sleeping DB
+.ENDE
+
+; just use the actual locations in our copy of SPR-RAM rather than zero-page
+; addresses, to avoid having to do multiple copies
+.define sprite_x $0203
+.define sprite_y $0200
+
+ .bank 0 slot 0
+ .org $0000
+RESET:
+ SEI
+ CLD
+ LDX #$FF
+ TXS
+ INX
+ STX $2000.w
+ STX $2001.w
+ STX $4010.w
+ LDX #$40
+ STX $4017.w
+
+vblankwait1:
+ BIT $2002
+ BPL vblankwait1
+
+clrmem:
+ LDA #$00
+ STA $0000, x
+ STA $0100, x
+ STA $0300, x
+ STA $0400, x
+ STA $0500, x
+ STA $0600, x
+ STA $0700, x
+ LDA #$FE
+ STA $0200, x
+ INX
+ BNE clrmem
+
+ ; start with the sprite near the middle of the screen
+ LDA #$80
+ STA sprite_x
+ STA sprite_y
+
+vblankwait2:
+ BIT $2002
+ BPL vblankwait2
+
+ ; PPU is initialized here, so we can start writing data into it. this is safe
+ ; because we have not yet enabled drawing, and so we don't have to restrain
+ ; ourselves to vblank.
+
+ ; first, we load the palettes into $3F00 and $3F10
+load_palettes:
+ LDA $2002 ; read here so that the next byte written to $2006 is the high
+ LDA #$3F ; byte of the address
+ STA $2006 ; write the high byte of the base address
+ LDA #$00
+ STA $2006 ; write the low byte of the base address
+ LDX #$00
+load_palettes_loop:
+ LDA palette.w, x
+ STA $2007
+ INX
+ CPX #$20 ; 16 byte background palette plus 16 byte sprite palette
+ BNE load_palettes_loop
+
+ ; then we draw the background (doing that here because it won't be changing)
+ LDA #$20
+ STA $2006 ; high byte of the starting address
+ LDA #$00
+ STA $2006 ; low byte of the starting address
+ LDA #$01 ; pattern index 1 is our background tile
+ LDX #$04 ; this loop will load $2000-$23FF, which includes the
+ LDY #$00 ; attribute table range, but we can just adjust the
+load_background_loop: ; palette to take that into account
+ STA $2007
+ INY
+ BNE load_background_loop
+ DEX
+ BNE load_background_loop
+
+ ; then we set the unchanging parts of our sprite (the pattern index and the
+ ; attributes)
+ LDA #$00
+ STA $0201 ; pattern index 0 is our sprite
+ STA $0202 ; don't need any attributes
+
+ ; enable the PPU
+ LDA #%10000000
+ STA $2000
+ LDA #%00011000
+ STA $2001
+
+loop:
+ INC sleeping
+wait_for_vblank_end:
+ LDA sleeping
+ BNE wait_for_vblank_end
+
+ LDA #$01
+ STA $4016
+ LDA #$00
+ STA $4016
+
+ ; we don't care about a, b, select, start
+ LDA $4016
+ LDA $4016
+ LDA $4016
+ LDA $4016
+
+up:
+ LDA $4016
+ AND #%00000001
+ BEQ down
+ LDX sprite_y.w
+ DEX
+ STX sprite_y.w
+down:
+ LDA $4016
+ AND #%00000001
+ BEQ left
+ LDX sprite_y.w
+ INX
+ STX sprite_y.w
+left:
+ LDA $4016
+ AND #%00000001
+ BEQ right
+ LDX sprite_x.w
+ DEX
+ STX sprite_x.w
+right:
+ LDA $4016
+ AND #%00000001
+ BEQ loop_end
+ LDX sprite_x.w
+ INX
+ STX sprite_x.w
+
+loop_end:
+ JMP loop
+
+NMI:
+ PHA
+ TXA
+ PHA
+ TYA
+ PHA
+
+ ; now the only thing we need to do here is issue a DMA call to transfer our
+ ; sprite data into SPR-RAM
+ LDA #$00
+ STA $2003 ; reset the SPR-RAM write offset
+ LDA #$02
+ STA $4014 ; start the DMA transfer from $0200
+
+ LDA #$00
+ STA sleeping
+ PLA
+ TAY
+ PLA
+ TAX
+ PLA
+
+ RTI
+
+palette:
+ .db $0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30
+ .db $0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30
+
+ .orga $FFFA
+ .dw NMI
+ .dw RESET
+ .dw 0
+
+ .bank 1 slot 1
+ .org $0000
+ .incbin "sprites.chr"
diff --git a/posts/writing-an-nes-game-part-4.md b/posts/writing-an-nes-game-part-4.md
new file mode 100644
index 0000000..d9a7c5c
--- /dev/null
+++ b/posts/writing-an-nes-game-part-4.md
@@ -0,0 +1,186 @@
+---
+title: writing an nes game, part 4
+date: "2014-10-17 03:30"
+tags: [hacker school, nes]
+---
+
+# Writing an NES game - graphics
+
+Now that we can handle input, it's time to learn how to draw things. All
+drawing on the NES is handled by the PPU. The PPU does sprite-based drawing,
+meaning that all drawing is two dimensional, and happens in 8x8 blocks. Colors
+are determined by a 16-color palette, of which a given sprite can only use
+four colors (transparency being one of the colors). There is a background
+layer, containing 32x30 tiles, and a set of up to 64 sprites, which can each
+be either in front of or behind the background.
+
+The PPU has its own memory space and mostly just runs on its own, handling the
+redrawing automatically every frame. In order to tell it what to do, we have
+to load various bits of data into its memory. As mentioned in an earlier post,
+the PPU is only capable of doing one thing at a time, so this drawing must
+happen at the beginning of the NMI interrupt, when we are guaranteed to be in
+VBlank.
+
+There are four steps involved in drawing a sprite. First, we need the sprite
+itself. The pixel data for sprites is stored in the *pattern table*, and is
+typically (although not always) stored in CHR-ROM. It contains 16 bytes per
+sprite, where each pixel contains two bits of data, so each sprite can contain
+at most four colors (including transparent). Since CHR-ROM banks are 8KB
+each, this provides enough room for two sets of 256 tiles - typically one set
+is used for the background and the other set is used for foreground sprites,
+although this is not required. The patterns are laid out in memory as two
+chunks of 8 bytes each, where the first 8 bytes correspond to the high bit for
+the pixel, and the second 8 bytes correspond to the low bit for the pixel
+(each byte representing a row of pixels in the sprite). To help with
+generating this sprite data, I have written a script called
+[pnm2chr](https://metacpan.org/pod/distribution/Games-NES-SpriteMaker/bin/pnm2chr),
+which can convert images in the .PBM/.PGM/.PPM formats (which most image
+editors can produce) into CHR-ROM data, so that you can edit sprites in an
+image editor instead of a hex editor.
+
+Once we have the pattern data, we then need the *palette table*, to determine
+the actual colors that will be displayed for a given sprite. The way that
+colors are determined is actually quite convoluted, going through several
+layers of indirection. As a basis, the NES is capable of producing 52 distinct
+colors (actually 51, since one of the colors ($0D) is outside of the NTSC
+[gamut](https://en.wikipedia.org/wiki/Gamut), and can damage older TVs if
+used). From those 52 colors, only 16 can be used at a time (although the 16
+colors can be distinct for the background layer and the sprite layer), and
+this set of 16 colors is known as the palette.
+
+To determine which palette color to use for each pixel in a given background
+tile, the two bits from the pattern table are combined with two additional
+bits of data from the *attribute table* to create a four bit number (the
+pattern bits being the low bits and the attribute bits being the high bits).
+The attribute table is a 64-byte chunk of memory which stores two bits for
+each 16x16 pixel area of the screen (so each sprite shares the same two-bit
+palette with three other sprites in a 2x2 block). The attribute data itself is
+packed into bytes such that each byte corresponds to a 4x4 block of sprites.
+This is all pretty confusing, so here is a diagram (from the [NES Technical
+Documentation](http://emu-docs.org/NES/nestech.txt)) which will hopefully make
+things a bit clearer:
+
+ +------------+------------+
+ | Square 0 | Square 1 | #0-F represents an 8x8 tile
+ | #0 #1 | #4 #5 |
+ | #2 #3 | #6 #7 | Square [x] represents four (4) 8x8 tiles
+ +------------+------------+ (i.e. a 16x16 pixel grid)
+ | Square 2 | Square 3 |
+ | #8 #9 | #C #D |
+ | #A #B | #E #F |
+ +------------+------------+
+
+ Attribute Byte
+ (Square #)
+ ----------------
+ 33221100
+ ||||||+--- Upper two (2) colour bits for Square 0 (Tiles #0,1,2,3)
+ ||||+----- Upper two (2) colour bits for Square 1 (Tiles #4,5,6,7)
+ ||+------- Upper two (2) colour bits for Square 2 (Tiles #8,9,A,B)
+ +--------- Upper two (2) colour bits for Square 3 (Tiles #C,D,E,F)
+
+For sprites, the upper two bits of the palette index is specified when
+requesting the sprite to be drawn.
+
+The data about which background tile to draw is then stored in the *name
+table*. This is a sequence of bytes which correspond to offsets into the
+pattern table. For instance, to draw the first pattern in the pattern table,
+you would write a $00 to the corresponding location in the name table. The
+name table data is the combined with the appropriate attribute table data to
+get a palette index, which is then looked up in the palette table to determine
+the actual colors to use when drawing the tile.
+
+The data about which sprites to draw is stored in an entirely separate area of
+memory (not part of any address space at all), called the SPR-RAM (sprite
+RAM). It is 256 bytes long, and holds four bytes for each of the 64 sprites
+that the NES is capable of drawing at any given time. The first byte holds the
+vertical offset for the sprite (where the top left of the screen is (0, 0)),
+the second byte holds the index into the pattern table for the sprite to draw,
+the third byte holds various attributes about the sprite, and the fourth byte
+holds the horizontal offset for the sprite. The sprite attributes contain
+these bits of data:
+
+* The low two bits (bits 0 and 1) contain the high bits of the palette index,
+ as described above.
+* Bit 5 is set if the sprite should be drawn behind the background.
+* Bit 6 is set if the sprite should be flipped horizontally.
+* Bit 7 is set if the sprite should be flipped vertically.
+
+If you don't need all 64 sprites, you should just move the horizontal and
+vertical coordinates such that the sprite is offscreen ($FE or so).
+
+Now that we have seen all of the different pieces of the PPU memory, here is
+how it is all laid out in memory:
+
+ $0000: Pattern Table 0 (typically in CHR-ROM)
+ $1000: Pattern Table 1 (typically in CHR-ROM)
+ $2000: Name Table 0
+ $23C0: Attribute Table 0
+ $2400: Name Table 1
+ $27C0: Attribute Table 1
+ $2800: (used for mirroring, which I won't discuss here)
+ $2BC0: (used for mirroring, which I won't discuss here)
+ $2C00: (used for mirroring, which I won't discuss here)
+ $2FC0: (used for mirroring, which I won't discuss here)
+ $3000: (used for mirroring, which I won't discuss here)
+ $3F00: Palette Table 0 (used for the background)
+ $3F10: Palette Table 1 (used for sprites)
+ $3F20: (used for mirroring, which I won't discuss here)
+ $4000: (used for mirroring, which I won't discuss here)
+
+So, to draw a background sprite at the top left of the screen, you would write
+the sprite index to VRAM address $2000 (assuming default settings).
+
+The final piece of information necessary to be able to use the PPU is how to
+transfer data from main memory into VRAM. This is done via certain
+memory-mapped IO addresses.
+
+First, to copy data into SPR-RAM, you should write all of the sprite data to a
+single page (a page is a 256-byte chunk of data whose addresses all start with
+the same byte) in RAM, and then write the page number into address $4014 (the
+$02 page ($0200-$02FF) is typically used for this purpose). The address to
+start writing from can be set by writing a byte to address $2003, and so you
+typically want to write $00 into $2003 before starting a full page transfer
+with $4014. If you want to write to only certain parts of SPR-RAM, you can do
+this via address $2004 - set the base address to write to via $2003 as above,
+and then write a sequence of bytes to $2004 to store them into SPR-RAM.
+
+To copy data into the main VRAM address space, you use the addresses $2006 and
+$2007 in the same way that $2003 and $2004 were used for SPR-RAM, except that
+you need to write two bytes into $2006 before you start writing to $2007,
+since the address space is larger. Since the order of the bytes matters here,
+you can read from $2002 to ensure that the next byte written to $2006 will be
+the high byte of the address. Note that $2006 is also used for scrolling
+(which is based on the last address written to), and so you generally want to
+write $2000 back into $2006 at the end of drawing.
+
+Finally, you need to initialize the PPU in a few ways in order to allow
+drawing, which is done via $2000 and $2001. These addresses hold quite a few
+different configuration bits, but the most important ones are:
+
+* Bit 7 of $2000 should be set to enable NMI interrupts (we did this last
+ time).
+* Bit 4 of $2000 should be set to use pattern table 1 instead of 0 for
+ the background.
+* Bit 3 of $2000 should be set to use pattern table 1 instead of 0 for
+ sprites.
+* Bit 0 of $2000 should be set to use name table 1 instead of 0 (this is
+ actually more complicated due to mirroring, but we won't get into that).
+* Bit 4 of $2001 should be set to enable the sprite layer.
+* Bit 3 of $2001 should be set to enable the background layer.
+
+The default pattern and name tables will be fine for now, and so
+initialization should set $2000 to $%10000000 and $2001 to $%00011000.
+
+[Here]({{urls.media}}/sprites.s) is a sample program which draws a background
+and a sprite, and allows you to move the sprite around the background with the
+controller D-pad. It will require a CHR-ROM data file with actual patterns in
+it, so you can download that from [here]({{urls.media}}/sprites.chr)
+
+Further reading:
+* [NES ASM Tutorial](http://nixw0rm.altervista.org/files/nesasm.pdf)
+* [NES technical documentation](http://emu-docs.org/NES/nestech.txt)
+* [NES ROM Quickstart](http://sadistech.com/nesromtool/romdoc.html)
+* [NES 101](http://hackipedia.org/Platform/Nintendo/NES/tutorial%2c%20NES%20programming%20101/NES101.html)
+* [CHR ROM vs. CHR RAM](http://wiki.nesdev.com/w/index.php/CHR_ROM_vs._CHR_RAM)
+* [CHR data layout for The Legend of Zelda](http://www.computerarcheology.com/wiki/wiki/NES/Zelda) (Note that unlike what is described above, Zelda stores its pattern data in RAM rather than ROM.)