Home / Commodore 64 / Assembly / Lesson 1
Lesson 1 of 8

Input & State Core

What you'll learn:

  • Read joystick and keyboard state via CIA registers in assembly.
  • Maintain game-state flags and transitions inside zero-page structs.
  • Feed input/state data into the new assembly main loop.
13% Complete

Introduction

Objects always spawn at column 20 - predictable and boring. Real games need randomness.

By the end of this lesson, every object will fall at a different position, making the game actually challenging!

The Problem with Predictability

Try catching 20 objects in the current game. Easy, right? You just park at column 20 and wait.

Games need unpredictability. The player should react to where objects appear, not memorize patterns.

Random Numbers on the C64

Modern computers have dedicated random number generators. The C64 doesn’t. But it has something almost as good: the SID chip’s noise generator.

The SID (Sound Interface Device) generates audio. One of its features is a noise waveform for sound effects. The noise values change constantly and unpredictably - perfect for random numbers!

The trick: Read from address $D41B (SID voice 3 oscillator output) to get a random byte (0-255).

But there’s a catch: we need to configure the SID to generate noise first.

Setting Up the SID for Randomness

Add this initialization:

Initialize Random
init_random:
    ; Configure SID voice 3 for random number generation
    lda #$ff                   ; Maximum frequency
    sta $d40e                  ; Voice 3 frequency low
    sta $d40f                  ; Voice 3 frequency high

    lda #$80                   ; Noise waveform
    sta $d412                  ; Voice 3 control register

    rts

Call this from main during setup:

main:
    jsr init_screen
    jsr init_random            ; Add this

    lda #PLAYER_START_COL
    sta PLAYER_COL
    ; etc...

Getting a Random Number

Now we can read random values:

get_random:
    ; Returns random value 0-255 in A
    lda $d41b                  ; Read SID voice 3 oscillator
    rts

Simple! But we need random columns (0-39), not random bytes (0-255).

Converting to Column Range

We need to convert 0-255 to 0-39. If we use just the lower 6 bits, we get values 0-63. That’s close to 40. We can:

  1. Get a random byte
  2. Keep only lower 6 bits (0-63)
  3. If >= 40, try again
Random Column
get_random_column:
    ; Returns random column 0-39 in A
try_again:
    jsr get_random             ; Get random byte
    and #%00111111             ; Keep only lower 6 bits (0-63)
    cmp #40                    ; Is it < 40?
    bcs try_again              ; If >= 40, try again
    rts                        ; Return valid column in A

💡 Why This Works
Most attempts (40 out of 64) succeed immediately. The rest loop once or twice. Fast enough!

Random Object Spawning

Update spawn_object:

spawn_object:
    lda #$01
    sta OBJ_ACTIVE

    lda #$00
    sta OBJ_ROW

    jsr get_random_column      ; Get random column!
    sta OBJ_COL

    jsr draw_object
    rts

That’s it! Now every object spawns at a random column.

Complete Lesson 11 Code

skyfall.asm - Complete
; SKYFALL - Lesson 11
; Random spawning

* = $0801

; BASIC stub: SYS 2061
!byte $0c,$08,$0a,$00,$9e
!byte $32,$30,$36,$31          ; "2061" in ASCII
!byte $00,$00,$00

* = $080d

; ===================================
; Constants
; ===================================
SCREEN_RAM = $0400
COLOR_RAM = $d800
VIC_BORDER = $d020
VIC_BACKGROUND = $d021
CIA1_PORT_A = $dc01
CIA1_PORT_B = $dc00
VIC_RASTER = $d012

SID_V3_FREQ_LO = $d40e
SID_V3_FREQ_HI = $d40f
SID_V3_CONTROL = $d412
SID_V3_OSC = $d41b

BLACK = $00
WHITE = $01
YELLOW = $07
DARK_GREY = $0b

PLAYER_CHAR = $1e
PLAYER_COLOR = WHITE
PLAYER_ROW = 23
PLAYER_START_COL = 20

OBJECT_CHAR = $2a
OBJECT_COLOR = YELLOW

; Variables
PLAYER_COL = $02
MOVE_COUNTER = $03
OBJ_COL = $04
OBJ_ROW = $05
OBJ_ACTIVE = $06
FALL_COUNTER = $07
SCORE = $08
MISSES = $09

; ===================================
; Main Program
; ===================================
main:
    jsr init_screen
    jsr init_random            ; Initialize random number generator

    lda #PLAYER_START_COL
    sta PLAYER_COL

    lda #3
    sta MOVE_COUNTER

    lda #5
    sta FALL_COUNTER

    lda #$00
    sta SCORE
    sta MISSES

    jsr draw_player
    jsr spawn_object
    jsr display_score
    jsr display_misses

