Skip to content
Game 1 Unit 2 of 16 1 hr learning time

Joystick Movement

Wrap the ship in a game loop and read the joystick through the CIA chip, so pushing the stick moves the ship — the moment the program becomes a game.

13% of Starfield

Push the joystick up and the ship moves up. Push right, it moves right. That reaction — hardware reading your intent, the screen answering — is the moment a program becomes a game.

You already have most of what you need. In the Primer you read a single bit to check the fire button, built decisions from compare-and-branch, and masked bits with AND. The one genuinely new idea here is the game loop: code that runs once every frame, reading input and updating the screen, over and over. We'll build it a single direction at a time.

Where we start

Unit 1 left a ship on screen that does nothing — it sits in a do-nothing loop. That's our starting point.

The white ship sprite sitting still near the bottom centre of an otherwise black screen.

Milestone 1 — the game loop, reading up

A game loop is just a loop that repeats at a fixed rate, doing the same work each pass: wait for the right moment, read input, update the screen, repeat.

every frame Setup (once) Wait for raster line 255 Read joystick Move ship
Setup runs once. Then the wait–read–move cycle repeats forever, once per frame.

We turn Unit 1's idle loop into that cycle, and read just one direction — up — to prove the whole pattern before repeating it:

Step 1: a game loop that reads up
+19-3
4040 sta $d015 ; Enable sprite 0
4141
4242 ; ------------------------------------------------
43-; Hold — do nothing, forever
43+; Game loop — runs once per frame
4444 ; ------------------------------------------------
45-hold:
46- jmp hold
45+game_loop:
46+ ; Wait for the raster beam to reach line 255
47+ ; This syncs our code to the display (~50Hz PAL)
48+- lda $d012
49+ cmp #$ff
50+ bne -
51+
52+ ; --- Read joystick and move ship ---
53+
54+ ; UP (bit 0)
55+ lda $dc00 ; Read joystick port 2
56+ and #%00000001 ; Isolate bit 0
57+ bne not_up ; Bit is 1 = NOT pressed (active low)
58+ dec $d001 ; Move ship up (decrease Y)
59+ dec $d001 ; 2 pixels per frame
60+not_up:
61+
62+ jmp game_loop
4763
4864 ; ------------------------------------------------
4965 ; Sprite data at $2000 (block 128)
The complete step 1 program
; Starfield - Unit 2: Joystick Movement
; Cumulative steps: step-00 (static ship) -> step-01 (+ game loop, up) -> step-02 (+ all directions)
; Assemble: acme -f cbm -o <step>.prg <step>.asm

; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Initialisation
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen
        ldx #$00
-       lda #$20
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

        ; Sprite 0 setup (ship)
        lda #128
        sta $07f8           ; Data pointer (block 128 = $2000)
        lda #172
        sta $d000           ; X position
        lda #220
        sta $d001           ; Y position
        lda #$01
        sta $d027           ; Colour (white)
        lda #%00000001
        sta $d015           ; Enable sprite 0

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
        ; This syncs our code to the display (~50Hz PAL)
-       lda $d012
        cmp #$ff
        bne -

        ; --- Read joystick and move ship ---

        ; UP (bit 0)
        lda $dc00           ; Read joystick port 2
        and #%00000001      ; Isolate bit 0
        bne not_up          ; Bit is 1 = NOT pressed (active low)
        dec $d001           ; Move ship up (decrease Y)
        dec $d001           ; 2 pixels per frame
not_up:

        jmp game_loop

; ------------------------------------------------
; Sprite data at $2000 (block 128)
; ------------------------------------------------
*= $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   ;

Run it and hold up: the ship climbs the screen and stops the moment you let go.

Holding up: the ship climbs from the bottom of the screen towards the top at two pixels per frame, then stops when the stick is released.

Syncing to the screen

-       lda $d012
        cmp #$ff
        bne -

The VIC-II draws the screen top to bottom, one line at a time, and register $d012 holds the line it's currently drawing. We wait for line 255 — the bottom of the visible picture — before we touch anything, so the ship never moves halfway through being drawn. CMP #$ff compares the line to 255; BNE loops back until they match. The result: our code runs once per frame, about 50 times a second on a PAL machine.

Reading the stick

The joystick plugs into a CIA chip (Complex Interface Adapter). Control port 2 lives at $dc00, and reading it gives one byte where each bit is one switch:

BitSwitchMask
0Up%00000001
1Down%00000010
2Left%00000100
3Right%00001000
4Fire%00010000

In the Primer you read one of these bits — the fire button. Now you read the whole stick. The catch is the same one as then: the bits are active-low. A 0 means pressed, a 1 means released. Untouched, $dc00 reads %11111111. Push up and bit 0 drops to 0:

$DC00 — Joystick Port 2 7 7 1 6 6 1 5 5 1 4 Fire active low 1 3 Right active low 1 2 Left active low 1 1 Down active low 1 0 Up active low 0
Joystick pushed up: bit 0 is 0 (active-low, so 0 = pressed). Bits 5–7 are unused.

