3

Neon Nexus: Electronic Pulse

Commodore 64 • Phase 1 • Tier 1

Bring your player to life with keyboard controls! Learn to read input from the C64 keyboard and create smooth, responsive movement as your geometric entity navigates the neon arena.

easy
⏱️ 30-45 minutes
💻 Code Examples
🛠️ Exercise
🎯

Learning Objectives

  • Read keyboard input using the C64's keyboard matrix
  • Update player position based on user input
  • Clear and redraw characters for smooth movement
  • Handle screen boundaries to keep player in bounds
  • Create responsive controls with proper game loop timing
🧠

Key Concepts

Keyboard matrix scanning and input detection Game state updates based on user input Character movement and screen refresh techniques Boundary checking and collision with screen edges Game loop design for interactive programs

Lesson 3: Neon Nexus - Electronic Pulse

Time to bring your player to life! In the last lesson, you created a player entity that could be positioned anywhere on screen. Today, you’ll add keyboard controls to make it move. By the end of this lesson, you’ll be navigating your neon arena with smooth, responsive controls!

Understanding C64 Keyboard Input

The C64 reads the keyboard through a matrix system. For now, we’ll use a simple method that checks if specific keys are pressed. Later lessons will explore the keyboard matrix in detail.

Building Movement Step by Step

Step 1: Start with Your Player Code

Create movement.s starting with your working player code from lesson 2:

; Neon Nexus - Lesson 3
; Adding player movement

*= $0801

; BASIC stub: 10 SYS 2061
!word next_line
!word 10
!byte $9e
!text "2061"
!byte 0
next_line:
!word 0

; Start of our program
start:
        jsr setup_arena
        jsr create_player
        
game_loop:
        jmp game_loop

; Include all subroutines from lesson 2
; (setup_arena, create_player, calculate_screen_pos)

; Player data (place at END to avoid breaking BASIC stub)
player_x:   !byte 20    ; X position (column)
player_y:   !byte 12    ; Y position (row)

Build and test to ensure your starting point works:

acme -f cbm -o movement.prg movement.s
x64sc movement.prg

Step 2: Add Keyboard Reading

Let’s add a routine to check for keypresses. Add this new subroutine:

read_keyboard:
        ; Read keyboard matrix
        lda $dc01       ; Read keyboard column
        sta $fb         ; Store for testing
        rts

Update your game loop to call it:

game_loop:
        jsr read_keyboard   ; ADD THIS LINE
        jmp game_loop

This doesn’t do anything visible yet, but it’s reading the keyboard!

Step 3: Check for Specific Keys

Let’s check for the W key (up). Replace your read_keyboard subroutine:

read_keyboard:
        ; Check for W key (up)
        lda #$fd        ; Row with W key
        sta $dc00       ; Select keyboard row
        lda $dc01       ; Read that row
        and #$02        ; Check W key bit
        bne check_s     ; If not pressed, check next key
        
        ; W is pressed!
        dec player_y    ; Move up (decrease Y)
        
check_s:
        ; We'll add more keys here later
        rts

Important: This moves the player position in memory, but we need to redraw to see it!

Step 4: Add Screen Update

We need to clear the old position and draw the new one. Add this new subroutine:

update_player:
        ; Clear old player position
        jsr calculate_screen_pos
        lda #$20        ; Space character
        ldy #$00
        sta ($fb),y     ; Clear old position
        
        ; Clear old color too
        lda #$00
        sta $fd
        lda #$d8
        sta $fe
        
        ; Recalculate color position
        ldy player_y
        beq clear_x_color
color_loop:
        lda $fd
        clc
        adc #40
        sta $fd
        bcc no_color_carry
        inc $fe
no_color_carry:
        dey
        bne color_loop
clear_x_color:
        lda $fd
        clc
        adc player_x
        sta $fd
        bcc clear_color
        inc $fe
clear_color:
        lda #$00        ; Black color
        ldy #$00
        sta ($fd),y
        
        ; Now draw at new position
        jsr create_player
        rts

Step 5: Connect Movement to Display

Update your game loop to use the new update routine:

game_loop:
        jsr read_keyboard
        jsr update_player   ; ADD THIS LINE
        jmp game_loop

Build and test - pressing W should move the player up! But there’s a problem - it moves too fast and can go off screen.

Step 6: Add Movement Delay

Let’s slow down the movement. First, add a delay counter at the very end of your file, after all subroutines:

; Put this at the END of your file, after calculate_screen_pos:

; Movement data
move_delay: !byte 0     ; Movement delay counter

Then update read_keyboard to use the delay:

