Results 1 to 1 of 1

Thread: NES 6502 Programming Tutorial - Part 3: Drawing a Sprite

  1. #1

    Thread Starter
    College Grad!!! Jacob Roman's Avatar
    Join Date
    Aug 2004
    Location
    Miami Beach, FL
    Posts
    5,339

    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!
    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
  •  



Click Here to Expand Forum to Full Width