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

Explosion

The SID chip fires a noise burst on voice 2 while the enemy flashes white — eight frames of audio-visual feedback before the respawn.

5% of Starfield

The enemy doesn’t just vanish — it flashes white and the SID fires a burst of noise. Eight frames of feedback, then the enemy respawns. Every hit feels like it matters.

Two Voices

The SID has three independent voices. Voice 1 handles the laser sound from Unit 4. Voice 2 now handles explosions. Each voice has its own frequency registers, ADSR envelope, and waveform selector — they play simultaneously without interfering.

Voice 2’s registers sit seven bytes above voice 1:

RegisterPurpose
$D407Frequency low byte
$D408Frequency high byte
$D40BControl register (waveform + gate)
$D40CAttack / Decay
$D40DSustain / Release

The control register at $D40B works the same way as voice 1’s $D404. Bit 0 is the gate (on/off). Bits 4–7 select the waveform. For explosions, we want noise — bit 7. So $80 means noise with gate off, and $81 means noise with gate on.

The Hit Response

hit_enemy:
        ; Deactivate bullet
        lda #$00
        sta bullet_active
        lda $d015
        and #%11111101          ; Disable sprite 1 (bullet)
        sta $d015

        ; Flash enemy white
        lda #$01
        sta $d029               ; Sprite 2 colour = white

        ; Start flash timer
        lda #$08
        sta flash_timer         ; 8 frames of flash

        ; Explosion sound — SID voice 2 (noise)
        lda #$00
        sta $d407               ; Frequency low
        lda #$08
        sta $d408               ; Frequency high (low rumble)
        lda #$09
        sta $d40c               ; Attack=0, Decay=9
        lda #$00
        sta $d40d               ; Sustain=0, Release=0
        lda #$80
        sta $d40b               ; Noise, gate OFF (reset envelope)
        lda #$81
        sta $d40b               ; Noise, gate ON (trigger)

Three things happen when a bullet hits the enemy.

Deactivate the bullet. Same as Unit 6 — clear bullet_active and disable sprite 1.

Flash the enemy white. Write $01 (white) to $D029, the sprite 2 colour register. Then set flash_timer to 8 — the enemy will stay white for 8 frames.

Trigger the explosion sound. Voice 2 gets a low frequency ($0800) for a bass rumble. The ADSR has zero attack and a moderate decay ($09) — the sound starts loud and fades quickly. The noise waveform is selected by setting bit 7 of $D40B. We write $80 first (gate off) to reset the envelope, then $81 (gate on) to trigger it — the same off-then-on pattern we used for the laser in Unit 4.

No respawn happens here. The enemy freezes in place during the flash.

Timing the Flash

        ; --- Update enemy ---
        lda flash_timer
        beq enemy_move          ; Not flashing, normal movement

        ; Enemy is frozen (flashing white)
        dec flash_timer
        bne no_respawn          ; Still flashing, skip everything

        ; Flash done — restore colour and respawn
        lda #$05
        sta $d029               ; Back to green
        lda #$32
        sta enemy_y
        sta $d005
        lda $d012
        and #$7f
        clc
        adc #$30
        sta enemy_x
        sta $d004
        jmp no_respawn

enemy_move:
        lda enemy_y
        clc
        adc #$01                ; Move down 1 pixel per frame
        sta enemy_y
        sta $d005               ; Update sprite 2 Y position

        ; Check if enemy has left the screen (Y > 248)
        cmp #$f8
        bcc no_respawn          ; Y < 248, still on screen

        ; Respawn at top with new X position
        lda #$32
        sta enemy_y
        sta $d005               ; Reset Y to top

        lda $d012               ; Pseudo-random from raster position
        and #$7f                ; Range 0-127
        clc
        adc #$30                ; Range 48-175
        sta enemy_x
        sta $d004               ; New X position

no_respawn:
        jmp game_loop

The enemy update section now checks flash_timer before doing anything else. If it’s zero, the enemy moves normally — same code as Unit 6. If it’s non-zero, the enemy is frozen mid-explosion.

Each frame, flash_timer decrements by one. While it’s still positive, we skip straight to the end of the loop — no movement, no off-screen check. The enemy just sits there, white.

When the timer reaches zero, we restore the colour to green ($05) and respawn the enemy at the top with a new random X position. Then normal movement resumes.

This frame counter pattern — set a value, decrement each frame, act when it reaches zero — is one you’ll use constantly. Invulnerability windows, animation timers, delayed events. It’s the simplest form of game timing.

The Complete Code

; Starfield - Unit 7: Explosion
; 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
enemy_x       = $04     ; Enemy X position
enemy_y       = $05     ; Enemy Y position
flash_timer   = $06     ; Explosion flash countdown (0 = no flash)

; ------------------------------------------------
; 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)

        ; Sprite 2 setup (enemy)
        lda #130
        sta $07fa           ; Data pointer (block 130 = $2080)
        lda #$05
        sta $d029           ; Colour (green)

        ; Enemy starting position
        lda #100
        sta enemy_x
        sta $d004           ; X position
        lda #$32
        sta enemy_y
        sta $d005           ; Y position

        ; Enable sprites 0 and 2 (bullet starts disabled)
        lda #%00000101
        sta $d015

        ; Bullet starts inactive
        lda #$00
        sta bullet_active

        ; Flash timer starts at zero (no flash)
        lda #$00
        sta flash_timer

        ; SID setup — voice 1 laser sound
        lda #$0f
        sta $d418           ; Volume to maximum

        lda #$00
        sta $d400           ; Frequency low byte
        lda #$10
        sta $d401           ; Frequency high byte ($1000 = mid-high pitch)

        lda #$09
        sta $d405           ; Attack=0, Decay=9
        lda #$00
        sta $d406           ; Sustain=0, Release=0

