Skip to content

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.

  • Commodore 64
  • Assembly

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)7Spr 706Spr 605Spr 504Spr 403Spr 302Spr 201Spr 100Spr 01
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 added−5 removed
        sta $d027           ; Colour (white)
        lda #%00000001
        sta $d015           ; Enable sprite 0
Added:         lda #$00
Added:         sta $d010           ; sprite high-X bits clear (ship starts under X=256)

; ------------------------------------------------
; Game loop — runs once per frame
…        inc $d001
not_down:

Removed:         ; LEFT (bit 2)
Added:         ; LEFT (bit 2) — 9-bit X
        lda $dc00
        and #%00000100
        bne not_left
Removed:         dec $d000           ; Move ship left (decrease X)
Removed:         dec $d000
Added:         ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
Added:         lda $d000
Added:         bne +
Added:         lda $d010
Added:         eor #$01            ; the eor bit-flip from the Primer, on sprite 0's high X bit
Added:         sta $d010
Added: +       dec $d000
Added:         lda $d000
Added:         bne +
Added:         lda $d010
Added:         eor #$01
Added:         sta $d010
Added: +       dec $d000
not_left:

Removed:         ; RIGHT (bit 3)
Added:         ; RIGHT (bit 3) — 9-bit X
        lda $dc00
        and #%00001000
        bne not_right
Removed:         inc $d000           ; Move ship right (increase X)
Added:         ; after each step, flip the 9th bit when X wraps $ff -> $00
        inc $d000
Added:         bne +
Added:         lda $d010
Added:         eor #$01
Added:         sta $d010
Added: +       inc $d000
Added:         bne +
Added:         lda $d010
Added:         eor #$01
Added:         sta $d010
Added: +
not_right:

        jmp game_loop
The complete step 1 program
step-01.asm
; 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 added−4 removed

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

Removed:         ; UP (bit 0)
Added:         ; 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)
Added:         lda $d001
Added:         cmp #52             ; 50 + room for a 2-pixel move
Added:         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:

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

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

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

Run it here

This unit's program, built for you.

Program
step-02.prg
Machine
Commodore 64 (PAL Breadbin)

Download step-02.prg