game_loop:
    jsr wait_for_raster

    ; Player movement (every 3 frames)
    dec MOVE_COUNTER
    bne skip_player_movement
    lda #3
    sta MOVE_COUNTER
    jsr check_keys
skip_player_movement:

    ; Object falling (every 5 frames)
    dec FALL_COUNTER
    bne skip_object_fall
    lda #5
    sta FALL_COUNTER
    jsr update_object
    jsr check_collision
    jsr draw_player           ; Redraw player (in case object erased it)
skip_object_fall:

    jmp game_loop

; ===================================
; Subroutines
; ===================================

wait_for_raster:
    lda VIC_RASTER
    cmp #250
    bne wait_for_raster
    rts

init_screen:
    jsr wait_for_raster
    lda #BLACK
    sta VIC_BACKGROUND
    lda #DARK_GREY
    sta VIC_BORDER
    jsr clear_screen
    rts

init_random:
    ; Set up SID voice 3 for random number generation
    lda #$ff
    sta SID_V3_FREQ_LO
    sta SID_V3_FREQ_HI

    lda #$80                   ; Noise waveform
    sta SID_V3_CONTROL
    rts

get_random:
    ; Returns random byte 0-255 in A
    lda SID_V3_OSC
    rts

get_random_column:
    ; Returns random column 0-39 in A
try_again:
    jsr get_random
    and #%00111111             ; Keep lower 6 bits (0-63)
    cmp #40
    bcs try_again              ; If >= 40, try again
    rts

clear_screen:
    lda #$20
    ldx #$00
clear_screen_loop:
    sta SCREEN_RAM,x
    sta SCREEN_RAM + $100,x
    sta SCREEN_RAM + $200,x
    sta SCREEN_RAM + $300,x
    inx
    bne clear_screen_loop

    lda #WHITE
    ldx #$00
clear_color_loop:
    sta COLOR_RAM,x
    sta COLOR_RAM + $100,x
    sta COLOR_RAM + $200,x
    sta COLOR_RAM + $300,x
    inx
    bne clear_color_loop
    rts

check_keys:
    ; Check D key (column 2, row 2)
    lda #%11111011
    sta CIA1_PORT_B
    lda CIA1_PORT_A
    and #%00000100
    beq move_right

    ; Check A key (column 1, row 2)
    lda #%11111101
    sta CIA1_PORT_B
    lda CIA1_PORT_A
    and #%00000100
    beq move_left

    rts

move_left:
    lda PLAYER_COL
    beq skip_left

    jsr erase_player
    dec PLAYER_COL
    jsr draw_player

skip_left:
    rts

move_right:
    lda PLAYER_COL
    cmp #39
    beq skip_right

    jsr erase_player
    inc PLAYER_COL
    jsr draw_player

skip_right:
    rts

erase_player:
    ldx PLAYER_COL
    lda #$20
    sta SCREEN_RAM + (PLAYER_ROW * 40),x
    rts

draw_player:
    ldx PLAYER_COL
    lda #PLAYER_CHAR
    sta SCREEN_RAM + (PLAYER_ROW * 40),x

    lda #PLAYER_COLOR
    sta COLOR_RAM + (PLAYER_ROW * 40),x
    rts

spawn_object:
    lda #$01
    sta OBJ_ACTIVE

    lda #$00
    sta OBJ_ROW

    jsr get_random_column      ; Random column!
    sta OBJ_COL

    jsr draw_object
    rts

update_object:
    lda OBJ_ACTIVE
    beq done_update

    jsr erase_object

    inc OBJ_ROW

    lda OBJ_ROW
    cmp #24
    bne still_falling

    lda #$00
    sta OBJ_ACTIVE

    inc MISSES
    jsr display_misses
    jsr spawn_object
    rts

still_falling:
    jsr draw_object

done_update:
    rts

check_collision:
    lda OBJ_ACTIVE
    beq no_collision

    lda OBJ_ROW
    cmp #PLAYER_ROW
    bne no_collision

    lda OBJ_COL
    cmp PLAYER_COL
    bne no_collision

    jsr handle_catch

no_collision:
    rts

handle_catch:
    jsr erase_object

    lda #$00
    sta OBJ_ACTIVE

    inc SCORE
    jsr display_score

    jsr spawn_object
    rts