read_keyboard:
        ; Check movement delay
        lda move_delay
        beq can_move    ; If 0, we can move
        dec move_delay  ; Otherwise count down
        rts
        
can_move:
        ; Check for W key (up)
        lda #$fd
        sta $dc00
        lda $dc01
        and #$02
        bne check_s
        
        ; W is pressed - check boundaries
        lda player_y
        beq check_s     ; Already at top (Y=0)
        
        dec player_y    ; Move up
        lda #$04        ; Reset delay (4 frames)
        sta move_delay
        
check_s:
        rts

Important: Variables must go at the end to avoid breaking the BASIC stub address!

Step 7: Add More Directions

Now let’s add all four directions. Replace your complete read_keyboard subroutine:

read_keyboard:
        ; Check movement delay
        lda move_delay
        beq can_move
        dec move_delay
        rts
        
can_move:
        ; Check for W key (up)
        lda #$fd
        sta $dc00
        lda $dc01
        and #$02
        bne check_s
        
        ; W pressed - move up
        lda player_y
        beq check_s     ; Already at top
        dec player_y
        lda #$04        ; 4 frame delay
        sta move_delay
        rts             ; Only one move per frame
        
check_s:
        ; Check for S key (down)
        lda #$fd
        sta $dc00
        lda $dc01
        and #$20
        bne check_a
        
        ; S pressed - move down
        lda player_y
        cmp #24         ; Bottom boundary
        beq check_a     ; Already at bottom
        inc player_y
        lda #$04        ; 4 frame delay
        sta move_delay
        rts
        
check_a:
        ; Check for A key (left)
        lda #$fd
        sta $dc00
        lda $dc01
        and #$04
        bne check_d
        
        ; A pressed - move left
        lda player_x
        beq check_d     ; Already at left edge
        dec player_x
        lda #$04        ; 4 frame delay
        sta move_delay
        rts
        
check_d:
        ; Check for D key (right)
        lda #$fb        ; Row 2 (inverted)
        sta $dc00
        lda $dc01
        and #$04        ; Bit 2
        bne done_keys
        
        ; D pressed - move right
        lda player_x
        cmp #39         ; Right boundary
        beq done_keys   ; Already at right edge
        inc player_x
        lda #$04        ; 4 frame delay
        sta move_delay
        rts             ; Only one move per frame
        
done_keys:
        rts

Step 8: Optimize Screen Updates

Our current update always redraws. Let’s make it smarter. First, add a movement flag after your variables:

; Put these at the END of your file, after all subroutines:

; Player data
player_x:     !byte 20
player_y:     !byte 12
move_delay:   !byte 0
player_moved: !byte 0    ; ADD THIS LINE

Now update your game loop to sync to the video frame and only redraw when needed:

game_loop:
        ; Wait for raster line 255 (bottom of screen)
wait_raster:
        lda $d012       ; Current raster line
        cmp #$ff        ; Wait for line 255
        bne wait_raster
        
        jsr read_keyboard
        
        ; Only update display if player moved
        lda player_moved
        beq game_loop
        
        jsr update_player
        lda #$00
        sta player_moved    ; Clear flag
        
        jmp game_loop

Understanding Raster Synchronization

The raster sync we just added is crucial for proper game timing:

  • $D012 contains the current raster line being drawn (0-255)
  • The C64 screen is drawn line by line, 50 times per second (PAL)
  • By waiting for line 255 (bottom of screen), our game loop runs exactly once per frame
  • This ensures consistent timing regardless of how fast your CPU/emulator runs

Why is this important?

  • Without sync: Game loop runs thousands of times per second → delay counters useless
  • With sync: Game loop runs 50 times per second → timing works correctly
  • Professional C64 games always use raster sync for smooth gameplay

Then update the read_keyboard routine to handle delay and set the movement flag:

read_keyboard:
        ; Check movement delay first
        lda move_delay
        beq can_move
        dec move_delay      ; Count down delay
        rts                 ; Exit if still in delay
        
can_move:
        
        ; Check for W key (up)
        lda #$fd
        sta $dc00
        lda $dc01
        and #$02
        bne check_s
        
        ; W pressed - move up
        lda player_y
        beq check_s     ; Already at top
        dec player_y
        lda #$01        ; Set movement flag
        sta player_moved
        lda #$04        ; 4 frame delay (fast response)
        sta move_delay
        rts
        
check_s:
        ; Check for S key (down)
        lda #$fd
        sta $dc00
        lda $dc01
        and #$20
        bne check_a
        
        ; S pressed - move down
        lda player_y
        cmp #24
        beq check_a     ; Already at bottom
        inc player_y
        lda #$01        ; Set movement flag
        sta player_moved
        lda #$04        ; 4 frame delay (fast response)
        sta move_delay
        rts
        
