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

Title Screen

The game starts with a title screen. The state machine grows from two states to three — title, playing, and game over — with stars scrolling behind everything.

12% of Starfield

The game launches straight into action — no introduction, no context. This unit adds a title screen that greets the player before the first enemy appears. The state machine that already handles playing and game over gains a third state: title.

A Third State

Until now, game_state held two values: 0 for playing, 1 for game over. A third state means renumbering:

; game_state: 0 = title, 1 = playing, 2 = game over

Title is state 0 because it’s the starting state — no initialisation needed to reach it. The game boots into title, transitions to playing when fire is pressed, shifts to game over on the last death, and returns to title when fire is pressed again. A cycle with three stops.

State Dispatch

The game loop checks game_state and branches to the right handler. BEQ catches state 0 (title). CMP #$02 / BEQ catches state 2 (game over). Everything else falls through to gameplay via JMP game_active.

        ; --- State dispatch ---
        lda game_state
        beq title_state         ; 0 = title
        cmp #$02
        beq game_over_state     ; 2 = game over
        jmp game_active         ; 1 = playing

title_state:
        ; Redraw title text (repairs any star damage)
        jsr show_title

        ; Poll fire button
        lda $dc00
        and #%00010000
        bne state_done          ; Not pressed

        ; Fire pressed — start the game
        jsr clear_screen
        jsr init_game
        jmp game_loop

game_over_state:
        ; Redraw game over text (repairs any star damage)
        jsr show_game_over

        ; Poll fire button
        lda $dc00
        and #%00010000
        bne state_done          ; Not pressed

        ; Fire pressed — return to title
        jsr clear_screen
        jsr init_title
        jmp game_loop

state_done:
        jmp game_loop

Each non-playing state polls the fire button. When pressed, it clears the screen and calls the appropriate init routine — init_game to start playing, init_title to return to the title. The state_done label is a shared exit point: one JMP game_loop that both states branch to when fire isn’t pressed.

The Title Screen

show_title writes two lines of text to screen RAM using screen codes — the same technique as show_game_over, just more characters. “STARFIELD” in white at row 10. “PRESS FIRE” in light grey at row 14.

; Subroutine: show_title
; Writes "STARFIELD" and "PRESS FIRE" to screen RAM
show_title:
        ; "STARFIELD" at row 10, col 16 ($05A0)
        lda #$13            ; S
        sta $05a0
        lda #$14            ; T
        sta $05a1
        lda #$01            ; A
        sta $05a2
        lda #$12            ; R
        sta $05a3
        lda #$06            ; F
        sta $05a4
        lda #$09            ; I
        sta $05a5
        lda #$05            ; E
        sta $05a6
        lda #$0c            ; L
        sta $05a7
        lda #$04            ; D
        sta $05a8

        ; Colour to white
        lda #$01
        sta $d9a0
        sta $d9a1
        sta $d9a2
        sta $d9a3
        sta $d9a4
        sta $d9a5
        sta $d9a6
        sta $d9a7
        sta $d9a8

        ; "PRESS FIRE" at row 14, col 15 ($063F)
        lda #$10            ; P
        sta $063f
        lda #$12            ; R
        sta $0640
        lda #$05            ; E
        sta $0641
        lda #$13            ; S
        sta $0642
        lda #$13            ; S
        sta $0643
        lda #$20            ; (space)
        sta $0644
        lda #$06            ; F
        sta $0645
        lda #$09            ; I
        sta $0646
        lda #$12            ; R
        sta $0647
        lda #$05            ; E
        sta $0648

        ; Colour to light grey
        lda #$0f
        sta $da3f
        sta $da40
        sta $da41
        sta $da42
        sta $da43
        sta $da44
        sta $da45
        sta $da46
        sta $da47
        sta $da48

        rts

Screen codes map letters starting from 1: A=$01, B=$02, through to Z=$1A. The address calculation is the same as before: row number times 40, plus the column, plus $0400. Colour RAM mirrors the position with a base of $D800.

Stars Behind Everything

Stars now update in every state — not just during gameplay. The star loop runs before the state dispatch, so the starfield scrolls behind the title text, behind the game, and behind the game over message.

When a star drifts across a title character, it briefly overwrites it. The fix is simple: show_title redraws the text every frame during title state. Any star damage is repaired before the frame is drawn. The same approach applies to show_game_over during the game over state.

Init Routines

Two init routines handle the two entry points:

