Skip to content
Game 1 Unit 3 of 128 1 hr learning time

Fire Button Shoots

Read the fire button and launch a bullet sprite using zero-page variables — the first step toward a shooting game.

2% of Starfield

Press fire. A yellow bullet appears at the ship and streaks upward. Let go — only one bullet at a time. When it flies off the top, you can fire again.

One button, one new sprite, and a pair of zero-page variables to track the bullet’s state. This is the foundation of every shooting game: spawn a projectile, move it each frame, remove it when it’s done.

Zero Page

The 6510 has a special region of memory called the zero page — addresses $00 to $FF. Instructions that access zero page are one byte shorter and one cycle faster than instructions that access the rest of memory:

InstructionCyclesBytes
LDA $03 (zero page)32
LDA $0400 (absolute)43

That might not sound like much, but in a game loop running 50 times per second, those saved cycles add up. The zero page is where you put variables that get read and written constantly.

Addresses $00 and $01 are reserved by the C64’s hardware. We start our variables at $02:

; Zero-page variables
bullet_active = $02     ; 0 = no bullet, 1 = active
bullet_y      = $03     ; Bullet Y position

; ... (inside initialisation)

        ; Sprite 1 setup (bullet)
        lda #129
        sta $07f9           ; Data pointer (block 129 = $2040)
        lda #$07
        sta $d028           ; Colour (yellow)

        ; Enable sprite 0 only (bullet starts disabled)
        lda #%00000001
        sta $d015

        ; Bullet starts inactive
        lda #$00
        sta bullet_active

bullet_active is a flag: 0 means no bullet on screen, 1 means there is one. bullet_y tracks the bullet’s Y position so we can move it each frame. We also set up sprite 1 during initialisation — its data pointer ($07F9) points to block 129 ($2040), and its colour is yellow ($07). But we don’t enable it yet. The bullet only appears when you press fire.

Reading the Fire Button

You already know $DC00 — the joystick register. Bits 0–3 are directions. Bit 4 is the fire button, and it follows the same active-low pattern: 0 means pressed, 1 means released.

$DC00 — Joystick Port 2 7 7 1 6 6 1 5 5 1 4 Fire active low 0 3 Right active low 1 2 Left active low 1 1 Down active low 1 0 Up active low 1
Fire button pressed: bit 4 is 0 ($EF). Same active-low pattern as the directions.

The check works exactly like the direction checks from Unit 2:

        lda $dc00
        and #%00010000      ; Isolate bit 4
        bne no_fire         ; Bit is 1 = NOT pressed

If the result is zero, fire is pressed.

Spawning the Bullet

Pressing fire isn’t enough — we also need to check that there isn’t already a bullet on screen. Without this guard, holding fire would reset the bullet’s position every frame:

yes no no yes Read $DC00 Fire pressed? Bullet active? Spawn bullet Update bullet
Only spawn a bullet if fire is pressed AND no bullet is currently active.
        ; --- Fire button (bit 4) ---
        lda $dc00
        and #%00010000
        bne no_fire         ; Bit is 1 = NOT pressed

        ; Fire is pressed — only spawn if no bullet active
        lda bullet_active
        bne no_fire         ; Already active, skip

        ; Spawn bullet at ship position
        lda $d000           ; Ship X → bullet X
        sta $d002
        lda $d001           ; Ship Y → bullet Y
        sta bullet_y

        ; Enable sprite 1 (keep sprite 0 enabled)
        lda $d015
        ora #%00000010
        sta $d015

        ; Mark bullet active
        lda #$01
        sta bullet_active

no_fire:

When we spawn the bullet:

  1. Copy the ship’s X position ($D000) to the bullet’s X position ($D002) — the bullet launches from where the ship is
  2. Copy the ship’s Y position ($D001) to bullet_y — this is our zero-page variable, not the sprite register directly
  3. Enable sprite 1 by OR-ing bit 1 into $D015 — this turns on the bullet without affecting sprite 0
  4. Set bullet_active to 1 — so we won’t spawn another bullet until this one is gone

The ORA #%00000010 instruction sets bit 1 of the sprite enable register. It’s the opposite of the AND masking you learnt in Unit 2 — AND clears bits, ORA sets them.

Moving the Bullet

