NES 6502 Programming Tutorial - Part 3: Drawing a Sprite
Before we get started, I completely forgot to include demos of Tutorials 1 and 2. So I did that if you wanna see for yourself the results in an emulator. Anyways its time for these tutorials to not be so dull and boring just staring at flat colors, and start drawing sprites for once. Makes it more interesting that way. Well...lets get started!!!
The first thing is that well need a color palette for the sprite loaded. So well have to make a small change with our Colored Background demo:
Code:
.inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x 8KB CHR data
.inesmap 0 ; mapper 0 = NROM, no bank swapping
.inesmir 1 ; background mirroring
;;;;;;;;;;;;;;;
.bank 0
.org $C000
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
LDX #$40
STX $4017 ; disable APU frame IRQ
LDX #$FF
TXS ; Set up stack
INX ; now X = 0
STX $2000 ; disable NMI
STX $2001 ; disable rendering
STX $4010 ; disable DMC IRQs
vblankwait1: ; First wait for vblank to make sure PPU is ready
BIT $2002
BPL vblankwait1
clrmem:
LDA #$00
STA $0000, x
STA $0100, x
STA $0400, x
STA $0500, x
STA $0600, x
STA $0700, x
LDA #$FE
STA $0300, x
INX
BNE clrmem
vblankwait2: ; Second wait for vblank, PPU is ready after this
BIT $2002
BPL vblankwait2
LoadPalette:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadBackgroundPaletteLoop:
LDA background_palette, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadBackgroundPaletteLoop ; Branch to LoadBackgroundPaletteLoop if compare was Not Equal to zero
LDX #$00 ;reset the x register to zero so we can start loading sprite palette colors.
LoadSpritePaletteLoop:
LDA sprite_palette, x ;load palette byte
STA $2007 ;write to PPU
INX ;set index to next byte
CPX #$10
BNE LoadSpritePaletteLoop ;if x = $10, all done
Foreverloop:
JMP Foreverloop ;jump back to Forever, infinite loop
NMI:
RTI
;;;;;;;;;;;;;;
.bank 1
.org $E000
background_palette:
.db $22,$29,$1A,$0F ;background palette 1
.db $22,$36,$17,$0F ;background palette 2
.db $22,$30,$21,$0F ;background palette 3
.db $22,$27,$17,$0F ;background palette 4
sprite_palette:
.db $22,$16,$27,$18 ;sprite palette 1
.db $22,$1A,$30,$27 ;sprite palette 2
.db $22,$16,$30,$27 ;sprite palette 3
.db $22,$0F,$36,$17 ;sprite palette 4
;;;;;;;;;;;;;;
.org $FFFA ;first of the three vectors starts here
.dw NMI ;when an NMI happens (once per frame if enabled) the
;processor will jump to the label NMI:
.dw RESET ;when the processor first turns on or is reset, it will jump
;to the label RESET:
.dw 0 ;external interrupt IRQ is not used in this tutorial
;;;;;;;;;;;;;;
.bank 2
.org $0000
.incbin "mario.chr" ;includes 8KB graphics file from SMB1
The next step is to setup some flags for the PPUCTRL register ($2000 write only) and PPUMASK register ($2001 write only) so we can enable sprites and generate an NMI at the start of the VBlank interval. But it's best you learn what each binary number does:
Code:
Controller ($2000) > write
Common name: PPUCTRL
Description: PPU control register
Access: write
Various flags controlling PPU operation
7 bit 0
---- ----
VPHB SINN
|||| ||||
|||| ||++- Base nametable address
|||| || (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
|||| |+--- VRAM address increment per CPU read/write of PPUDATA
|||| | (0: add 1, going across; 1: add 32, going down)
|||| +---- Sprite pattern table address for 8x8 sprites
|||| (0: $0000; 1: $1000; ignored in 8x16 mode)
|||+------ Background pattern table address (0: $0000; 1: $1000)
||+------- Sprite size (0: 8x8; 1: 8x16)
|+-------- PPU master/slave select
| (0: read backdrop from EXT pins; 1: output color on EXT pins)
+--------- Generate an NMI at the start of the
vertical blanking interval (0: off; 1: on)
Code:
Mask ($2001) > write
Common name: PPUMASK
Description: PPU mask register
Access: write
This register controls the rendering of sprites and backgrounds, as well as colour effects.
7 bit 0
---- ----
BGRs bMmG
|||| ||||
|||| |||+- Greyscale (0: normal color, 1: produce a greyscale display)
|||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide
|||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide
|||| +---- 1: Show background
|||+------ 1: Show sprites
||+------- Emphasize red*
|+-------- Emphasize green*
+--------- Emphasize blue*
So now that you know what the registers do, its time to generate the NMI and draw a sprite from Pattern Table 0. Wait whats Pattern Table 0 you ask? Well, remember when I showed you that Super Mario Bros CHR image?
The left half, believe it or not is Pattern Table 0, and the right half is Pattern Table 1. And for this demo we'll only be snagging it from Pattern Table 0 for simplicity. But wait, there is more!. In the PPUMASK register ($2001), we need to enable sprite drawing in general or we won't see the results. So were going to add a couple of lines of code right after loading the palettes:
Code:
.inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x 8KB CHR data
.inesmap 0 ; mapper 0 = NROM, no bank swapping
.inesmir 1 ; background mirroring
;;;;;;;;;;;;;;;
.bank 0
.org $C000
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
LDX #$40
STX $4017 ; disable APU frame IRQ
LDX #$FF
TXS ; Set up stack
INX ; now X = 0
STX $2000 ; disable NMI
STX $2001 ; disable rendering
STX $4010 ; disable DMC IRQs
vblankwait1: ; First wait for vblank to make sure PPU is ready
BIT $2002
BPL vblankwait1
clrmem:
LDA #$00
STA $0000, x
STA $0100, x
STA $0400, x
STA $0500, x
STA $0600, x
STA $0700, x
LDA #$FE
STA $0300, x
INX
BNE clrmem
vblankwait2: ; Second wait for vblank, PPU is ready after this
BIT $2002
BPL vblankwait2
LoadPalette:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadBackgroundPaletteLoop:
LDA background_palette, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadBackgroundPaletteLoop ; Branch to LoadBackgroundPaletteLoop if compare was Not Equal to zero
LDX #$00
LoadSpritePaletteLoop:
LDA sprite_palette, x ;load palette byte
STA $2007 ;write to PPU
INX ;set index to next byte
CPX #$10
BNE LoadSpritePaletteLoop ;if x = $10, all done
LDA #%10000000 ; enable NMI, sprites from Pattern Table 0
STA $2000
LDA #%00010000 ; enable sprites
STA $2001
Foreverloop:
JMP Foreverloop ;jump back to Forever, infinite loop
NMI:
RTI
;;;;;;;;;;;;;;
.bank 1
.org $E000
background_palette:
.db $22,$29,$1A,$0F ;background palette 1
.db $22,$36,$17,$0F ;background palette 2
.db $22,$30,$21,$0F ;background palette 3
.db $22,$27,$17,$0F ;background palette 4
sprite_palette:
.db $22,$16,$27,$18 ;sprite palette 1
.db $22,$1A,$30,$27 ;sprite palette 2
.db $22,$16,$30,$27 ;sprite palette 3
.db $22,$0F,$36,$17 ;sprite palette 4
;;;;;;;;;;;;;;
.org $FFFA ;first of the three vectors starts here
.dw NMI ;when an NMI happens (once per frame if enabled) the
;processor will jump to the label NMI:
.dw RESET ;when the processor first turns on or is reset, it will jump
;to the label RESET:
.dw 0 ;external interrupt IRQ is not used in this tutorial
;;;;;;;;;;;;;;
.bank 2
.org $0000
.incbin "mario.chr" ;includes 8KB graphics file from SMB1
Now, in order to access any sprite tiles from the Pattern Table, we need to setup access to register $0200, because registers $0200, $0201, $0202, and $0203 is our first sprite (4 bytes). The next 4 bytes is the 2nd sprite, and so on until $02FF, giving us a total of a possible 64 sprites. In all reality, their definition of a sprite for the NES is just any 8x8 tile. Because some sprites programmed are actually a group of sprites used as one. Like small Mario in Super Mario Bros consists of 4 sprites, and big Mario consists of 8 sprites. What do the 4 registers do you ask? I'm glad you asked!
$0200 - The Y coordinate of the sprite on screen
$0201 - The Tile Index of the sprite from the Pattern Table, allowing you to pick which tile to use for that sprite.
$0202 - The Attribute Table of the sprite. Here's what the binary values represent:
Code:
76543210
||||||||
||||||++- Palette (4 to 7) of sprite
|||+++--- Unimplemented
||+------ Priority (0: in front of background; 1: behind background)
|+------- Flip sprite horizontally
+-------- Flip sprite vertically
$0203 - The X coordinate of the sprite on screen
To setup access to register $0200, well need to write it, little endian style, by writing $00 to the OAM Address register ($2003 write only), and write $02 to the OAM DMA Register ($4014) in the very beginning of the NMI. And believe it or not, since we now enabled the NMI, it'll now fire the NMI sub routine. If you were to write anything such as, for the sake of messing around, changing the background color to black:
Code:
NMI:
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDA #$0F ; change background to black (was blue)
STA $2007
RTI
and you didn't enable NMI in register $2000, this will completely be ignored, and you be stuck in the Foreverloop. Speaking of the loops, did you know that Foreverloop fires every scanline, whereas NMI fires every frame. So you kinda have 2 loops to possibly work with depending on what you wanna do. And like I said in the first tutorial, you don't need to clear the background at all, because the NES does it for you through the NMI (Non Maskable Interrupt). So lets go ahead and have access to the register $0200:
Code:
.inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x 8KB CHR data
.inesmap 0 ; mapper 0 = NROM, no bank swapping
.inesmir 1 ; background mirroring
;;;;;;;;;;;;;;;
.bank 0
.org $C000
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
LDX #$40
STX $4017 ; disable APU frame IRQ
LDX #$FF
TXS ; Set up stack
INX ; now X = 0
STX $2000 ; disable NMI
STX $2001 ; disable rendering
STX $4010 ; disable DMC IRQs
vblankwait1: ; First wait for vblank to make sure PPU is ready
BIT $2002
BPL vblankwait1
clrmem:
LDA #$00
STA $0000, x
STA $0100, x
STA $0400, x
STA $0500, x
STA $0600, x
STA $0700, x
LDA #$FE
STA $0300, x
INX
BNE clrmem
vblankwait2: ; Second wait for vblank, PPU is ready after this
BIT $2002
BPL vblankwait2
LoadPalette:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadBackgroundPaletteLoop:
LDA background_palette, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadBackgroundPaletteLoop ; Branch to LoadBackgroundPaletteLoop if compare was Not Equal to zero
LDX #$00
LoadSpritePaletteLoop:
LDA sprite_palette, x ;load palette byte
STA $2007 ;write to PPU
INX ;set index to next byte
CPX #$10
BNE LoadSpritePaletteLoop ;if x = $10, all done
LDA #%10000000 ; enable NMI, sprites from Pattern Table 0
STA $2000
LDA #%00010000 ; enable sprites
STA $2001
Foreverloop:
JMP Foreverloop ;jump back to Forever, infinite loop
NMI:
LDA #$00
STA $2003 ; set the low byte (00) of the RAM address
LDA #$02
STA $4014 ; set the high byte (02) of the RAM address, start the transfer
RTI
;;;;;;;;;;;;;;
.bank 1
.org $E000
background_palette:
.db $22,$29,$1A,$0F ;background palette 1
.db $22,$36,$17,$0F ;background palette 2
.db $22,$30,$21,$0F ;background palette 3
.db $22,$27,$17,$0F ;background palette 4
sprite_palette:
.db $22,$16,$27,$18 ;sprite palette 1
.db $22,$1A,$30,$27 ;sprite palette 2
.db $22,$16,$30,$27 ;sprite palette 3
.db $22,$0F,$36,$17 ;sprite palette 4
;;;;;;;;;;;;;;
.org $FFFA ;first of the three vectors starts here
.dw NMI ;when an NMI happens (once per frame if enabled) the
;processor will jump to the label NMI:
.dw RESET ;when the processor first turns on or is reset, it will jump
;to the label RESET:
.dw 0 ;external interrupt IRQ is not used in this tutorial
;;;;;;;;;;;;;;
.bank 2
.org $0000
.incbin "mario.chr" ;includes 8KB graphics file from SMB1
Finally the last step, it's time we setup info on $0200, $0201, $0202, and $0203 so we can finally see it! I want to see the top left section of Mario standing still, which is tile number $3A of Pattern Table 0, so let's go ahead and set it up!
$0200 - $00 (Top of the screen)
$0201 - $3A (Top Left section of Mario standing still)
$0202 - $00 (No attributes, using first sprite palette which is number 0)
$0203 - $00 (Left of the screen)
Code:
NMI:
LDA #$00
STA $2003 ; set the low byte (00) of the RAM address
LDA #$02
STA $4014 ; set the high byte (02) of the RAM address, start the transfer
DrawSprite:
LDA #$00 ; Top of the screen
STA $0200 ; Sprite Y Position
LDA #$3A ; Top Left section of Mario standing still
STA $0201 ; Sprite Tile Number
LDA #$00 ; No attributes, using first sprite palette which is number 0
STA $0202 ; Sprite Attributes
LDA #$00 ; Left of the screen.
STA $0203 ; Sprite X Position
RTI
After adding this new section of code over where the NMI is, go ahead and compile it, and run it through the emulator. DOH! Where did it go? Turns out that it is off screen. The REAL top left for the sprite is actually $08 $08. So lets change the 2 values.
Code:
NMI:
LDA #$00
STA $2003 ; set the low byte (00) of the RAM address
LDA #$02
STA $4014 ; set the high byte (02) of the RAM address, start the transfer
DrawSprite:
LDA #$08 ; Top of the screen
STA $0200 ; Sprite Y Position
LDA #$3A ; Top Left section of Mario standing still
STA $0201 ; Sprite Tile Number
LDA #$00 ; No attributes, using first sprite palette which is number 0
STA $0202 ; Sprite Attributes
LDA #$08 ; Left of the screen.
STA $0203 ; Sprite X Position
RTI
Save, compile, and run it again. do you see it?!!! You drew a sprite!!! Congratulations! Lets draw the rest of Mario by using not only 3A, but 37, 4F, and a horizontal mirror of 4F. But, some positions of the sprite tiles will need adjusted or all the tiles will overlap.
Code:
NMI:
LDA #$00
STA $2003 ; set the low byte (00) of the RAM address
LDA #$02
STA $4014 ; set the high byte (02) of the RAM address, start the transfer
DrawSprite:
LDA #$08 ; Top of the screen
STA $0200 ; Sprite 1 Y Position
LDA #$08
STA $0204 ; Sprite 2 Y Position
LDA #$10
STA $0208 ; Sprite 3 Y Position
LDA #$10
STA $020C ; Sprite 4 Y Position
LDA #$3A ; Top Left section of Mario standing still
STA $0201 ; Sprite 1 Tile Number
LDA #$37 ; Top Right section of Mario standing still
STA $0205 ; Sprite 2 Tile Number
LDA #$4F ; Bottom Left section of Mario standing still
STA $0209 ; Sprite 3 Tile Number
LDA #$4F ; Bottom Right section of Mario standing still
STA $020D ; Sprite 4 Tile Number
LDA #$00 ; No attributes, using first sprite palette which is number 0
STA $0202 ; Sprite 1 Attributes
STA $0206 ; Sprite 2 Attributes
STA $020A ; Sprite 3 Attributes
LDA #$40 ; Flip horizontal attribute
STA $020E ; Sprite 4 Attributes
LDA #$08 ; Left of the screen.
STA $0203 ; Sprite 1 X Position
LDA #$10
STA $0207 ; Sprite 2 X Position
LDA #$08
STA $020B ; Sprite 3 X Position
LDA #$10
STA $020F ; Sprite 4 X Position
RTI
Save, compile, and run again. Now little Mario is complete! Next time I'm gonna teach you how to create variables and make Mario move on its own. In the mean time, you can go ahead and download my tutorial. Have a good day!