Player Death
Ship-enemy collision loops through all three enemies each frame, and a game_over flag freezes the action when the ship is hit.
Three enemies descend, but the ship is untouchable. Not any more. This unit adds ship-enemy collision — and when an enemy reaches the ship, the game is over.
Reusing the Collision Pattern
The bullet-enemy collision from Unit 6 checks Y distance, then X distance, using subtraction and range comparison. Ship-enemy collision is the same pattern — different sprites, same maths.
For each enemy: subtract the enemy’s Y from the ship’s Y. If the result is less than $10 (within 16 pixels positive) or greater than $F0 (within 16 pixels negative/wrapped), the Y coordinates are close enough. Then check X the same way.
Checking All Enemies
The ship collision loop mirrors the bullet collision loop. X steps from 0 to 2, skipping flashing enemies (they’re mid-explosion, not solid). The first hit ends the loop — one collision is enough to end the game.
; --- Check ship-enemy collision ---
ldx #$00
ship_collision_loop:
lda flash_tbl,x
bne next_ship_check ; Skip flashing enemies
; Check Y distance (ship Y vs enemy Y)
lda $d001 ; Ship Y
sec
sbc enemy_y_tbl,x
cmp #$10
bcc check_ship_x
cmp #$f0
bcc next_ship_check
check_ship_x:
; Check X distance (ship X vs enemy X)
lda $d000 ; Ship X
sec
sbc enemy_x_tbl,x
cmp #$10
bcc ship_hit
cmp #$f0
bcc next_ship_check
jmp ship_hit ; >= $F0 = close (negative)
next_ship_check:
inx
cpx #$03
bne ship_collision_loop
jmp no_ship_hit
ship_hit:
; Ship is destroyed
lda #$01
sta game_over
; Turn ship red
lda #$02
sta $d027
; Death sound — SID voice 3 (descending sawtooth)
lda #$00
sta $d40e ; Frequency low
lda #$10
sta $d40f ; Frequency high
lda #$0a
sta $d412 ; Attack=0, Decay=10 (long decay)
lda #$00
sta $d413 ; Sustain=0, Release=0
lda #$20
sta $d411 ; Sawtooth, gate OFF (reset envelope)
lda #$21
sta $d411 ; Sawtooth, gate ON (trigger)
no_ship_hit:
On hit, three things happen: the game_over flag is set to 1, the ship sprite turns red ($02), and SID voice 3 plays a descending sawtooth — a falling tone that contrasts with the sharp noise of enemy explosions.
Freezing the Game
A single byte controls everything. At the top of the game loop, after the raster wait, check game_over. If it’s non-zero, jump straight back to game_loop — no input, no movement, no collision checks. The screen freezes exactly as it was when the ship was hit.
game_loop:
; Wait for the raster beam to reach line 255
- lda $d012
cmp #$ff
bne -
; --- Check game over ---
lda game_over
beq game_active
jmp game_loop ; Game frozen — just keep waiting
game_active:
; ... input, movement, collision code continues here
The raster wait still runs, so the VIC-II keeps displaying. The game isn’t locked up — it’s deliberately frozen. The player sees the red ship, the enemies still visible, and knows what happened.
The Complete Code
; Starfield - Unit 10: Player Death
; 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
score = $07 ; Score (0-99, BCD format)
enemy_x_tbl = $08 ; 3 bytes ($08, $09, $0a)
enemy_y_tbl = $0b ; 3 bytes ($0b, $0c, $0d)
flash_tbl = $0e ; 3 bytes ($0e, $0f, $10)
game_over = $11 ; 0 = playing, 1 = game over
; ------------------------------------------------
; 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)
; Sprites 2, 3, 4 setup (enemies)
lda #130
sta $07fa ; Sprite 2 data pointer
sta $07fb ; Sprite 3 data pointer
sta $07fc ; Sprite 4 data pointer
lda #$05
sta $d029 ; Sprite 2 colour (green)
sta $d02a ; Sprite 3 colour (green)
sta $d02b ; Sprite 4 colour (green)
; Spawn three enemies at staggered heights
lda #$32 ; Top
ldx #$00
jsr spawn_enemy
lda #$82 ; Middle
ldx #$01
jsr spawn_enemy
lda #$d2 ; Lower
ldx #$02
jsr spawn_enemy
; Enable sprites 0, 2, 3, 4 (bullet starts disabled)
lda #%00011101
sta $d015
; Bullet starts inactive
lda #$00
sta bullet_active
; Game starts in playing state
lda #$00
sta game_over
; Score starts at zero (BCD)
lda #$00
sta score
; Write "00" to screen RAM (top-left corner)
lda #$30 ; Screen code for '0'
sta $0400 ; Tens digit (row 0, col 0)
sta $0401 ; Ones digit (row 0, col 1)
; Set score colour to white
lda #$01
sta $d800
sta $d801
; 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 {
; Set a visible score
lda #$07
sta score
lda #$30
sta $0400
lda #$37
sta $0401
; Ship is dead — turn red
lda #$02
sta $d027
lda #$01
sta game_over
; Position enemy 1 near the ship (just collided)
lda #200
sta enemy_y_tbl+1
lda $d000
sta enemy_x_tbl+1
ldy #$06
lda enemy_x_tbl+1
sta $d000,y
lda enemy_y_tbl+1
sta $d001,y
; Enable sprites (ship + 3 enemies, no bullet)
lda #%00011101
sta $d015
}
; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
; Wait for the raster beam to reach line 255
- lda $d012
cmp #$ff
bne -
; --- Check game over ---
lda game_over
beq game_active
jmp game_loop ; Game frozen — just keep waiting
game_active:
; --- 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
bne check_collision
jmp no_hit
check_collision:
ldx #$00
collision_loop:
lda flash_tbl,x
bne next_collision ; Skip flashing enemies
; Check Y distance
lda bullet_y
sec
sbc enemy_y_tbl,x
cmp #$10
bcc check_x
cmp #$f0
bcc next_collision
check_x:
; Check X distance
lda $d002 ; Bullet X (sprite 1)
sec
sbc enemy_x_tbl,x
cmp #$10
bcc hit_enemy
cmp #$f0
bcc next_collision
jmp hit_enemy ; >= $F0 = close (negative)
next_collision:
inx
cpx #$03
bne collision_loop
jmp no_hit
hit_enemy:
; X = index of hit enemy
; Deactivate bullet
lda #$00
sta bullet_active
lda $d015
and #%11111101
sta $d015
; Flash this enemy white
lda #$08
sta flash_tbl,x
ldy sprite_colour_off,x
lda #$01
sta $d000,y ; White
; 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)
; Increment score (BCD)
sed ; Decimal mode
lda score
clc
adc #$01
sta score
cld ; Back to binary mode
; Update score display — tens digit
lda score
lsr
lsr
lsr
lsr ; High nybble in A (0-9)
clc
adc #$30 ; Convert to screen code
sta $0400 ; Write tens digit
; Update score display — ones digit
lda score
and #$0f ; Low nybble in A (0-9)
clc
adc #$30 ; Convert to screen code
sta $0401 ; Write ones digit
no_hit:
; --- Update all enemies ---
ldx #$00
enemy_loop:
lda flash_tbl,x
bne do_flash
; Move down 1 pixel per frame
lda enemy_y_tbl,x
clc
adc #$01
sta enemy_y_tbl,x
; Off-screen check (Y >= 248)
cmp #$f8
bcc update_sprite
; Respawn at top
lda #$32
jsr spawn_enemy
jmp next_enemy
do_flash:
dec flash_tbl,x
bne update_sprite ; Still flashing
; Flash done — respawn
lda #$32
jsr spawn_enemy
jmp next_enemy
update_sprite:
; Copy position to VIC-II
ldy sprite_pos_off,x
lda enemy_x_tbl,x
sta $d000,y
lda enemy_y_tbl,x
sta $d001,y
next_enemy:
inx
cpx #$03
bne enemy_loop
; --- Check ship-enemy collision ---
ldx #$00
ship_collision_loop:
lda flash_tbl,x
bne next_ship_check ; Skip flashing enemies
; Check Y distance (ship Y vs enemy Y)
lda $d001 ; Ship Y
sec
sbc enemy_y_tbl,x
cmp #$10
bcc check_ship_x
cmp #$f0
bcc next_ship_check
check_ship_x:
; Check X distance (ship X vs enemy X)
lda $d000 ; Ship X
sec
sbc enemy_x_tbl,x
cmp #$10
bcc ship_hit
cmp #$f0
bcc next_ship_check
jmp ship_hit ; >= $F0 = close (negative)
next_ship_check:
inx
cpx #$03
bne ship_collision_loop
jmp no_ship_hit
ship_hit:
; Ship is destroyed
lda #$01
sta game_over
; Turn ship red
lda #$02
sta $d027
; Death sound — SID voice 3 (descending sawtooth)
lda #$00
sta $d40e ; Frequency low
lda #$10
sta $d40f ; Frequency high
lda #$0a
sta $d412 ; Attack=0, Decay=10 (long decay)
lda #$00
sta $d413 ; Sustain=0, Release=0
lda #$20
sta $d411 ; Sawtooth, gate OFF (reset envelope)
lda #$21
sta $d411 ; Sawtooth, gate ON (trigger)
no_ship_hit:
jmp game_loop
; ------------------------------------------------
; Subroutine: spawn_enemy
; A = starting Y position, X = enemy index
; ------------------------------------------------
spawn_enemy:
sta enemy_y_tbl,x
; Random X from raster
lda $d012
and #$7f
clc
adc #$30
sta enemy_x_tbl,x
; Clear flash timer
lda #$00
sta flash_tbl,x
; Restore sprite colour to green
ldy sprite_colour_off,x
lda #$05
sta $d000,y
; Update VIC-II sprite position
ldy sprite_pos_off,x
lda enemy_x_tbl,x
sta $d000,y ; Sprite X
lda enemy_y_tbl,x
sta $d001,y ; Sprite Y (offset + 1)
rts
; ------------------------------------------------
; Lookup tables
; ------------------------------------------------
sprite_pos_off:
!byte $04, $06, $08 ; VIC-II X-position offsets for sprites 2, 3, 4
sprite_colour_off:
!byte $29, $2a, $2b ; VIC-II colour register offsets
; ------------------------------------------------
; 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
- Ship never dies? Check the collision loop runs after the enemy update loop. The ship position must be read from
$D000/$D001, not from zero-page variables. - Game freezes immediately? Make sure
game_overis initialised to 0 in the init section. An uninitialised zero-page byte might be non-zero. - Wrong enemy triggers death? The loop must skip flashing enemies — they’re mid-explosion and shouldn’t count as solid.
- No death sound? Check voice 3 registers (
$D40E–$D413). The gate must go off then on to trigger the envelope. - Ship doesn’t turn red? Sprite 0’s colour register is
$D027. Write$02(red) after setting the game_over flag.
Try This: Death Animation
Instead of an instant freeze, flash the ship between red and white for 16 frames before setting game_over. Use a death_timer — the same pattern as the enemy flash timer. The game continues running during the flash, giving the player a moment to see what hit them.
Try This: Border Flash
Set $D020 to red on death. A single STA turns the entire border red — a dramatic whole-screen effect that screams danger.
What You’ve Learnt
- Reusing collision patterns — the same distance check works for any two sprites.
- Game state flag — one byte controls whether the entire game loop runs.
- Looped collision — checking one sprite against a table of others with indexed addressing.
- SID voice 3 — the third independent voice, used here for a death sound distinct from laser and explosion.
What’s Next
The game freezes on death, but there’s no message and no way to try again. In Unit 11, “GAME OVER” appears on screen and pressing fire restarts the game.