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.
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 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.
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:
| 38 | 38 | sta $d027 ; Colour (white) | |
| 39 | 39 | lda #%00000001 | |
| 40 | 40 | sta $d015 ; Enable sprite 0 | |
| 41 | + | lda #$00 | |
| 42 | + | sta $d010 ; sprite high-X bits clear (ship starts under X=256) | |
| 41 | 43 | | |
| 42 | 44 | ; ------------------------------------------------ | |
| 43 | 45 | ; Game loop — runs once per frame | |
| ... | |||
| 67 | 69 | inc $d001 | |
| 68 | 70 | not_down: | |
| 69 | 71 | | |
| 70 | - | ; LEFT (bit 2) | |
| 72 | + | ; LEFT (bit 2) — 9-bit X | |
| 71 | 73 | lda $dc00 | |
| 72 | 74 | and #%00000100 | |
| 73 | 75 | 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 | |
| 76 | 89 | not_left: | |
| 77 | 90 | | |
| 78 | - | ; RIGHT (bit 3) | |
| 91 | + | ; RIGHT (bit 3) — 9-bit X | |
| 79 | 92 | lda $dc00 | |
| 80 | 93 | and #%00001000 | |
| 81 | 94 | bne not_right | |
| 82 | - | inc $d000 ; Move ship right (increase X) | |
| 95 | + | ; after each step, flip the 9th bit when X wraps $ff -> $00 | |
| 83 | 96 | 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 | + | + | |
| 84 | 107 | not_right: | |
| 85 | 108 | | |
| 86 | 109 | 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:
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.
| 53 | 53 | | |
| 54 | 54 | ; --- Read joystick and move ship --- | |
| 55 | 55 | | |
| 56 | - | ; UP (bit 0) | |
| 56 | + | ; UP (bit 0) — clamp to Y >= 50 | |
| 57 | 57 | lda $dc00 ; Read joystick port 2 | |
| 58 | 58 | and #%00000001 ; Isolate bit 0 | |
| 59 | 59 | 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 | |
| 60 | 63 | dec $d001 ; Move ship up (decrease Y) | |
| 61 | 64 | dec $d001 ; 2 pixels per frame | |
| 62 | 65 | not_up: | |
| 63 | 66 | | |
| 64 | - | ; DOWN (bit 1) | |
| 67 | + | ; DOWN (bit 1) — clamp to Y <= 234 | |
| 65 | 68 | lda $dc00 | |
| 66 | 69 | and #%00000010 | |
| 67 | 70 | 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 | |
| 68 | 74 | inc $d001 ; Move ship down (increase Y) | |
| 69 | 75 | inc $d001 | |
| 70 | 76 | not_down: | |
| 71 | 77 | | |
| 72 | - | ; LEFT (bit 2) — 9-bit X | |
| 78 | + | ; LEFT (bit 2) — 9-bit X, clamp to X >= 24 | |
| 73 | 79 | lda $dc00 | |
| 74 | 80 | and #%00000100 | |
| 75 | 81 | 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: | |
| 76 | 89 | ; before each step, flip the 9th bit when X is about to wrap $00 -> $ff | |
| 77 | 90 | lda $d000 | |
| 78 | 91 | bne + | |
| ... | |||
| 88 | 101 | + dec $d000 | |
| 89 | 102 | not_left: | |
| 90 | 103 | | |
| 91 | - | ; RIGHT (bit 3) — 9-bit X | |
| 104 | + | ; RIGHT (bit 3) — 9-bit X, clamp to X <= 320 | |
| 92 | 105 | lda $dc00 | |
| 93 | 106 | and #%00001000 | |
| 94 | 107 | 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: | |
| 95 | 115 | ; after each step, flip the 9th bit when X wraps $ff -> $00 | |
| 96 | 116 | inc $d000 | |
| 97 | 117 | 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:
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
$d010flips 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. eorto carry the bit — flipping$d010on each wrap, the same single-bit flip you used in the Primer.- Boundary clamping —
cmpagainst 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.