From 7429c0e75f9d54c19ac847b754a77ff5104b3e5e Mon Sep 17 00:00:00 2001 From: Rhett Aultman Date: Mon, 1 Oct 2018 11:21:24 -0400 Subject: [PATCH] Drawing sprites This demo shows how to use the Object Attribute Memory (OAM), a part of the PPU memory space dedicated to working with sprites. Additionally, this demo uses the OAM DMA technique for trasferring a full OAM table from RAM into the PPU memory space, since this is the most common technique used by NES game programmers. --- Makefile | 2 +- src/background.asm | 178 ------------------------------- src/sprite.asm | 259 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 179 deletions(-) delete mode 100644 src/background.asm create mode 100644 src/sprite.asm diff --git a/Makefile b/Makefile index a8a75ea..39b6bf3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SRCDIR = src OBJDIR = obj BINDIR = bin -TARGET = background.nes +TARGET = sprite.nes SOURCES := $(wildcard $(SRCDIR)/*.asm) OBJECTS := $(SOURCES:$(SRCDIR)/%.asm=$(OBJDIR)/%.o) diff --git a/src/background.asm b/src/background.asm deleted file mode 100644 index bf6afc3..0000000 --- a/src/background.asm +++ /dev/null @@ -1,178 +0,0 @@ -; Nametables and attribute tables Demo for NES -; Following on from the background palette program, this program looks at how -; to load a pattern table, which contains the actual graphic tiles, a nametable, -; which contains the layout of graphic tiles for the background, and the -; attribute table, which specifies what palette to paint everything with. -; -; I am not an artist, and even then, converting bitmaps to pattern tables can be -; a chore. So, I used the "Generitiles" from the NESDev wiki -; (https://wiki.nesdev.com/w/index.php/Placeholder_graphics). I then ran these -; through a Python script supplied by Damian Yerrick -; (https://github.com/pinobatch/nesbgeditor) which converts a 2-bit PNG into -; pattern tables pretty darn effectively. Thanks, Damian! -; -; So, we're now *finally* using the CHARS section, which gets directly mapped -; into the PPU's memory at power on. This section really should more -; be named "CHR-ROM", as this is the more common name for it. You'll notice -; that I can directly include the file produced by Damian's tools, which keeps -; the code tidy. -; -; With the patterns directly mapped in, the next step is to load up some data -; in the name table. Since we're not doing anything fancy, I've restricted -; things to the first of the two name tables. Much like we did for loading the -; palette colors, we load this through the use of PPUADDR and PPUDATA. -; -; Finally, we load the attribute table, which says which palette to use for -; each 32x32 pixel region on the screen. In a more advanced demo, this would -; raise the number of effective colors I was using on the screen. For now, -; though, I just want to keep things simple and easy-to-explain. -; -; Note that the annyoing "hello world" sound is now gone. The graphics show -; that everything is working. - -.define PPUMASK $2001 -.define PPUSTATUS $2002 -.define PPUADDR $2006 -.define PPUSCROLL $2005 -.define PPUDATA $2007 - -.define NAMETABLE_0_HI $20 -.define NAMETABLE_0_LO $00 -.define ATTRTABLE_0_HI $23 -.define ATTRTABLE_0_LO $C0 -.define BGPALETTE_HI $3F -.define BGPALETTE_LO $00 - -; Mandatory iNES header. -.segment "HEADER" - -.byte "NES", $1A ; "NES" magic value -.byte 2 ; number of 16KB code pages (we don't need 2, but nes.cfg declares 2) -.byte 1 ; number of 8KB "char" data pages -.byte $00 ; "mapper" and bank-switching type (0 for "none") -.byte $00 ; background mirroring flats - ; - ; Note the header is 16 bytes but the nes.cfg will zero-pad for us. - -; code ROM segment -; all code and on-ROM program data goes here - -.segment "STARTUP" - -; reset vector -reset: - bit PPUSTATUS ; clear the VBL flag if it was set at reset time -vwait1: - bit PPUSTATUS - bpl vwait1 ; at this point, about 27384 cycles have passed -vwait2: - bit PPUSTATUS - bpl vwait2 ; at this point, about 57165 cycles have passed - - ; Interesting little fact I learned along the way. Because it takes two - ; stores on PPUADDR to move its pointer, it's good practice to start all of - ; your PPUADDR use with a peek at PPUSTATUS since this resets its "latch" - ; and ensures you're addressing the address you expect. - ; Technically, we don't need this because we did it in the reset code, but - ; it's a neat little thing to mention here - - bit PPUSTATUS - - ; load the background palette - lda #BGPALETTE_HI - sta PPUADDR - lda #BGPALETTE_LO - sta PPUADDR - - ; prep the loop - ldx #0 - -paletteloop: - lda bgpalette, X ; load from the bgpalette array - sta PPUDATA ; store in PPUDATA, PPU will auto-increment - inx ; increment the X (index) register - cpx #16 - bne paletteloop ; run the loop until X=16 (size of the palettes) - -; move PPUADDR over to nametable 0. - lda #NAMETABLE_0_HI - sta PPUADDR - lda #NAMETABLE_0_LO - sta PPUADDR - -; This loop iterates over the pattern table, outputting it in lines of 16 -; The other 16 are just padded out with a pattern that's blank. This lets -; me easily show you some simple graphics that are made up of multiple -; stacked tiles without getting too fancy. In reality, you'd probably have -; complete nametables that you'd load in from files and simply run in a loop. - - ldx #$00 - ldy #16 -nametableloop: - stx PPUDATA - inx - dey - bne nametableloop - ldy #16 -padright: - sta PPUDATA - dey - bne padright - cpx $D0 - beq done - ldy #16 - jmp nametableloop -done: - -; set up Palette 0 for everything - bit PPUSTATUS - lda #ATTRTABLE_0_HI - sta PPUADDR - lda #ATTRTABLE_0_LO - sta PPUADDR - ldx #64 ; 64 tiles in the attribute table - lda #0 - -attrloop: - sta PPUDATA - dex - bne attrloop - -; Enable background and sprite rendering. This is suuuuuper important to -; remember. I didn't remember to put this in and probably blew a whole day -; trying to figure out why my emulator hated me. - lda #$1e - sta PPUMASK - -forever: - jmp forever - -nmi: - rti ; Return from the NMI (NTSC refresh interrupt) - - -; The background colors are, in order: -; $0F: black -; $15: red -; $22: blue -; $20: white - -bgpalette: - .byte $0F, $15, $22, $20 ; palette 0 - .byte $0F, $15, $22, $20 ; palette 1 - .byte $0F, $15, $22, $20 ; palette 2 - .byte $0F, $15, $22, $20 ; palette 3 - - -; vectors declaration -.segment "VECTORS" -.word nmi -.word reset -.word 0 - -; As mentioned above, this is the place where you put your pattern table data -; so that it can automatically be mapped into the PPU's memory at $0000-$1FFF. -; Note the use of .incbin so I can just import a binary file. Neato! - -.segment "CHARS" -.incbin "generitiles.pat" diff --git a/src/sprite.asm b/src/sprite.asm new file mode 100644 index 0000000..ef9a4e2 --- /dev/null +++ b/src/sprite.asm @@ -0,0 +1,259 @@ +; Drawing static sprites +; Now that we can confirm the palettes, pattern table, and nametable loading +; are all working, we can work with the next part of the NES PPU-- the sprite +; subsystem. +; +; Sprites are controlled by the Object Attribute Memory (OAM). There are 64 +; available entries in the OAM, with each specifying the coordinates and +; pattern for the sprite. Sprites may be layered in front of or behind the +; background, too (see Super Mario Bros 3 for a classic example of behind-the- +; background sprite use). +; +; Technically, you can write to the OAM using addresses $2003 and $2004, called +; OAMADDR and OAMDATA, but this is not commonly done because address $4014 +; manages a direct memory access (DMA) controller that can copy a complete OAM +; table from the page of your choice in memory. By convention, most NES coders +; use $0200, so writing 2 to $4014 triggers a dump into the PPU's OMA table. +; The CPU is stalled during this transfer, so you need only make the write and +; then carry on with the code. + +.define SPRITE_PAGE $0200 + +.define PPUMASK $2001 +.define PPUSTATUS $2002 +.define PPUADDR $2006 +.define PPUSCROLL $2005 +.define PPUDATA $2007 + +.define OAM_DMA $4014 + +.define OAM_PAGE 2 + +.define NAMETABLE_0_HI $20 +.define NAMETABLE_0_LO $00 +.define ATTRTABLE_0_HI $23 +.define ATTRTABLE_0_LO $C0 +.define BGPALETTE_HI $3F +.define BGPALETTE_LO $00 + +; Mandatory iNES header. +.segment "HEADER" + +.byte "NES", $1A ; "NES" magic value +.byte 2 ; number of 16KB code pages (we don't need 2, but nes.cfg declares 2) +.byte 1 ; number of 8KB "char" data pages +.byte $00 ; "mapper" and bank-switching type (0 for "none") +.byte $00 ; background mirroring flats + ; + ; Note the header is 16 bytes but the nes.cfg will zero-pad for us. + +; code ROM segment +; all code and on-ROM program data goes here + +.segment "STARTUP" + +; reset vector +reset: + bit PPUSTATUS ; clear the VBL flag if it was set at reset time +vwait1: + bit PPUSTATUS + bpl vwait1 ; at this point, about 27384 cycles have passed +vwait2: + bit PPUSTATUS + bpl vwait2 ; at this point, about 57165 cycles have passed + + ; Interesting little fact I learned along the way. Because it takes two + ; stores on PPUADDR to move its pointer, it's good practice to start all of + ; your PPUADDR use with a peek at PPUSTATUS since this resets its "latch" + ; and ensures you're addressing the address you expect. + ; Technically, we don't need this because we did it in the reset code, but + ; it's a neat little thing to mention here + + bit PPUSTATUS + + ; load the palettes + lda #BGPALETTE_HI + sta PPUADDR + lda #BGPALETTE_LO + sta PPUADDR + + ; prep the loop + ldx #0 + +; the palette loop now loads 8 palettes; 4 are for the background +; tiles and 4 are for sprites. Before working with sprites, you +; must set sprite palette colors, or you'll have a bad time + +paletteloop: + lda bgpalette, X ; load from the bgpalette array + sta PPUDATA ; store in PPUDATA, PPU will auto-increment + inx ; increment the X (index) register + cpx #32 + bne paletteloop ; run the loop until X=32 (size of the palettes) + +; move PPUADDR over to nametable 0. + lda #NAMETABLE_0_HI + sta PPUADDR + lda #NAMETABLE_0_LO + sta PPUADDR + +; set up Palette 0 for everything + bit PPUSTATUS + lda #ATTRTABLE_0_HI + sta PPUADDR + lda #ATTRTABLE_0_LO + sta PPUADDR + ldx #64 ; 64 tiles in the attribute table + lda #0 + +attrloop: + sta PPUDATA + dex + bne attrloop + +; Now for the meat of this lab-- making a sprite. This makes a +; basic character using tiles we already have at hand from last lab's +; pattern table. Note that the character is made up of six 8x8 tiles +; and thus it takes six entries in the OAM to draw him. It's common +; to think of "sprite" as "character," but on this early hardware, +; all sprites are fixed sizes expressed in either 8x8 or 8x16 tiles. +; So those 64 sprites don't go as far as it might seem! + +; zero out the OAM DMA shadow page + ldx #$FF + lda $0 +zero_oam: + sta SPRITE_PAGE, X + dex + bne zero_oam + +; refresh our index register...we're going to make heavy use of it +; now... + + ldx #0 + +; head + lda #$7F ; Y coordinate + sta SPRITE_PAGE, X + inx + lda #$A0 ; Pattern bank 0, tile A0 (A1 is bottom) + sta SPRITE_PAGE, X + inx + lda #$00 ; No flipping, in front of background, palette 0 + sta SPRITE_PAGE, X + inx + lda #$7F ; X coordinate + sta SPRITE_PAGE, X + inx + lda #$7F ; Y coordinate + sta SPRITE_PAGE, X + inx + lda #$A1 ; Pattern bank 0, tile A0 (A1 is bottom) + sta SPRITE_PAGE, X + inx + lda #$00 ; No flipping, in front of background, palette 0 + sta SPRITE_PAGE, X + inx + lda #$87 ; X coordinate + sta SPRITE_PAGE, X + inx +; torso + lda #$87 ; Y coordinate + sta SPRITE_PAGE, X + inx + lda #$B0 ; Pattern bank 0, tile A0 (A1 is bottom) + sta SPRITE_PAGE, X + inx + lda #$00 ; No flipping, in front of background, palette 0 + sta SPRITE_PAGE, X + inx + lda #$7F ; X coordinate + sta SPRITE_PAGE, X + inx + lda #$87 ; Y coordinate + sta SPRITE_PAGE, X + inx + lda #$B1 ; Pattern bank 0, tile A0 (A1 is bottom) + sta SPRITE_PAGE, X + inx + lda #$00 ; No flipping, in front of background, palette 0 + sta SPRITE_PAGE, X + inx + lda #$87 ; X coordinate + sta SPRITE_PAGE, X + inx +; feet + lda #$8F ; Y coordinate + sta SPRITE_PAGE, X + inx + lda #$C0 ; Pattern bank 0, tile A0 (A1 is bottom) + sta SPRITE_PAGE, X + inx + lda #$00 ; No flipping, in front of background, palette 0 + sta SPRITE_PAGE, X + inx + lda #$7F ; X coordinate + sta SPRITE_PAGE, X + inx + lda #$8F ; Y coordinate + sta SPRITE_PAGE, X + inx + lda #$C1 ; Pattern bank 0, tile A0 (A1 is bottom) + sta SPRITE_PAGE, X + inx + lda #$00 ; No flipping, in front of background, palette 0 + sta SPRITE_PAGE, X + inx + lda #$87 ; X coordinate + sta SPRITE_PAGE, X + +; OAM DMA must always be a transfer from address XX00-XXFF, so we write +; the value of XX (in this case, 2) to OAM_DMA ($4014) to trigger the +; transfer + + lda #OAM_PAGE + sta OAM_DMA + +; Enable background and sprite rendering. + lda #$1e + sta PPUMASK + + + +forever: + jmp forever + +nmi: + rti ; Return from the NMI (NTSC refresh interrupt) + + +; The background colors are, in order: +; $0F: black +; $15: red +; $22: blue +; $20: white + +bgpalette: + .byte $0F, $15, $22, $20 ; palette 0 + .byte $0F, $15, $22, $20 ; palette 1 + .byte $0F, $15, $22, $20 ; palette 2 + .byte $0F, $15, $22, $20 ; palette 3 +spritepalette: + .byte $0F, $07, $19, $20 ; palette 0 + .byte $0F, $07, $19, $20 ; palette 1 + .byte $0F, $07, $19, $20 ; palette 2 + .byte $0F, $07, $19, $20 ; palette 3 + + +; vectors declaration +.segment "VECTORS" +.word nmi +.word reset +.word 0 + +; As mentioned above, this is the place where you put your pattern table data +; so that it can automatically be mapped into the PPU's memory at $0000-$1FFF. +; Note the use of .incbin so I can just import a binary file. Neato! + +.segment "CHARS" +.incbin "generitiles.pat" -- 2.34.1