; Row offset lookup table (low bytes)
row_offset_lo:
    !byte <(0*40), <(1*40), <(2*40), <(3*40), <(4*40)
    !byte <(5*40), <(6*40), <(7*40), <(8*40), <(9*40)
    !byte <(10*40), <(11*40), <(12*40), <(13*40), <(14*40)
    !byte <(15*40), <(16*40), <(17*40), <(18*40), <(19*40)
    !byte <(20*40), <(21*40), <(22*40), <(23*40), <(24*40)

; Row offset lookup table (high bytes)
row_offset_hi:
    !byte >(0*40), >(1*40), >(2*40), >(3*40), >(4*40)
    !byte >(5*40), >(6*40), >(7*40), >(8*40), >(9*40)
    !byte >(10*40), >(11*40), >(12*40), >(13*40), >(14*40)
    !byte >(15*40), >(16*40), >(17*40), >(18*40), >(19*40)
    !byte >(20*40), >(21*40), >(22*40), >(23*40), >(24*40)

draw_object:
    ; Get row offset from table
    ldx OBJ_ROW
    lda row_offset_lo,x
    sta $fb
    lda row_offset_hi,x
    sta $fc

    ; Add column (with 16-bit carry handling)
    lda $fb
    clc
    adc OBJ_COL
    sta $fb
    bcc +
    inc $fc
+
    ; Add SCREEN_RAM base address
    lda $fb
    clc
    adc #<SCREEN_RAM
    sta $fb
    lda $fc
    adc #>SCREEN_RAM
    sta $fc

    ; Draw character
    ldy #0
    lda #OBJECT_CHAR
    sta ($fb),y

    ; Calculate COLOR_RAM address
    lda $fb
    clc
    adc #<(COLOR_RAM-SCREEN_RAM)
    sta $fb
    lda $fc
    adc #>(COLOR_RAM-SCREEN_RAM)
    sta $fc

    ; Draw color
    lda #OBJECT_COLOR
    sta ($fb),y
    rts

erase_object:
    ; Get row offset from table
    ldx OBJ_ROW
    lda row_offset_lo,x
    sta $fb
    lda row_offset_hi,x
    sta $fc

    ; Add column
    lda $fb
    clc
    adc OBJ_COL
    sta $fb
    bcc +
    inc $fc
+
    ; Add SCREEN_RAM base
    lda $fb
    clc
    adc #<SCREEN_RAM
    sta $fb
    lda $fc
    adc #>SCREEN_RAM
    sta $fc

    ; Erase with space
    ldy #0
    lda #$20
    sta ($fb),y
    rts

display_score:
    lda SCORE
    clc
    adc #$30
    sta SCREEN_RAM

    lda #WHITE
    sta COLOR_RAM
    rts

display_misses:
    lda MISSES
    clc
    adc #$30
    sta SCREEN_RAM + 5

    lda #WHITE
    sta COLOR_RAM + 5
    rts

Build and run. Objects now fall at unpredictable positions! The game is suddenly much more challenging.

New Concepts

Opcodes Used in This Lesson

bcs label - Branch if Carry Set

  • Opposite of bcc (branch if carry clear)
  • After cmp #40, carry is SET if A >= 40
  • We use this to retry if random number is too large
  • Example: bcs try_again loops back when value >= 40

The SID Chip

The SID (Sound Interface Device) is the C64’s sound chip. It has:

  • 3 voices (oscillators) for music
  • Multiple waveforms (pulse, sawtooth, triangle, noise)
  • Filters and envelopes

We’re using voice 3’s noise waveform. The values change constantly based on the oscillator state - effectively random for game purposes.

Why Voice 3?

Voice 3 is special: its oscillator can be read directly ($D41B) without affecting audio output. Voices 1 and 2 don’t have this feature.

This lets us use voice 3 for randomness while still having voices 1 and 2 available for sound effects (which we’ll add later).

”Random Enough”

The SID’s noise isn’t cryptographically random - it’s deterministic based on oscillator state. But for games, it’s perfect:

  • Fast to read
  • No complex algorithms needed
  • Good distribution
  • Unpredictable to players

Rejection Sampling

Our get_random_column uses rejection sampling: get a random value from a larger range, reject values outside the target range, try again.

This is simpler than using modulo or division, and fast enough since most attempts succeed immediately.

🎯 Your Tasks
  1. Build and run - Play with random spawning, notice the difficulty increase
  2. Test distribution - Play for a while, do objects seem evenly distributed across columns?
  3. Change the range - Try modifying to spawn only in columns 10-30
  4. Remove randomness - Comment out jsr init_random and see what happens

Next Steps

The game is now genuinely challenging! Objects fall at unpredictable positions, forcing the player to react rather than memorize.

In Lesson 12, we’ll add multiple falling objects simultaneously to really challenge the player!