Each frame, if the bullet is active, we subtract 4 from its Y position. When Y drops below 30 (above the visible screen area), we deactivate it:

        ; --- Update bullet ---
        lda bullet_active
        beq no_bullet       ; Not active, skip

        ; Move bullet up (4 pixels per frame)
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003           ; Update sprite 1 Y position

        ; Check if bullet has left the screen (Y < 30)
        cmp #$1e
        bcs no_bullet       ; Y >= 30, still on screen

        ; Deactivate bullet
        lda #$00
        sta bullet_active

        ; Disable sprite 1 (keep sprite 0 enabled)
        lda $d015
        and #%11111101
        sta $d015

no_bullet:

A new instruction here: SBC (SuBtract with Carry). It subtracts a value from the accumulator. The SEC beforehand sets the carry flag — SBC needs this to subtract correctly (it’s the opposite of how ADC needs a clear carry to add).

The CMP #$1E / BCS no_bullet pair checks if Y is still 30 or above. BCS (Branch if Carry Set) branches when the value is greater than or equal to the comparison value. If the bullet has gone above line 30, we clear bullet_active and disable sprite 1 with AND #%11111101 — clearing bit 1 while keeping bit 0 (the ship) enabled.

The Bullet Sprite

The bullet is a simple vertical line — 2 pixels wide, 6 pixels tall, centred in the 24×21 sprite frame:

$00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $18 $00 $00 $18 $00 $00 $18 $00 $00 $18 $00 $00 $18 $00 $00 $18 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00
The bullet sprite — a 2-pixel-wide vertical line centred in the 24×21 frame. Rendered in yellow ($07) to contrast with the white ship.
; Sprite data at $2040 (block 129) — bullet
*= $2040
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

The bullet data lives at $2040 — that’s block 129 (the next 64-byte block after the ship at $2000). The sprite pointer at $07F9 tells the VIC-II where to find sprite 1’s data, just as $07F8 does for sprite 0.

The Complete Code

; Starfield - Unit 3: Fire Button Shoots
; Assemble with: acme -f cbm -o starfield.prg starfield.asm

; ------------------------------------------------
; Zero-page variables
; ------------------------------------------------
bullet_active = $02     ; 0 = no bullet, 1 = active
bullet_y      = $03     ; Bullet Y position

; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Initialisation
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen
        ldx #$00
-       lda #$20
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

        ; Sprite 0 setup (ship)
        lda #128
        sta $07f8           ; Data pointer (block 128 = $2000)
        lda #172
        sta $d000           ; X position
        lda #220
        sta $d001           ; Y position
        lda #$01
        sta $d027           ; Colour (white)

        ; Sprite 1 setup (bullet)
        lda #129
        sta $07f9           ; Data pointer (block 129 = $2040)
        lda #$07
        sta $d028           ; Colour (yellow)

        ; Enable sprite 0 only (bullet starts disabled)
        lda #%00000001
        sta $d015

        ; Bullet starts inactive
        lda #$00
        sta bullet_active

!ifdef SCREENSHOT_MODE {
        ; Place a static bullet for screenshot capture
        ; Set sprite registers directly but leave bullet_active = 0
        ; so the game loop won't move or deactivate it
        lda $d000
        sta $d002           ; Bullet X = ship X
        lda #140
        sta $d003           ; Bullet Y mid-screen
        lda #%00000011
        sta $d015           ; Enable both sprites
}

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
-       lda $d012
        cmp #$ff
        bne -

        ; --- Read joystick and move ship ---

        ; UP (bit 0)
        lda $dc00
        and #%00000001
        bne not_up
        dec $d001
        dec $d001
not_up:

        ; DOWN (bit 1)
        lda $dc00
        and #%00000010
        bne not_down
        inc $d001
        inc $d001
not_down:

        ; LEFT (bit 2)
        lda $dc00
        and #%00000100
        bne not_left
        dec $d000
        dec $d000
not_left:

        ; RIGHT (bit 3)
        lda $dc00
        and #%00001000
        bne not_right
        inc $d000
        inc $d000