To test one direction we isolate its bit — the masking you already know:

  • LDA $dc00 reads the port into A (no # — we're reading an address, not a literal).
  • AND #%00000001 keeps only bit 0. If up is pressed that bit is 0, so the result is 0; if not, the result is 1.
  • BNE not_up branches past the movement when the result is not zero — "not pressed, skip."
  • If we don't branch, up is held, so DEC $d001 lowers the sprite's Y position. Twice, for two pixels a frame.

DEC subtracts one from the value at an address in a single instruction. Because the sprite's Y position is a memory-mapped register, DEC $d001 moves the ship directly — no need to load, change, and store it back.

Milestone 2 — all four directions

One direction proved the pattern. The other three are the same four instructions with a different mask and a different register — $d000 is the ship's X, $d001 its Y:

Step 2: down, left and right
+24
5858 dec $d001 ; Move ship up (decrease Y)
5959 dec $d001 ; 2 pixels per frame
6060 not_up:
61+
62+ ; DOWN (bit 1)
63+ lda $dc00
64+ and #%00000010
65+ bne not_down
66+ inc $d001 ; Move ship down (increase Y)
67+ inc $d001
68+not_down:
69+
70+ ; LEFT (bit 2)
71+ lda $dc00
72+ and #%00000100
73+ bne not_left
74+ dec $d000 ; Move ship left (decrease X)
75+ dec $d000
76+not_left:
77+
78+ ; RIGHT (bit 3)
79+ lda $dc00
80+ and #%00001000
81+ bne not_right
82+ inc $d000 ; Move ship right (increase X)
83+ inc $d000
84+not_right:
6185
6286 jmp game_loop
6387
The complete program
; Starfield - Unit 2: Joystick Movement
; Cumulative steps: step-00 (static ship) -> step-01 (+ game loop, up) -> step-02 (+ all directions)
; Assemble: acme -f cbm -o <step>.prg <step>.asm

; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Initialisation
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen
        ldx #$00
-       lda #$20
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

        ; Sprite 0 setup (ship)
        lda #128
        sta $07f8           ; Data pointer (block 128 = $2000)
        lda #172
        sta $d000           ; X position
        lda #220
        sta $d001           ; Y position
        lda #$01
        sta $d027           ; Colour (white)
        lda #%00000001
        sta $d015           ; Enable sprite 0

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
        ; This syncs our code to the display (~50Hz PAL)
-       lda $d012
        cmp #$ff
        bne -

        ; --- Read joystick and move ship ---

        ; UP (bit 0)
        lda $dc00           ; Read joystick port 2
        and #%00000001      ; Isolate bit 0
        bne not_up          ; Bit is 1 = NOT pressed (active low)
        dec $d001           ; Move ship up (decrease Y)
        dec $d001           ; 2 pixels per frame
not_up:

        ; DOWN (bit 1)
        lda $dc00
        and #%00000010
        bne not_down
        inc $d001           ; Move ship down (increase Y)
        inc $d001
not_down:

        ; LEFT (bit 2)
        lda $dc00
        and #%00000100
        bne not_left
        dec $d000           ; Move ship left (decrease X)
        dec $d000
not_left:

        ; RIGHT (bit 3)
        lda $dc00
        and #%00001000
        bne not_right
        inc $d000           ; Move ship right (increase X)
        inc $d000
not_right:

        jmp game_loop

; ------------------------------------------------
; Sprite data at $2000 (block 128)
; ------------------------------------------------
*= $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   ;

Now the ship goes anywhere. Diagonals fall out for free: push up-right and both the up check and the right check fire in the same frame.

The ship flies right, then up, then diagonally up-and-left, then down — following the stick. Pushing two directions at once moves it on both axes.

INC is the mirror of DEC — add one to the value at an address. Down and right INC the position register; up and left DEC it. Each direction calls its instruction twice, so the ship moves two pixels a frame: at 50 frames a second, that's 100 pixels a second, about two seconds to cross the screen.

That speed is a choice, and it's worth feeling the difference. One pixel a frame is slow and precise; three is quick and a little twitchy; four starts to feel like it's sliding out from under you. Two is a deliberate middle — responsive without overshooting. Movement speed is the first place a game starts to feel like something, and it's a single number you control.

When it's wrong, see why

Input bugs are confusing because nothing on screen explains them — the ship just moves wrong, or not at all. The fix is to stop guessing and look at the byte the program reads. If your emulator has a monitor, peek $dc00 while you hold the stick:

  • At rest it reads $ff (%11111111) — every switch open.
  • Push up and it reads $fe — bit 0 has dropped to 0.
  • Push up-right and it reads $f6 (%11110110) — bits 0 and 3 dropped.

If the ship moves the wrong way, that byte tells you whether the problem is your reading (wrong bit) or your moving (wrong INC/DEC). If pushing up gives you $fd instead of $fe, you're testing down's bit, not up's. Watching the value the program sees beats re-reading your own code hoping to spot the slip.

Before and after

We started with a ship that only sat there and finished with one the player flies anywhere on screen — the moment it stopped being a program and started being a game.

Try this

  • Change the speed. Add or remove an INC/DEC in each direction. One each is slow and precise; three each is fast and twitchy. Feel where it stops being comfortable.
  • Forward only. Comment out the whole LEFT block. The ship can move up, down and right but never left — the trick scrolling shooters use to stop you backing out of a level.

What you've learnt

  • The game loop — wait for the frame, read input, update, repeat. The raster wait at $d012 is what locks it to the display.
  • Reading the joystick — control port 2 at $dc00 is one byte, one bit per switch, active-low (0 = pressed).
  • AND masking, in anger — the same bit-isolating you met in the Primer, now picking one direction out of the port.
  • BNE to skip — branch past the movement when a direction isn't pressed.
  • INC and DEC — change a memory-mapped register in one instruction, moving the sprite directly.

What's next

The ship moves — but nothing stops it sliding off the edge of the screen and vanishing. Next you'll read its position and hold it inside the visible area, so it always stays where you can see it.