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.
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:
| Register | Purpose |
|---|---|
$D407 | Frequency low byte |
$D408 | Frequency high byte |
$D40B | Control register (waveform + gate) |
$D40C | Attack / Decay |
$D40D | Sustain / 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
$D418volume is$0F(set during init) and that voice 2’s gate is toggled —$80then$81written to$D40B. If you only write$81, the envelope may not retrigger. - Flash doesn’t show? Make sure
$D029is set to$01in the hit block and restored to$05when the timer expires. - Enemy keeps moving during flash? The
flash_timercheck 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.