Code status: assembled and emulator-executed. PAL and NTSC C64, normal-size single-colour sprite 0, in Emu198x and VICE. The recorded checks identify the tested sources and observations. They establish these cases on those configurations; original-hardware checks are separate.
Move the sprite
This example puts a white 24×21-pixel sprite on a black screen. Use joystick port 2 to move it. Hold a direction and it reaches the edge, then stays there. Move diagonally and both coordinates change. Release the stick and the sprite stops.
The movement rule takes two pixels per update and clamps each coordinate to its permitted range. If Y=51 and you move up, the proposed position is 49, so the result is 50. Rejecting that step would leave a one-pixel gap; clamping reaches the edge.
Run it
Use the maintained samples checkout, keeping its folder structure. The demonstration reuses the setup routine and shape from Hardware Sprites.
git clone https://github.com/code198x/code-samples.git
cd code-samples/commodore-64/patterns/assembly/physics/sprite-movement-bounds
makemake uses Asm198x to produce build/demo.prg. Load it in a PAL or NTSC C64 emulator, type RUN, and map your controls to port 2. Reset to leave.
The standalone program takes over the screen, VIC bank, sprites, CIA port directions and interrupt masks. It maps out BASIC and KERNAL and does not return to BASIC. It is not a subroutine you can drop into a program that still relies on those services.
; Load, RUN, then use joystick port 2. Reset to leave.
; It owns the screen, VIC bank, CIA interrupt masks and sprite registers.
DEMO_X = 160
DEMO_Y = 140
SHARED_SEED = 0 ; verification seeds bits belonging to other sprites
* = $0801
!byte $0b, $08, $0a, $00, $9e
!text "2064"
!byte 0, 0, 0
* = $0810
start:
sei
lda #$7f
sta $dc0d
sta $dd0d
lda $dc0d
lda $dd0d
lda #$35
sta $01 ; RAM at BASIC/KERNAL, I/O visible
lda #<nmi_return
sta $fffa
lda #>nmi_return
sta $fffb
lda $dd02
ora #3
sta $dd02
lda $dd00
ora #3
sta $dd00 ; VIC bank 0
lda #$14
sta $d018 ; screen $0400, character ROM $1000
lda #$1b
sta $d011 ; display on, normal text
lda #$08
sta $d016 ; 40 columns, single-colour text
lda #0
sta $d015
sta $d020
sta $d021
ldx #0
lda #32
clear_screen:
sta $0400, x
sta $0500, x
sta $0600, x
sta $06e8, x
inx
bne clear_screen
; Blank shapes keep the other sprites invisible during ownership checks.
ldx #7
lda #$81
blank_pointers:
sta $07f8, x
dex
bpl blank_pointers
lda #SHARED_SEED
sta $d015
ora #1 ; sprite 0 starts with wrong high/mode bits
sta $d010
sta $d017
sta $d01b
sta $d01c
sta $d01d
lda #<DEMO_X
ldx #>DEMO_X
ldy #DEMO_Y
jsr sprite0_show
lda #0
sta $dc02 ; both CIA1 ports input: no keyboard scanning
sta $dc03
lda #<DEMO_X
sta sprite_x_lo
lda #>DEMO_X
sta sprite_x_hi
lda #DEMO_Y
sta sprite_y
jmp wait_top
; Guest snapshots make input, movement and shared bits observable.
copy_registers_start:
ldx #0
copy_registers:
lda $d000, x
sta observed, x
inx
cpx #$2f
bne copy_registers
inc frames
frame_done:
jmp wait_top
wait_top:
bit $d011
bmi wait_top
wait_picture_end:
lda $d012
cmp #250
bne wait_picture_end
picture_ready: ; debugger observation point; completed picture
wait_bottom:
bit $d011
bpl wait_bottom
jsr read_joystick2
jsr move_sprite
jmp copy_registers_start
nmi_return:
rti
frames:
!byte 0
observed:
!fill $2f, 0
read_joystick2:
lda $dc00
eor #$ff
and #$0f
rts
!source "movement.inc"
!source "../../rendering/hardware-sprites/sprite.inc"
!source "../../rendering/hardware-sprites/shape.inc"
blank_shape:
!fill 64, 0The caller reads $DC00 once per update. Its direction switches are active low, so EOR #$FF turns pressed switches into set bits. Both CIA1 ports are configured as inputs and keyboard scanning is disabled. That ownership matters when combining joystick code with a keyboard reader.
The movement contract
move_sprite takes pressed direction bits in A: up=1, down=2, left=4, right=8. It reads and updates sprite_x_lo, sprite_x_hi and sprite_y, then writes sprite 0’s VIC position. It changes A and flags, preserves X and Y, and keeps its input in joy_state.
Call with the processor in binary arithmetic mode (decimal flag clear). State must start inside X=24..320, Y=50..229, inclusive, with X’s high byte equal to 0 or 1. The routine assumes a normal-size sprite and the display window set up by this caller.
Opposite directions cancel on their axis. Up+down therefore leaves Y alone; left+right leaves X alone. A diagonal applies both axes, moving two pixels horizontally and two vertically. It covers more distance than a straight move; there is no diagonal speed normalisation. Fire does nothing here.
; A = pressed directions: bit 0 up, 1 down, 2 left, 3 right.
; Opposites cancel per axis; fire and other bits are ignored.
; State must start within X=24..320, Y=50..229. Two pixels per call.
; Clobbers A and flags; preserves X/Y. Owns sprite 0 position, not its modes.
MIN_X = 24
MAX_X = 320
MIN_Y = 50
MAX_Y = 229
MOVE_SPEED = 2
move_sprite:
sta joy_state
and #3
cmp #1
beq move_up
cmp #2
beq move_down
jmp move_horizontal
move_up:
lda sprite_y
sec
sbc #MOVE_SPEED
cmp #MIN_Y
bcs store_y
lda #MIN_Y
jmp store_y
move_down:
lda sprite_y
clc
adc #MOVE_SPEED
cmp #MAX_Y
bcc store_y
lda #MAX_Y
store_y:
sta sprite_y
move_horizontal:
lda joy_state
and #12
cmp #4
beq move_left
cmp #8
beq move_right
jmp update_sprite_pos
move_left:
lda sprite_x_lo
sec
sbc #MOVE_SPEED
sta sprite_x_lo
lda sprite_x_hi
sbc #0
sta sprite_x_hi
bne update_sprite_pos
lda sprite_x_lo
cmp #MIN_X
bcs update_sprite_pos
lda #MIN_X
sta sprite_x_lo
jmp update_sprite_pos
move_right:
lda sprite_x_lo
clc
adc #MOVE_SPEED
sta sprite_x_lo
lda sprite_x_hi
adc #0
sta sprite_x_hi
cmp #>MAX_X
bcc update_sprite_pos
bne clamp_right
lda sprite_x_lo
cmp #<MAX_X
bcc update_sprite_pos
clamp_right:
lda #<MAX_X
sta sprite_x_lo
lda #>MAX_X
sta sprite_x_hi
update_sprite_pos:
lda sprite_x_lo
sta $d000
lda sprite_y
sta $d001
lda $d010
and #$fe
ora sprite_x_hi
sta $d010
rts
movement_end:
sprite_x_lo: !byte 160
sprite_x_hi: !byte 0
sprite_y: !byte 140
joy_state: !byte 0Crossing X=255
The VIC-II splits a sprite’s X coordinate between a low-byte register and a bit in $D010. Moving right from 254 gives 256: the low byte becomes 0 and the high bit becomes 1. Moving left from 256 gives 254: the borrow clears that high bit again.
update_sprite_pos clears only bit 0 of $D010, then inserts sprite 0’s high bit. Bits 1–7 belong to the other sprites and remain unchanged. The routine also leaves their positions, colours and modes alone. The caller must exclude a competing writer during the read/modify/write; preserving bits does not make the sequence atomic.
Inclusive screen bounds
These are sprite coordinates for the normal 40-column, 25-row window:
| Axis | Visible coordinates | Sprite size | Last fully visible origin |
|---|---|---|---|
| X | 24 through 343 | 24 pixels | 343 - (24 - 1) = 320 |
| Y | 50 through 249 | 21 lines | 249 - (21 - 1) = 229 |
An origin plus size - 1 gives the final occupied coordinate. That is why the lower bound includes Y=229, rather than stopping at 228. Expansion or a different display window needs different bounds.
One update per frame
The caller polls the raster and updates below the displayed area, after the sprite has been drawn. Its frame counter and register snapshot make progress observable to the verifier. This is a small polling loop with exclusive hardware ownership, not an interrupt-driven game framework.
Two pixels per frame means about 100 pixels per second on PAL and 120 on NTSC for straight movement. Keeping the same speed per second would require a separate regional timing policy. This example preserves the simple frame-based rule.
Trade-offs
| Aspect | Cost or limit |
|---|---|
| Code | 160 assembled bytes for the movement routine, excluding the caller and sprite setup |
| State | Four bytes for X low/high, Y and the sampled direction mask |
| Display | A 64-byte sprite block plus its pointer; sprite DMA still takes CPU bus time |
| CPU | Work depends on the selected directions and clamping path; no measured cycle bound is claimed |
| Movement | Normal-size sprite 0, fixed bounds, two pixels per update; diagonals and PAL/NTSC have the speed differences described above |
What has been checked
The verification recipe compares Asm198x and ACME output, then executes 6,144 arithmetic/register cases across Emu198x and VICE on PAL and NTSC. It covers both sides of every boundary, odd and even coordinates, X=255/256, all direction/fire combinations and preservation of seeded high bits belonging to other sprites.
A separate sequence drives the emulator control ports and checks 48 captures, including held input, release, both X-byte transitions, corners, diagonals and opposite directions. It reads state first, then releases the controls for one unchanged frame before checking every lit pixel in the completed picture. Injected direction-mask tests alone do not establish joystick wiring.
The old listing’s boundary failures are reproduced, and deliberately removing a clamp fails the same observer. These are configuration-specific emulator checks, not physical joystick or original-hardware tests.
Related
Patterns: Hardware Sprites, Joystick Reading, Game Loop (Basic)
Vault: Commodore 64
Sources
Commodore, Commodore 64 Programmer’s Reference Guide (1983), chapter 3, pp. 131–144 (sprite size and positioning), and pp. 343–344 (control ports, direction switches and active-low input). Clamping, opposite-direction cancellation and diagonal speed are the example’s software choices.