check_a:
        ; Check for A key (left)
        lda #$fd
        sta $dc00
        lda $dc01
        and #$04
        bne check_d
        
        ; A pressed - move left
        lda player_x
        beq check_d     ; Already at left edge
        dec player_x
        lda #$01        ; Set movement flag
        sta player_moved
        lda #$04        ; 4 frame delay (fast response)
        sta move_delay
        rts
        
check_d:
        ; Check for D key (right)
        lda #$fb
        sta $dc00
        lda $dc01
        and #$04
        bne done_keys
        
        ; D pressed - move right
        lda player_x
        cmp #39
        beq done_keys   ; Already at right edge
        inc player_x
        lda #$01        ; Set movement flag
        sta player_moved
        lda #$04        ; 4 frame delay (fast response)
        sta move_delay
        
done_keys:
        rts

Step 9: Understanding Screen Updates

Our current update_player routine works, but let’s understand what it’s doing and why it’s efficient.

The challenge: When a player moves, we need to:

  1. Clear the old position (erase the character that was there)
  2. Draw at the new position (show the character in the new location)

Our current approach already handles this correctly:

update_player:
        ; Clear old position
        jsr calculate_screen_pos    ; Uses current player_x, player_y
        lda #$20        ; Space character
        ldy #$00
        sta ($fb),y     ; Clear where player WAS
        
        ; Clear old color too
        ; (color clearing code...)
        
        ; Now draw at new position
        jsr create_player   ; This recalculates position and draws
        rts

Actually, there’s a problem here! When update_player is called, player_x and player_y have already been changed to the NEW position by read_keyboard. So we’re clearing the new position, not the old one!

Why it seems to work: In our simple case, we’re clearing the position and immediately redrawing there, so you don’t notice the problem.

The proper solution: Store the old position before changing it. First, add variables to track the old position:

; Add these to your player data at the end of the file:
old_player_x: !byte 20    ; For clearing old position
old_player_y: !byte 12    ; For clearing old position

Then create a subroutine to store the old position (add this after your other subroutines):

store_old_position:
        lda player_x
        sta old_player_x
        lda player_y  
        sta old_player_y
        rts

Now fix read_keyboard to use this subroutine before any movement:

        ; W pressed - move up
        lda player_y
        beq check_s     ; Already at top
        
        jsr store_old_position  ; Store position before changing
        dec player_y            ; Now change position
        lda #$01
        sta player_moved
        lda #$04
        sta move_delay
        rts
        
check_s:
        ; S pressed - move down
        lda player_y
        cmp #24
        beq check_a     ; Already at bottom
        
        jsr store_old_position  ; Store position before changing
        inc player_y            ; Now change position
        lda #$01
        sta player_moved
        lda #$04
        sta move_delay
        rts
        
check_a:
        ; A pressed - move left
        lda player_x
        beq check_d     ; Already at left edge
        
        jsr store_old_position  ; Store position before changing
        dec player_x            ; Now change position
        lda #$01
        sta player_moved
        lda #$04
        sta move_delay
        rts
        
check_d:
        ; D pressed - move right
        lda player_x
        cmp #39
        beq done_keys   ; Already at right edge
        
        jsr store_old_position  ; Store position before changing
        inc player_x            ; Now change position
        lda #$01
        sta player_moved
        lda #$04
        sta move_delay

Benefits of using a subroutine:

  • Less repetition - 6 lines of code become 1 jsr call
  • Easier to maintain - change the logic in one place
  • More readable - intent is clear from the subroutine name

Then update_player uses the old positions for clearing:

update_player:
        ; Temporarily store current position in zero page
        lda player_x
        sta $02         ; Temp storage for X
        lda player_y
        sta $03         ; Temp storage for Y
        
        ; Use old position to calculate where to clear
        lda old_player_x
        sta player_x    ; Temporarily use old X
        lda old_player_y
        sta player_y    ; Temporarily use old Y
        
        ; Clear old position
        jsr calculate_screen_pos
        lda #$20        ; Space character
        ldy #$00
        sta ($fb),y     ; Clear old position
        
        ; Restore current position
        lda $02
        sta player_x
        lda $03
        sta player_y
        
        ; Now draw at current position
        jsr create_player
        rts

The key insight: Always store the old position before modifying coordinates, so you can clear the correct screen location. This becomes crucial when adding enemies, bullets, or other moving objects that need precise clearing.

Step 10: Add Visual Feedback

Let’s flash the border when the player hits a wall. First, add a flash counter variable with your other variables:

