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

Screen Edges

Push the ship to the right and it wraps off the screen. Add a ninth X bit and boundary clamping so it reaches the full width and stops cleanly at every edge.

19% of Starfield

The ship moves now — but push it right and something's wrong. It slides across, then jumps straight back to the left edge, and the rightmost slice of screen is unreachable. This unit fixes both problems: a ninth bit so the ship can reach the whole screen, and boundary checks so it stops at the edges instead of wrapping.

Where we start

Here's the trouble. Hold right on Unit 2's ship and watch it wrap:

The ship slides right, reaches about seven-eighths across, then jumps back to the left edge — the X position wrapped past 255, and the right strip of screen is unreachable.

The cause is the X register's range. $d000 holds the ship's X position in a single byte, so it counts 0–255. But the visible screen runs wider than that — to about X=344. When the ship passes 255, the byte wraps to 0 and the ship leaps to the left. Sixty-odd pixels on the right are out of reach.

Milestone 1 — the ninth bit

The VIC-II keeps a ninth X bit for each sprite in register $d010 — one bit per sprite, bit 0 for sprite 0. With it clear, the ship's X is just $d000 (0–255). With it set, the position is $d000 + 256. Together they count 0–511, more than enough for the full width.

$D010 — Sprite X high bits (bit 8) 7 Spr 7 0 6 Spr 6 0 5 Spr 5 0 4 Spr 4 0 3 Spr 3 0 2 Spr 2 0 1 Spr 1 0 0 Spr 0 1
Bit 0 is sprite 0's ninth X bit. Set, it adds 256 to the ship's position — the right half of the screen.

So when the low byte wraps — $ff$00 going right, or $00$ff going left — we flip that ninth bit. Flipping a single bit is exactly the eor you met in the Primer:

Step 1: carry the ninth bit with EOR
+28-5
3838 sta $d027 ; Colour (white)
3939 lda #%00000001
4040 sta $d015 ; Enable sprite 0
41+ lda #$00
42+ sta $d010 ; sprite high-X bits clear (ship starts under X=256)
4143
4244 ; ------------------------------------------------
4345 ; Game loop — runs once per frame
...
6769 inc $d001
6870 not_down:
6971
70- ; LEFT (bit 2)
72+ ; LEFT (bit 2) — 9-bit X
7173 lda $dc00
7274 and #%00000100
7375 bne not_left
74- dec $d000 ; Move ship left (decrease X)
75- dec $d000
76+ ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
77+ lda $d000
78+ bne +
79+ lda $d010
80+ eor #$01 ; the eor bit-flip from the Primer, on sprite 0's high X bit
81+ sta $d010
82++ dec $d000
83+ lda $d000
84+ bne +
85+ lda $d010
86+ eor #$01
87+ sta $d010
88++ dec $d000
7689 not_left:
7790
78- ; RIGHT (bit 3)
91+ ; RIGHT (bit 3) — 9-bit X
7992 lda $dc00
8093 and #%00001000
8194 bne not_right
82- inc $d000 ; Move ship right (increase X)
95+ ; after each step, flip the 9th bit when X wraps $ff -> $00
8396 inc $d000
97+ bne +
98+ lda $d010
99+ eor #$01
100+ sta $d010
101++ inc $d000
102+ bne +
103+ lda $d010
104+ eor #$01
105+ sta $d010
106++
84107 not_right:
85108
86109 jmp game_loop
The complete step 1 program
; Starfield - Unit 3: Screen Edges
; Cumulative steps: step-00 (ship + 4-way joystick, 8-bit X) -> step-01 (+ 9th X bit) -> step-02 (+ edge clamping)
; 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
        lda #$00
        sta $d010           ; sprite high-X bits clear (ship starts under X=256)

; ------------------------------------------------
; 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) — 9-bit X
        lda $dc00
        and #%00000100
        bne not_left
        ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
        lda $d000
        bne +
        lda $d010
        eor #$01            ; the eor bit-flip from the Primer, on sprite 0's high X bit
        sta $d010
+       dec $d000
        lda $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       dec $d000
not_left:

        ; RIGHT (bit 3) — 9-bit X
        lda $dc00
        and #%00001000
        bne not_right
        ; after each step, flip the 9th bit when X wraps $ff -> $00
        inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+
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   ;

Going right, we inc $d000; if it wrapped to $00, eor #$01 flips the ninth bit on. Going left, we check for $00 before decrementing and flip the bit back off. eor doesn't care which way the bit is currently set — it just flips it — so the same instruction handles both crossings. Now the ship reaches the whole width:

With the ninth bit carried across, holding right takes the ship smoothly past the old wrap point all the way to the right edge.

Milestone 2 — stop at the edges

Reaching the edge is progress, but nothing stops the ship running clean off it and wrapping round. We hold it in with boundary clamping: before each move, compare the position against the limit and skip the move if it's already there. That cmp-then-branch is the same compare-and-decide from the Primer.