init_title sets up the title screen — disables all sprites, draws the stars, shows the title text, and sets game_state to 0.

init_game sets up gameplay — positions the ship, spawns enemies, enables sprites, resets the score and lives, draws the stars, and sets game_state to 1.

Both call a shared init_stars subroutine that resets star positions and draws them. One piece of star setup code, two callers.

The Complete Code

; Starfield - Unit 15: Title Screen
; 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 = title, 1 = playing, 2 = game over
lives          = $12   ; Lives remaining (starts at 3)
death_timer    = $13   ; Death flash countdown (0 = no flash)
star_row       = $14   ; 8 bytes ($14-$1b) — row 0-24
star_col       = $1c   ; 8 bytes ($1c-$23) — column 0-39
frame_count    = $24   ; Frame counter for parallax timing

; $fb-$fc: temporary pointer (used by star routines)

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

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

        ; Start on the title screen
        jsr clear_screen
        jsr init_title

!ifdef SCREENSHOT_MODE {
        ; Title screen is the default state — no modifications needed
}

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
-       lda $d012
        cmp #$ff
        bne -

        ; --- Update stars (all states) ---
        inc frame_count

        ldx #$00
star_loop:
        jsr erase_star

        ; Check if star should move this frame
        cpx #$04
        bcc move_star           ; Close stars (0-3) always move

        ; Distant star — only move on odd frames
        lda frame_count
        and #$01
        beq skip_move           ; Even frame, don't move

move_star:
        inc star_row,x
        lda star_row,x
        cmp #25
        bcc skip_move

        ; Wrap to row 0
        lda #$00
        sta star_row,x

skip_move:
        jsr draw_star

        inx
        cpx #$08
        bne star_loop

        ; --- State dispatch ---
        lda game_state
        beq title_state         ; 0 = title
        cmp #$02
        beq game_over_state     ; 2 = game over
        jmp game_active         ; 1 = playing

title_state:
        ; Redraw title text (repairs any star damage)
        jsr show_title

        ; Poll fire button
        lda $dc00
        and #%00010000
        bne state_done          ; Not pressed

        ; Fire pressed — start the game
        jsr clear_screen
        jsr init_game
        jmp game_loop

game_over_state:
        ; Redraw game over text (repairs any star damage)
        jsr show_game_over

        ; Poll fire button
        lda $dc00
        and #%00010000
        bne state_done          ; Not pressed

        ; Fire pressed — return to title
        jsr clear_screen
        jsr init_title
        jmp game_loop

state_done:
        jmp game_loop

game_active:

        ; --- Death timer (invulnerability flash) ---
        lda death_timer
        beq no_death_flash

        dec death_timer
        bne no_death_flash

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

        lda bullet_active
        bne no_fire

        ; Spawn bullet at ship position
        lda $d000
        sta $d002
        lda $d001
        sta bullet_y

        ; Enable sprite 1
        lda $d015
        ora #%00000010
        sta $d015

        lda #$01
        sta bullet_active

        ; Trigger laser sound
        lda #$20
        sta $d404
        lda #$21
        sta $d404

no_fire:

        ; --- Update bullet ---
        lda bullet_active
        beq no_bullet

        ; Move bullet up
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003

        ; Off-screen check
        cmp #$1e
        bcs no_bullet

        lda #$00
        sta bullet_active
        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

        ; 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
        sec
        sbc enemy_x_tbl,x
        cmp #$10
        bcc hit_enemy
        cmp #$f0
        bcc next_collision
        jmp hit_enemy

next_collision:
        inx
        cpx #$03
        bne collision_loop
        jmp no_hit

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

        ; Explosion sound — SID voice 2 (noise)
        lda #$00
        sta $d407
        lda #$08
        sta $d408
        lda #$09
        sta $d40c
        lda #$00
        sta $d40d
        lda #$80
        sta $d40b
        lda #$81
        sta $d40b

        ; Increment score (BCD)
        sed
        lda score
        clc
        adc #$01
        sta score
        cld

        ; Update score display — tens digit
        lda score
        lsr
        lsr
        lsr
        lsr
        clc
        adc #$30
        sta $0400

        ; Update score display — ones digit
        lda score
        and #$0f
        clc
        adc #$30
        sta $0401

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
        cmp #$f8
        bcc update_enemy_sprite

        ; Respawn at top
        lda #$32
        jsr spawn_enemy
        jmp next_enemy

do_flash:
        dec flash_tbl,x
        bne update_enemy_sprite

        ; Flash done — respawn
        lda #$32
        jsr spawn_enemy
        jmp next_enemy

