NES 6502 Programming Tutorial - Part 4: Sprite Movement And Declaring Variables
Hi and welcome back to my awesome NES 6502 programming tutorial. And today I am going to introduce you to two things. Sprite movement, because static sprites are boring as hell, and how to declare variables so we aren't putting up fixed constants. And as a unique twist, I am also going to show you how to load sprites from a database rather than hard-coding everything. Now before we get started, I just realized that drawing a sprite looked like a sloppy mess with this sub routine I wrote for you all:
Code:
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
And you won't believe that it would have looked a hell of a lot better if I just put in a little bit of organization:
Code:
DrawSprite:
;Y Positions
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
;Tile Number From Attribute Table
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
;Sprite Attributes
LDA #%00000000 ; 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 #%01000000 ; Flip horizontal attribute
STA $020E ; Sprite 4 Attributes
;X Positions
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
Although that is much cleaner code (especially where we wrote $40 and changed it to something much cleaner such as #%01000000 to see the horizontal flip attribute bit), I think we should take it a step further and load it from a database. This actually saves wasted cpu cycles and does the same code, only we load from the beginning and draw later on. So we are going to load any of our database stuff over at where our other database stuff was loaded (such as palette data for sprites and the background) at the memory address E000.
Next, just before the RTI, delete the DrawSprite sub and all the code within it. After that, over where you are loading the palettes for the background and sprites, just right after, add this chunk of code to preload sprite data from the database:
Code:
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
LDX #$00 ; start at 0
LoadSpritesLoop:
LDA sprites, x ; load data from address (sprites + x)
STA $0200, x ; store into RAM address ($0200 + x)
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadSpritesLoop ; Branch to LoadSpritesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down
By now your completed code should look like this:
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
LDX #$00 ; start at 0
LoadSpritesLoop:
LDA sprites, x ; load data from address (sprites + x)
STA $0200, x ; store into RAM address ($0200 + x)
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadSpritesLoop ; Branch to LoadSpritesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down
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
sprites:
;vert tile attr horiz
.db $08, $3A, %00000000, $08 ;sprite 0
.db $08, $37, %00000000, $10 ;sprite 1
.db $10, $4f, %00000000, $08 ;sprite 2
.db $10, $4f, %01000000, $10 ;sprite 3
;;;;;;;;;;;;;;
.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
Compile the code with your compile.bat file and run it in your emulator debugger. You should see that it did exactly the same thing, only in less code. And it loaded it from a database! But guess whaaaaaat!!! It's still a static sprite! BOOOORING!!! I'd say let's move it!
So in order to move it, we will need to create a variable over at the address 0000. To do this, you will need to go to the top of your code right after your headers and put in the command .rsset $0000. This is where we can put any variables really. And we are going to create a variable called player_x and reserve 1 byte of space:
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
;;;;;;;;;;;;;;;
;DECLARE SOME VARIABLES HERE
.rsset $0000 ;;start variables at ram location 0
player_x .rs 1 ; .rs 1 means reserve one byte of space
;;;;;;;;;;;;;;;
Next we are going to load info into player_x by storing whatever what was stored into the address $0203 to get the top left X position of the sprite:
Code:
LoadSpritesLoop:
LDA sprites, x ; load data from address (sprites + x)
STA $0200, x ; store into RAM address ($0200 + x)
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadSpritesLoop ; Branch to LoadSpritesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down
LDA #%10000000 ; enable NMI, sprites from Pattern Table 0
STA $2000
LDA #%00010000 ; enable sprites
STA $2001
LDA $0203
STA player_x
Last but not least, in our main NMI loop, we are going to load data from player_x to get the X position, and store it ONLY in $0203 and $020B since they are the left furthest sprites. And since it's the main value to work with, we need to make a copy of this value to the X register by using the command TAX (Transfer A to X, but actually it's making a copy). Then we are going to add 8, and store into $0207 and $020F. All together, this stored the X positions of the sprite. Now to work with the X register by incrementing it with INX, and store the new value into player_x. When you loop it, it moves the entire sprite group across the screen over and over and over since in 6502 assembly, it loops byte values from $00 to $FF over and over.
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
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
INX
STX player_x
RTI
Your completed code should now look like this:
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
;;;;;;;;;;;;;;;
;; DECLARE SOME VARIABLES HERE
.rsset $0000 ;;start variables at ram location 0
player_x .rs 1 ; .rs 1 means reserve one byte of space
;;;;;;;;;;;;;;;
.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
LDX #$00 ; start at 0
LoadSpritesLoop:
LDA sprites, x ; load data from address (sprites + x)
STA $0200, x ; store into RAM address ($0200 + x)
INX ; X = X + 1
CPX #$10 ; Compare X to hex $10, decimal 16
BNE LoadSpritesLoop ; Branch to LoadSpritesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down
LDA #%10000000 ; enable NMI, sprites from Pattern Table 0
STA $2000
LDA #%00010000 ; enable sprites
STA $2001
LDA $0203
STA player_x
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
LDA player_x
STA $0203
STA $020B
TAX
CLC
ADC #$08
STA $0207
STA $020F
INX
STX player_x
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
sprites:
;vert tile attr horiz
.db $08, $3A, %00000000, $08 ;sprite 0
.db $08, $37, %00000000, $10 ;sprite 1
.db $10, $4f, %00000000, $08 ;sprite 2
.db $10, $4f, %01000000, $10 ;sprite 3
;;;;;;;;;;;;;;
.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
Once again, compile the code with compile.bat, and run it into your favorite emulator debugger. SUCCESS!!! Mario now moves right over and over. Congratulations! With the next tutorial, we will add Controller commands so you don't need to watch it move on its own. Enjoy the code and have a wonderful day!