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.
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:
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:
- Get a random byte
- Keep only lower 6 bits (0-63)
- If >= 40, try again
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
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 - 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.
- Build and run - Play with random spawning, notice the difficulty increase
- Test distribution - Play for a while, do objects seem evenly distributed across columns?
- Change the range - Try modifying to spawn only in columns 10-30
- 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!