update_enemy_sprite:
        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

        ldx #$00
ship_collision_loop:
        lda flash_tbl,x
        bne next_ship_check

        ; Check Y distance
        lda $d001
        sec
        sbc enemy_y_tbl,x
        cmp #$10
        bcc check_ship_x
        cmp #$f0
        bcc next_ship_check

check_ship_x:
        ; Check X distance
        lda $d000
        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

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

        ; Game over
        lda #$02
        sta game_state
        lda #$02
        sta $d027
        lda #$00
        sta $d020
        sta death_timer
        jsr show_game_over
        jmp play_death_sound

life_lost:
        ; Reset ship position
        lda #172
        sta $d000
        lda #220
        sta $d001

        ; Start death flash
        lda #16
        sta death_timer
        lda #$02
        sta $d020

play_death_sound:
        ; SID voice 3 (descending sawtooth)
        lda #$00
        sta $d40e
        lda #$10
        sta $d40f
        lda #$0a
        sta $d412
        lda #$00
        sta $d413
        lda #$20
        sta $d411
        lda #$21
        sta $d411

        jmp game_loop

; ------------------------------------------------
; Subroutine: erase_star
; X = star index. Writes space to the star's screen position.
; ------------------------------------------------
erase_star:
        ldy star_row,x
        lda row_addr_lo,y
        sta $fb
        lda row_addr_hi,y
        sta $fc
        ldy star_col,x
        lda #$20
        sta ($fb),y
        rts

; ------------------------------------------------
; Subroutine: draw_star
; X = star index. Writes character and colour at the star's position.
; ------------------------------------------------
draw_star:
        ldy star_row,x
        lda row_addr_lo,y
        sta $fb
        lda row_addr_hi,y
        sta $fc
        ldy star_col,x

        ; Write character to screen RAM
        lda star_char_tbl,x
        sta ($fb),y

        ; Switch pointer to colour RAM (high byte + $D4)
        lda $fc
        clc
        adc #$d4
        sta $fc

        ; Write colour
        lda star_colour_tbl,x
        sta ($fb),y
        rts

; ------------------------------------------------
; 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_title
; Sets up the title screen
; ------------------------------------------------
init_title:
        ; No sprites on title screen
        lda #$00
        sta $d015
        sta frame_count
        sta game_state          ; 0 = title

        ; Draw stars and title text
        jsr init_stars
        jsr show_title

        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
        lda #220
        sta $d001
        lda #$01
        sta $d027

        ; Enemy colours
        lda #$05
        sta $d029
        sta $d02a
        sta $d02b

        ; Spawn three enemies at staggered heights
        lda #$32
        ldx #$00
        jsr spawn_enemy
        lda #$82
        ldx #$01
        jsr spawn_enemy
        lda #$d2
        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 score
        sta death_timer
        sta frame_count
        sta $d020               ; Border black

        lda #$01
        sta game_state          ; 1 = playing

        ; Score display
        lda #$30
        sta $0400
        sta $0401

        ; Lives
        lda #$03
        sta lives
        lda #$33
        sta $0427

        ; Draw stars
        jsr init_stars

        rts

; ------------------------------------------------
; Subroutine: init_stars
; Resets star positions and draws them
; ------------------------------------------------
init_stars:
        ldx #$00
-       lda star_init_row,x
        sta star_row,x
        lda star_init_col,x
        sta star_col,x
        jsr draw_star
        inx
        cpx #$08
        bne -
        rts

; ------------------------------------------------
; Subroutine: show_title
; Writes "STARFIELD" and "PRESS FIRE" to screen RAM
; ------------------------------------------------
show_title:
        ; "STARFIELD" at row 10, col 16 ($05A0)
        lda #$13            ; S
        sta $05a0
        lda #$14            ; T
        sta $05a1
        lda #$01            ; A
        sta $05a2
        lda #$12            ; R
        sta $05a3
        lda #$06            ; F
        sta $05a4
        lda #$09            ; I
        sta $05a5
        lda #$05            ; E
        sta $05a6
        lda #$0c            ; L
        sta $05a7
        lda #$04            ; D
        sta $05a8

        ; Colour to white
        lda #$01
        sta $d9a0
        sta $d9a1
        sta $d9a2
        sta $d9a3
        sta $d9a4
        sta $d9a5
        sta $d9a6
        sta $d9a7
        sta $d9a8

        ; "PRESS FIRE" at row 14, col 15 ($063F)
        lda #$10            ; P
        sta $063f
        lda #$12            ; R
        sta $0640
        lda #$05            ; E
        sta $0641
        lda #$13            ; S
        sta $0642
        lda #$13            ; S
        sta $0643
        lda #$20            ; (space)
        sta $0644
        lda #$06            ; F
        sta $0645
        lda #$09            ; I
        sta $0646
        lda #$12            ; R
        sta $0647
        lda #$05            ; E
        sta $0648

        ; Colour to light grey
        lda #$0f
        sta $da3f
        sta $da40
        sta $da41
        sta $da42
        sta $da43
        sta $da44
        sta $da45
        sta $da46
        sta $da47
        sta $da48

        rts