!ifdef SCREENSHOT_MODE {
        lda $d000
        sta $d002           ; Bullet X = ship X
        lda #140
        sta $d003           ; Bullet Y mid-screen

        ; Show enemy flashing white (explosion in progress)
        lda #$01
        sta $d029           ; White

        lda #%00000111
        sta $d015           ; Enable all three 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 other sprites enabled)
        lda $d015
        ora #%00000010
        sta $d015

        ; Mark bullet active
        lda #$01
        sta bullet_active

        ; Trigger laser sound (gate off then on to retrigger)
        lda #$20
        sta $d404           ; Sawtooth, gate OFF (reset envelope)
        lda #$21
        sta $d404           ; Sawtooth, gate ON (start sound)

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 other sprites enabled)
        lda $d015
        and #%11111101
        sta $d015

no_bullet:

        ; --- Check bullet-enemy collision ---
        lda bullet_active
        beq no_hit              ; No bullet, skip

        ; Check Y distance
        lda bullet_y
        sec
        sbc enemy_y
        cmp #$10                ; Within 16 pixels? (positive direction)
        bcc y_close
        cmp #$f0                ; Within 16 pixels? (negative/wrapped)
        bcc no_hit              ; Too far apart
y_close:
        ; Check X distance
        lda $d002               ; Bullet X
        sec
        sbc enemy_x
        cmp #$10                ; Within 16 pixels? (positive direction)
        bcc hit_enemy
        cmp #$f0                ; Within 16 pixels? (negative/wrapped)
        bcc no_hit              ; Too far apart

hit_enemy:
        ; Deactivate bullet
        lda #$00
        sta bullet_active
        lda $d015
        and #%11111101          ; Disable sprite 1 (bullet)
        sta $d015

        ; Flash enemy white
        lda #$01
        sta $d029               ; Sprite 2 colour = white

        ; Start flash timer
        lda #$08
        sta flash_timer         ; 8 frames of flash

        ; Explosion sound — SID voice 2 (noise)
        lda #$00
        sta $d407               ; Frequency low
        lda #$08
        sta $d408               ; Frequency high (low rumble)
        lda #$09
        sta $d40c               ; Attack=0, Decay=9
        lda #$00
        sta $d40d               ; Sustain=0, Release=0
        lda #$80
        sta $d40b               ; Noise, gate OFF (reset envelope)
        lda #$81
        sta $d40b               ; Noise, gate ON (trigger)

no_hit:

        ; --- Update enemy ---
        lda flash_timer
        beq enemy_move          ; Not flashing, normal movement

        ; Enemy is frozen (flashing white)
        dec flash_timer
        bne no_respawn          ; Still flashing, skip everything

        ; Flash done — restore colour and respawn
        lda #$05
        sta $d029               ; Back to green
        lda #$32
        sta enemy_y
        sta $d005
        lda $d012
        and #$7f
        clc
        adc #$30
        sta enemy_x
        sta $d004
        jmp no_respawn

enemy_move:
        lda enemy_y
        clc
        adc #$01                ; Move down 1 pixel per frame
        sta enemy_y
        sta $d005               ; Update sprite 2 Y position

        ; Check if enemy has left the screen (Y > 248)
        cmp #$f8
        bcc no_respawn          ; Y < 248, still on screen

        ; Respawn at top with new X position
        lda #$32
        sta enemy_y
        sta $d005               ; Reset Y to top

        lda $d012               ; Pseudo-random from raster position
        and #$7f                ; Range 0-127
        clc
        adc #$30                ; Range 48-175
        sta enemy_x
        sta $d004               ; New X position

no_respawn:
        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   ;

; ------------------------------------------------
; Sprite data at $2080 (block 130) — enemy
; ------------------------------------------------
*= $2080
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $01,$7e,$80   ;    #.######.#
        !byte $01,$3c,$80   ;    #..####..#
        !byte $00,$a5,$00   ;     #.#..#.#
        !byte $01,$81,$80   ;    ##......##
        !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   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

If It Doesn’t Work

  • No explosion sound? Check $D418 volume is $0F (set during init) and that voice 2’s gate is toggled — $80 then $81 written to $D40B. If you only write $81, the envelope may not retrigger.
  • Flash doesn’t show? Make sure $D029 is set to $01 in the hit block and restored to $05 when the timer expires.
  • Enemy keeps moving during flash? The flash_timer check must come before the normal movement code. If you put it after, the enemy moves for one frame before freezing.
  • Sound plays but wrong? Make sure you’re writing to $D407$D40D (voice 2), not $D400$D406 (voice 1). Easy mistake — the registers look identical, just offset by 7.

Try This: Different Explosion Sounds

Change voice 2’s frequency and envelope to hear different effects:

  • $D408 = $02 — Very low. Subterranean rumble.
  • $D408 = $18 — Higher. More of a crackle.
  • $D40C = $0F — Longer decay. The explosion lingers.
  • $D40C = $03 — Short decay. A sharp pop.

Try This: Longer Flash

Change flash_timer from 8 to 16. Or try 4 for a quick blink. At 50 fps (PAL), 8 frames is about 160ms — just long enough to notice.

What You’ve Learnt

  • SID voice 2 — an independent sound channel with its own registers, offset 7 bytes from voice 1.
  • Noise waveform — bit 7 of the control register. Good for explosions, static, percussion.
  • Frame counter — set a value, decrement each frame, act when it reaches zero. The simplest game timer.
  • Timed visual effects — temporary colour change with a countdown, frozen movement during the effect.

What’s Next

The enemy explodes but there’s no record of the carnage. In Unit 8, screen RAM gives you a score display — proof of every hit.