Life Lost Flash
The border flashes red when the player loses a life. A death timer counts down frames of invulnerability, preventing instant re-death while the screen recovers.
Losing a life should feel like something happened. Right now the ship just teleports back to its starting position — no flash, no pause, no feedback. This unit adds a red border flash that lasts 16 frames, and while the border is red, the ship can’t be hit again.
The Death Timer
A new zero-page variable:
death_timer = $13 ; Death flash countdown (0 = no flash)
When the player loses a life (but the game isn’t over), death_timer is set to 16. Each frame, the game loop decrements it. When it reaches zero, the border returns to black. Same pattern as flash_tbl for enemy explosions — a countdown that drives a visual effect.
The Border as Feedback
The VIC-II border colour register at $D020 controls the entire border area surrounding the screen. One store to $D020 and the whole frame changes colour — the most dramatic single-byte visual effect on the C64.
On death: LDA #$02 / STA $D020 turns the border red. When the timer expires: LDA #$00 / STA $D020 restores it to black. The flash is visible for exactly 16 frames — about a third of a second at PAL timing.
; --- Death timer (invulnerability flash) ---
lda death_timer
beq no_death_flash ; Timer not running
dec death_timer
bne no_death_flash ; Still counting down
; Timer just expired — restore border to black
lda #$00
sta $d020
no_death_flash:
The timer check runs every frame, right at the start of the game logic. BEQ skips the entire block when the timer is zero — three instructions of overhead during normal play. When the timer is running, DEC subtracts one and BNE skips the border restore unless the timer just hit zero.
Invulnerability During Flash
Without protection, the ship would die again immediately — the enemy that killed it is still right there. The death timer doubles as an invulnerability window. Before checking ship-enemy collisions, one test decides whether to skip the entire collision loop.
; --- Check ship-enemy collision ---
lda death_timer
bne skip_ship_collision ; Invulnerable during flash
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
next_ship_check:
inx
cpx #$03
bne ship_collision_loop
skip_ship_collision:
jmp game_loop
LDA death_timer / BNE skip_ship_collision — if the timer is running, branch past the entire collision loop to JMP game_loop. The ship still moves, bullets still fire, enemies still descend. Only the collision check is disabled.
The label skip_ship_collision sits right after the loop’s natural end. When the loop finishes without finding a hit, it falls through to the same JMP game_loop. One label, two paths to it.
The Complete Code
; Starfield - Unit 13: Life Lost Flash
; 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_state = $11 ; 0 = playing, 1 = game over
lives = $12 ; Lives remaining (starts at 3)
death_timer = $13 ; Death flash countdown (0 = no flash)
; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
; ------------------------------------------------
; One-time hardware setup
; ------------------------------------------------
*= $080d
; Black screen
lda #$00
sta $d020 ; Border colour
sta $d021 ; Background colour
; Sprite 1 colour (yellow, never changes)
lda #$07
sta $d028
; Set score colour to white (persists across restarts)
lda #$01
sta $d800
sta $d801
; Set lives colour to white (persists across restarts)
sta $d827
; 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
; First-time init
jsr clear_screen
jsr init_game
!ifdef SCREENSHOT_MODE {
; Set a visible score
lda #$05
sta score
lda #$30
sta $0400
lda #$35
sta $0401
; Lives at 2 (lost one life)
lda #$02
sta lives
lda #$32 ; '2'
sta $0427
; Death flash active (border red, invulnerable)
lda #$08
sta death_timer
lda #$02
sta $d020 ; Border red
; Position bullet mid-screen
lda $d000
sta $d002
lda #140
sta $d003
; Enable all sprites (ship + bullet + 3 enemies)
lda #%00011111
sta $d015
; Freeze the game (prevents enemies moving during capture)
lda #$01
sta game_state
}
; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
; Wait for the raster beam to reach line 255
- lda $d012
cmp #$ff
bne -
; --- Check game state ---
lda game_state
beq game_active
; --- Game over: poll fire button ---
lda $dc00
and #%00010000
bne game_loop ; Not pressed, keep waiting
; Fire pressed — restart the game
jsr clear_screen
jsr init_game
jmp game_loop
game_active:
; --- Death timer (invulnerability flash) ---
lda death_timer
beq no_death_flash ; Timer not running
dec death_timer
bne no_death_flash ; Still counting down
; Timer just expired — restore border to black
lda #$00
sta $d020
no_death_flash:
; --- 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 ---
lda death_timer
bne skip_ship_collision ; Invulnerable during flash
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
skip_ship_collision:
jmp game_loop
ship_hit:
; --- Decrement lives ---
dec lives
; Update lives display
lda lives
clc
adc #$30
sta $0427
; Check if lives exhausted
lda lives
bne life_lost
; --- No lives left — game over ---
lda #$01
sta game_state
; Turn ship red
lda #$02
sta $d027
; Restore border to black (clear any death flash)
lda #$00
sta $d020
sta death_timer
; Show GAME OVER text
jsr show_game_over
jmp play_death_sound
life_lost:
; --- Lives remaining — reset ship position ---
lda #172
sta $d000
lda #220
sta $d001
; Start death flash (invulnerability)
lda #16
sta death_timer
; Border flash red
lda #$02
sta $d020
play_death_sound:
; 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)
jmp game_loop
; ------------------------------------------------
; Subroutine: clear_screen
; Fills screen RAM with spaces ($20)
; ------------------------------------------------
clear_screen:
ldx #$00
- lda #$20
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne -
rts
; ------------------------------------------------
; Subroutine: init_game
; Resets all game state for a new game
; ------------------------------------------------
init_game:
; Sprite data pointers (must be set after clear_screen)
lda #128
sta $07f8 ; Ship (block 128 = $2000)
lda #129
sta $07f9 ; Bullet (block 129 = $2040)
lda #130
sta $07fa ; Enemy 0 (block 130 = $2080)
sta $07fb ; Enemy 1
sta $07fc ; Enemy 2
; Ship position and colour
lda #172
sta $d000 ; X position
lda #220
sta $d001 ; Y position
lda #$01
sta $d027 ; Colour (white)
; Enemy colours
lda #$05
sta $d029
sta $d02a
sta $d02b
; 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
; Reset state
lda #$00
sta bullet_active
sta game_state
sta score
sta death_timer
sta $d020 ; Border black (clear any death flash)
; Score display
lda #$30
sta $0400
sta $0401
; Lives
lda #$03
sta lives
lda #$33 ; Screen code for '3'
sta $0427
rts
; ------------------------------------------------
; Subroutine: show_game_over
; Writes "GAME OVER" to screen RAM, row 12, col 16
; ------------------------------------------------
show_game_over:
; Screen codes: G=7, A=1, M=13, E=5, space=32, O=15, V=22, E=5, R=18
; Position: row 12 x 40 + col 16 = 496 -> $0400 + $01F0 = $05F0
lda #$07 ; G
sta $05f0
lda #$01 ; A
sta $05f1
lda #$0d ; M
sta $05f2
lda #$05 ; E
sta $05f3
lda #$20 ; (space)
sta $05f4
lda #$0f ; O
sta $05f5
lda #$16 ; V
sta $05f6
lda #$05 ; E
sta $05f7
lda #$12 ; R
sta $05f8
; Colour to white
lda #$01
sta $d9f0
sta $d9f1
sta $d9f2
sta $d9f3
sta $d9f4
sta $d9f5
sta $d9f6
sta $d9f7
sta $d9f8
rts
; ------------------------------------------------
; 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
- Border never flashes? Check
life_loststores$02to$D020and setsdeath_timerto 16. - Border stays red forever? The timer decrement must run every frame. Check the
death_timersection appears aftergame_activeand before the joystick code. - Ship dies immediately after respawn? The invulnerability check must test
death_timerbefore the ship collision loop.BNEmust branch toskip_ship_collision, not back into the loop. - Border red on restart?
init_gamemust clear$D020to zero. CheckSTA $D020appears in the reset section. - Flash too short or too long? The timer starts at 16 (about 0.3 seconds at PAL). Increase for longer invulnerability, decrease for shorter.
Try This: Flash the Border Rapidly
Instead of solid red for 16 frames, alternate between red and black. Test bit 0 of death_timer: LDA death_timer / AND #$01 / BEQ to choose red or black. The border strobes — more urgent, more dramatic.
Try This: Change Ship Colour During Flash
Set the ship sprite colour to red ($D027 = $02) when the timer starts. Restore it to white when the timer expires. Visual feedback on the ship itself, not just the border.
What You’ve Learnt
$D020for visual feedback — the border colour register is the simplest whole-screen effect.- Frame-counting timers — decrement each frame, act when zero. The same pattern used for enemy flash timers.
- Invulnerability windows — one branch instruction skips an entire collision loop, protecting the player during recovery.
What’s Next
Black space is empty space. In Unit 14, characters written to screen RAM become a scrolling starfield — the game gets its name, and the background finally has depth.