; ------------------------------------------------
; Subroutine: show_game_over
; Writes "GAME OVER" to screen RAM, row 12, col 16
; ------------------------------------------------
show_game_over:
        lda #$07
        sta $05f0
        lda #$01
        sta $05f1
        lda #$0d
        sta $05f2
        lda #$05
        sta $05f3
        lda #$20
        sta $05f4
        lda #$0f
        sta $05f5
        lda #$16
        sta $05f6
        lda #$05
        sta $05f7
        lda #$12
        sta $05f8

        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
        lda enemy_y_tbl,x
        sta $d001,y

        rts

; ------------------------------------------------
; Lookup tables
; ------------------------------------------------

; VIC-II sprite register offsets
sprite_pos_off:
        !byte $04, $06, $08

sprite_colour_off:
        !byte $29, $2a, $2b

; Screen RAM row start addresses (rows 0-24)
row_addr_lo:
        !byte $00, $28, $50, $78, $a0, $c8, $f0, $18
        !byte $40, $68, $90, $b8, $e0, $08, $30, $58
        !byte $80, $a8, $d0, $f8, $20, $48, $70, $98, $c0

row_addr_hi:
        !byte $04, $04, $04, $04, $04, $04, $04, $05
        !byte $05, $05, $05, $05, $05, $06, $06, $06
        !byte $06, $06, $06, $06, $07, $07, $07, $07, $07

; Star initial positions (8 stars: 0-3 close, 4-7 distant)
star_init_row:
        !byte 2, 8, 14, 20, 5, 11, 17, 23

star_init_col:
        !byte 5, 28, 15, 35, 18, 7, 32, 22

; Star appearance (close = bright asterisk, distant = dim period)
star_char_tbl:
        !byte $2a, $2a, $2a, $2a, $2e, $2e, $2e, $2e

star_colour_tbl:
        !byte $01, $01, $01, $01, $0b, $0b, $0b, $0b

; ------------------------------------------------
; 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 title screen? Check the boot sequence calls init_title, not init_game. The game should start in state 0.
  • Title text doesn’t appear? Check show_title writes to $05A0 (row 10, col 16) and $063F (row 14, col 15). Check colour RAM writes use $D9A0 and $DA3F.
  • Fire button doesn’t start the game? The title state handler must check bit 4 of $DC00 and call init_game on press. Check game_state is set to 1 inside init_game.
  • Game over doesn’t return to title? The game over handler must call init_title, not init_game. Check game_state is set to 2 (not 1) in ship_hit.
  • Stars freeze on title screen? The star update loop must run before the state dispatch, not inside game_active.
  • Title text flickers? show_title must be called every frame in title_state to repair star overwrites. Check JSR show_title appears before the fire button check.

Try This: Colour Cycling on the Title

Change the title text colour each frame. Use frame_count AND #$07 to cycle through 8 colours. Write the result to the colour RAM positions for “STARFIELD”. The title pulses with colour while the stars drift behind it.

Try This: Hide Score and Lives on Title

The score and lives display from the previous game lingers on the title screen. Clear them in init_title by writing spaces to $0400-$0401 and $0427. The title screen shows only the title and stars.

What You’ve Learnt

  • Three-state machinesCMP and BEQ to dispatch between title, playing, and game over.
  • State transitions — each state handles its own exit condition and calls the right init routine.
  • Screen RAM text — writing characters and colours to fixed positions for UI elements.
  • Shared subroutinesinit_stars called from both init_title and init_game.

What’s Next

The ship can only move across half the screen — sprite X positions are 8-bit, but the C64 screen is 320 pixels wide. In Unit 16, the $D010 register unlocks the ninth bit, and boundary clamping keeps everything on screen.