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

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.

10% of Starfield

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_lost stores $02 to $D020 and sets death_timer to 16.
  • Border stays red forever? The timer decrement must run every frame. Check the death_timer section appears after game_active and before the joystick code.
  • Ship dies immediately after respawn? The invulnerability check must test death_timer before the ship collision loop. BNE must branch to skip_ship_collision, not back into the loop.
  • Border red on restart? init_game must clear $D020 to zero. Check STA $D020 appears 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

  • $D020 for 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.