dcsimg
Results 1 to 1 of 1

Thread: NES 6502 Programming Tutorial - Part 4: Sprite Movement And Declaring Variables

  1. #1

    Thread Starter
    Android OpenGL ES Guru Jacob Roman's Avatar
    Join Date
    Aug 2004
    Location
    Miami Beach, FL
    Posts
    5,225

    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.

    Code:
      .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
    
    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!
    Attached Files Attached Files

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Featured


Click Here to Expand Forum to Full Width