; Add this to your player data at the end of the file:
flash_counter: !byte 0    ; For border flash timing

Then add this subroutine after your other subroutines:

flash_border:
        lda #$02        ; Red color
        sta $d020       ; Set border red
        lda #$10        ; Flash for 16 frames
        sta flash_counter
        rts

update_flash:
        ; Check if we're flashing
        lda flash_counter
        beq not_flashing
        
        ; Count down flash
        dec flash_counter
        bne still_flashing
        
        ; Flash finished - restore blue border
        lda #$06
        sta $d020
        
still_flashing:
not_flashing:
        rts

Then update your main game loop to handle the flash:

game_loop:
        ; Wait for raster sync
wait_raster:
        lda $d012
        cmp #$ff
        bne wait_raster
        
        jsr read_keyboard
        jsr update_flash    ; ADD THIS LINE
        
        ; Only update display if player moved
        lda player_moved
        beq game_loop
        
        jsr update_player
        lda #$00
        sta player_moved
        
        jmp game_loop

Then update your boundary checks to use it. Change the boundary check lines:

        ; W pressed - move up
        lda player_y
        beq hit_top     ; Change this from "beq check_s"
        
        jsr store_old_position
        dec player_y
        lda #$01
        sta player_moved
        lda #$04
        sta move_delay
        rts
        
hit_top:
        jsr flash_border    ; Flash when hitting top wall
        jmp check_s         ; Continue checking other keys

Apply the same pattern to all boundaries:

  • S key: beq hit_bottomhit_bottom: jsr flash_border / jmp check_a
  • A key: beq hit_lefthit_left: jsr flash_border / jmp check_d
  • D key: beq hit_righthit_right: jsr flash_border / jmp done_keys

Complete Working Examples

The complete working code for this lesson is available in our code samples repository:

📁 Lesson 3 Code Examples

All examples are tested and ready to assemble with ACME:

acme -f cbm -o movement.prg complete.s
x64sc movement.prg

Understanding the Code

Keyboard Matrix

The C64 keyboard is arranged in an 8×8 matrix:

  • Write to $DC00 to select a row
  • Read from $DC01 to check which keys in that row are pressed
  • Each bit represents a different key (0 = pressed)

Movement Logic

Our movement system:

  1. Checks if enough time has passed (movement delay)
  2. Reads the keyboard for WASD keys
  3. Checks boundaries before moving
  4. Updates position and sets movement flag
  5. Only redraws screen if player actually moved

Boundary Checking

Before moving, we check:

  • Up: Is Y already 0?
  • Down: Is Y already 24?
  • Left: Is X already 0?
  • Right: Is X already 39?

Experiment and Enhance

Try these modifications:

  1. Different movement speeds:

    lda #$08    ; Faster movement (8 frames)
    lda #$18    ; Slower movement (24 frames)
    lda #$04    ; Very fast (4 frames)
    
  2. Diagonal movement:

    ; Check multiple keys and update both X and Y
    
  3. Different boundary effects:

    ; Wrap around instead of stopping
    lda player_x
    cmp #40
    bne no_wrap
    lda #$00
    sta player_x
    
  4. Trail effect:

    ; Don't clear old position completely
    lda #$2e    ; Dot instead of space
    

Common Issues

Player moves too fast:

  • Increase the move_delay value
  • Add more delay in the game loop

Flickering:

  • Make sure you’re only updating when moved
  • Clear and draw in the right order

Keys not responding:

  • Check you’re using the right row/bit values
  • Some key combinations conflict

What You’ve Achieved

Read keyboard input using the C64’s matrix system
Created smooth movement with proper timing control
Added boundary detection to keep player on screen
Implemented visual feedback for wall collisions
Built an efficient update system that only redraws when needed

Your player entity is now fully interactive - a crucial milestone in game development!

Coming Next

In Lesson 4, you’ll add digital opposition - enemies that move around your arena! You’ll learn:

  • Creating multiple entities
  • Different movement patterns
  • Managing arrays of game objects
  • Basic AI behavior

Get ready for your first real gameplay challenge!

Quick Reference

Keyboard Reading:

lda #$fd    ; Select row
sta $dc00   ; Keyboard row register
lda $dc01   ; Read column data
and #$02    ; Check specific key bit

Key Row/Bit Values (tested and working):

  • W: Row $FD, Bit $02
  • A: Row $FD, Bit $04
  • S: Row $FD, Bit $20
  • D: Row $FB, Bit $04

Movement Pattern:

  1. Check delay counter
  2. Read keyboard
  3. Validate movement
  4. Update position
  5. Set flags
  6. Redraw if moved

Ready to add some enemies? On to lesson 4!