not_right:

        ; --- Fire button (bit 4) ---
        lda $dc00
        and #%00010000
        bne no_fire         ; Bit is 1 = NOT pressed

        ; Fire is pressed — only spawn if no bullet active
        lda bullet_active
        bne no_fire         ; Already active, skip

        ; Spawn bullet at ship position
        lda $d000           ; Ship X → bullet X
        sta $d002
        lda $d001           ; Ship Y → bullet Y
        sta bullet_y

        ; Enable sprite 1 (keep sprite 0 enabled)
        lda $d015
        ora #%00000010
        sta $d015

        ; Mark bullet active
        lda #$01
        sta bullet_active

no_fire:

        ; --- Update bullet ---
        lda bullet_active
        beq no_bullet       ; Not active, skip

        ; Move bullet up (4 pixels per frame)
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003           ; Update sprite 1 Y position

        ; Check if bullet has left the screen (Y < 30)
        cmp #$1e
        bcs no_bullet       ; Y >= 30, still on screen

        ; Deactivate bullet
        lda #$00
        sta bullet_active

        ; Disable sprite 1 (keep sprite 0 enabled)
        lda $d015
        and #%11111101
        sta $d015

no_bullet:
        jmp game_loop

; ------------------------------------------------
; Sprite data at $2000 (block 128) — ship
; ------------------------------------------------
*= $2000
        !byte $00,$18,$00   ;        ##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$7e,$00   ;      ######
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $03,$ff,$c0   ;   ############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$e7,$e0   ;  ###..####..###
        !byte $03,$c3,$c0   ;   ##....##....##
        !byte $01,$ff,$80   ;    ##########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$24,$00   ;       #..#
        !byte $00,$00,$00   ;

; ------------------------------------------------
; Sprite data at $2040 (block 129) — bullet
; ------------------------------------------------
*= $2040
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

If It Doesn’t Work

  • No bullet appears? Check that $D015 has bit 1 set when firing, that $07F9 points to block 129, and that bullet_active is being set to 1.
  • Bullet stuck in place? Make sure bullet_y is being decremented and that you’re storing the result to $D003 (sprite 1 Y register).
  • Bullet fires continuously? The bullet_active guard is missing or broken. The LDA bullet_active / BNE no_fire pair should prevent respawning while a bullet is on screen.
  • Bullet appears in the wrong position? The bullet’s X should be copied from $D000 (ship X) to $D002 (bullet X). The Y comes from $D001 via bullet_y.
  • Bullet never disappears? Check the CMP #$1E / BCS logic. The comparison should deactivate the bullet when Y drops below 30.

Try This: Change Bullet Speed

The bullet moves 4 pixels per frame. Change the SBC #$04 value:

  • SBC #$02 — Slow, deliberate shots. Easier to see what’s happening.
  • SBC #$04 — The default. A brisk, arcade-like speed.
  • SBC #$06 — Fast. The bullet zips across the screen in about a second.

Try This: Fire Downward

Replace the bullet update section to make it fire downward:

        ; Move bullet down instead of up
        lda bullet_y
        clc
        adc #$04
        sta bullet_y
        sta $d003

        ; Deactivate when Y > 250
        cmp #$fa
        bcc no_bullet       ; Y < 250, still on screen

ADC (ADd with Carry) adds to the accumulator — the opposite of SBC. CLC clears the carry before adding, just as SEC sets it before subtracting. BCC (Branch if Carry Clear) is the counterpart to BCS.

Ship sprite with bullet firing capability
Ship sprite that can be moved with the joystick
Unit 2: Movement only Unit 3: Fire button shoots

What You’ve Learnt

  • Zero page ($02$FF) — Fast variable storage. One byte shorter, one cycle faster than absolute addressing. Put your most-used variables here.
  • Fire button (bit 4 of $DC00) — Same active-low pattern as directions. 0 = pressed, 1 = released.
  • Sprite 1 — A second hardware sprite with its own position registers ($D002/$D003), colour ($D028), and data pointer ($07F9).
  • Bullet state — An active flag prevents multiple bullets on screen. Check the flag before spawning, clear it when the bullet leaves the screen.
  • Conditional spawning — Two checks in sequence: is fire pressed? Is the bullet inactive? Both must pass to spawn.
  • SBC for movement — Subtract with carry moves the bullet upward. SEC before SBC, just as CLC before ADC.

What’s Next

The bullet fires silently. In Unit 4, you’ll make the SID chip produce a laser sound when the fire button is pressed — your first taste of the C64’s legendary sound hardware.