Step 2: clamp to the visible area
+24-4
5353
5454 ; --- Read joystick and move ship ---
5555
56- ; UP (bit 0)
56+ ; UP (bit 0) — clamp to Y >= 50
5757 lda $dc00 ; Read joystick port 2
5858 and #%00000001 ; Isolate bit 0
5959 bne not_up ; Bit is 1 = NOT pressed (active low)
60+ lda $d001
61+ cmp #52 ; 50 + room for a 2-pixel move
62+ bcc not_up ; already at the top — don't move
6063 dec $d001 ; Move ship up (decrease Y)
6164 dec $d001 ; 2 pixels per frame
6265 not_up:
6366
64- ; DOWN (bit 1)
67+ ; DOWN (bit 1) — clamp to Y <= 234
6568 lda $dc00
6669 and #%00000010
6770 bne not_down
71+ lda $d001
72+ cmp #233 ; 234 - room for a 2-pixel move
73+ bcs not_down ; already at the bottom — don't move
6874 inc $d001 ; Move ship down (increase Y)
6975 inc $d001
7076 not_down:
7177
72- ; LEFT (bit 2) — 9-bit X
78+ ; LEFT (bit 2) — 9-bit X, clamp to X >= 24
7379 lda $dc00
7480 and #%00000100
7581 bne not_left
82+ lda $d010
83+ and #$01
84+ bne left_ok ; high bit set: X >= 256, always safe to go left
85+ lda $d000
86+ cmp #26 ; 24 + room for a 2-pixel move
87+ bcc not_left ; already at the left edge — don't move
88+left_ok:
7689 ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
7790 lda $d000
7891 bne +
...
88101 + dec $d000
89102 not_left:
90103
91- ; RIGHT (bit 3) — 9-bit X
104+ ; RIGHT (bit 3) — 9-bit X, clamp to X <= 320
92105 lda $dc00
93106 and #%00001000
94107 bne not_right
108+ lda $d010
109+ and #$01
110+ beq right_ok ; high bit clear: X < 256, always safe to go right
111+ lda $d000
112+ cmp #63 ; (320 - 256) - room for a 2-pixel move
113+ bcs not_right ; already at the right edge — don't move
114+right_ok:
95115 ; after each step, flip the 9th bit when X wraps $ff -> $00
96116 inc $d000
97117 bne +
The complete program
; Starfield - Unit 3: Screen Edges
; Cumulative steps: step-00 (ship + 4-way joystick, 8-bit X) -> step-01 (+ 9th X bit) -> step-02 (+ edge clamping)
; 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
        lda #$00
        sta $d010           ; sprite high-X bits clear (ship starts under X=256)

; ------------------------------------------------
; 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) — clamp to Y >= 50
        lda $dc00           ; Read joystick port 2
        and #%00000001      ; Isolate bit 0
        bne not_up          ; Bit is 1 = NOT pressed (active low)
        lda $d001
        cmp #52             ; 50 + room for a 2-pixel move
        bcc not_up          ; already at the top — don't move
        dec $d001           ; Move ship up (decrease Y)
        dec $d001           ; 2 pixels per frame
not_up:

        ; DOWN (bit 1) — clamp to Y <= 234
        lda $dc00
        and #%00000010
        bne not_down
        lda $d001
        cmp #233            ; 234 - room for a 2-pixel move
        bcs not_down        ; already at the bottom — don't move
        inc $d001           ; Move ship down (increase Y)
        inc $d001
not_down:

        ; LEFT (bit 2) — 9-bit X, clamp to X >= 24
        lda $dc00
        and #%00000100
        bne not_left
        lda $d010
        and #$01
        bne left_ok         ; high bit set: X >= 256, always safe to go left
        lda $d000
        cmp #26             ; 24 + room for a 2-pixel move
        bcc not_left        ; already at the left edge — don't move
left_ok:
        ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
        lda $d000
        bne +
        lda $d010
        eor #$01            ; the eor bit-flip from the Primer, on sprite 0's high X bit
        sta $d010
+       dec $d000
        lda $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       dec $d000
not_left:

        ; RIGHT (bit 3) — 9-bit X, clamp to X <= 320
        lda $dc00
        and #%00001000
        bne not_right
        lda $d010
        and #$01
        beq right_ok        ; high bit clear: X < 256, always safe to go right
        lda $d000
        cmp #63             ; (320 - 256) - room for a 2-pixel move
        bcs not_right       ; already at the right edge — don't move
right_ok:
        ; after each step, flip the 9th bit when X wraps $ff -> $00
        inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+
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   ;

Up and down are one comparison each — cmp the Y position against the top (50) or bottom (234) and branch away if we're there. Left and right need the ninth bit: when it's set the ship is past X=255 and always safe to move left, so we only compare the low byte against the edge when the bit is clear (and the mirror for the right). Now it runs to each edge and stops:

Clamped: the ship runs to the right edge and stops dead, then to the left edge and stops — always fully on screen, never wrapping.

When it's wrong, see why

Two failure shapes here, and the values to check for each:

  • Ship still teleports across the screen. The ninth bit isn't keeping up. Check that $d010 flips on both the right wrap ($ff$00) and the left wrap ($00$ff). If it flips going one way but not the other, the ship jumps the moment it crosses 256.
  • Ship runs off an edge, or stops too early. A clamp comparison is off. The limits are Y ≥ 52 (top), Y ≤ 233 (bottom), X ≥ 26 (left), and X ≤ 63 with the ninth bit set (right). Walk the value the ship reaches at the edge against the number it's compared with — an off-by-one there shows up as a one-pixel gap or a sprite half in the border.

Before and after

We started with a ship that wrapped off the screen at the three-quarter mark and finished with one that reaches the full width and stops cleanly at every edge.

Try this

  • Asteroids wrap. Take the clamps back out and let the ninth bit run: the ship leaves the right edge and reappears on the left. That wrap was a bug a moment ago — here it's a design choice. Which feels right for a shooter?
  • A tighter cage. Raise the left limit and lower the right so the ship can only roam the middle of the screen. Boundaries are just numbers; move them and the playfield changes.

What you've learnt

  • The ninth X bit ($d010) — one bit per sprite extends its X range past 255 to the full screen width.
  • eor to carry the bit — flipping $d010 on each wrap, the same single-bit flip you used in the Primer.
  • Boundary clampingcmp against a limit and branch away to hold a value in range.
  • Nine-bit comparisons — checking the ninth bit first, then the low byte, to clamp a position that spans two registers.

What's next

The ship goes anywhere on screen now, but it's unarmed. Next you'll read the fire button and launch a bullet — the ship's first shot.