Code status: illustrative pseudocode. Adapt the listing to your program and check it on your target machine. This page links no assembly or execution record for the listing itself.
Overview
Our 6502, Z80 and 68000 examples use integer instructions rather than a floating-point unit. Fixed-point math lets you work with fractional values using integers. The trick: store position as two bytes — the high byte is the pixel position, the low byte is the sub-pixel fraction.
This gives you smooth movement at any speed, even slower than 1 pixel per frame.
Concept
; 8.8 fixed-point: 8 bits integer, 8 bits fraction
; Value $0180 = 1.5 (1 pixel + 128/256 = 1.5)
; Value $0040 = 0.25 (0 pixels + 64/256 = 0.25)
position_lo: byte ; Fractional part (0-255 = 0.0 to 0.996)
position_hi: byte ; Integer part (pixel position)
velocity_lo: byte ; Fractional velocity
velocity_hi: byte ; Integer velocity (usually 0 for slow movement)Pseudocode
; Player position (8.8 fixed-point)
player_x_lo: byte = 0 ; Fraction
player_x_hi: byte = 100 ; Pixel position
; Velocity: 0.75 pixels per frame
; 0.75 * 256 = 192 = $C0
velocity_x_lo: byte = $C0
velocity_x_hi: byte = 0
; Update position each frame
update_position:
; Add velocity to position (16-bit add)
player_x_lo = player_x_lo + velocity_x_lo
if carry:
player_x_hi = player_x_hi + 1
player_x_hi = player_x_hi + velocity_x_hi
; Use player_x_hi as sprite X position
sprite_x = player_x_hi
returnImplementation Notes
6502:
; Position
player_x_lo: !byte 0
player_x_hi: !byte 100
; Velocity (0.75 px/frame = $00C0)
velocity_x_lo: !byte $C0
velocity_x_hi: !byte $00
update_position:
clc
lda player_x_lo
adc velocity_x_lo
sta player_x_lo
lda player_x_hi
adc velocity_x_hi
sta player_x_hi
rtsZ80:
; Add 16-bit velocity to 16-bit position
update_position:
ld hl,(player_x) ; L=lo, H=hi
ld bc,(velocity_x) ; C=lo, B=hi
add hl,bc
ld (player_x),hl
retCommon Velocities
| Speed (px/frame) | Fixed-point value | Hex |
|---|---|---|
| 0.25 | 64 | $0040 |
| 0.5 | 128 | $0080 |
| 0.75 | 192 | $00C0 |
| 1.0 | 256 | $0100 |
| 1.5 | 384 | $0180 |
| 2.0 | 512 | $0200 |
| 2.5 | 640 | $0280 |
Trade-offs
| Aspect | Cost |
|---|---|
| CPU | Two byte additions on an 8-bit CPU, plus loads, stores and the call; measure the chosen memory placement |
| Memory | 2 bytes per axis (vs 1 for integer) |
| Precision | 1/256 pixel (~0.004 pixel resolution) |
When to use: Any movement that isn’t exactly integer pixels per frame — gravity, acceleration, diagonal movement, slow enemies.
When to avoid: Grid-based games where everything moves in whole tiles.
Gravity Example
SCALE = 256
GRAVITY = 26 ; about 0.1 pixel per update per update
TERMINAL = 4 * SCALE ; 4 pixels per update, downward
velocity_y = 0 ; signed scaled integer; negative means upward
apply_gravity:
candidate = velocity_y + GRAVITY
velocity_y = MIN(candidate, TERMINAL)Compute candidate in a representation wide enough for the addition. On an 8-bit machine, propagate the low-byte carry and use a signed comparison of the complete value. An unsigned test of the high byte would mistake $FF80 (−0.5) for a huge downward speed. For example, −128 + 26 = −102 scaled units: a jumping object is still moving upward.
This version assumes the initial velocity is at most TERMINAL and fits the chosen signed range. Decide how the game handles a value outside that range. The position addition also needs a boundary policy: wrapping 16 bits is arithmetic behaviour, not automatic collision or clamping.
Signed Movement
For movement in both directions, use signed arithmetic or separate positive/negative velocities. Another option is to store a positive magnitude and choose addition or subtraction from the direction.
For genuine signed fixed-point, treat the high byte as a signed two’s-complement integer. On 8.8, that gives a range of -128.0 to +127.996 pixels. Negative velocities work naturally with ADC because the carry semantics are the same — $FF80 (-0.5) added to $0100 (1.0) gives $0080 (0.5), with the carry rolling out as expected:
; Velocity: -0.5 px/frame = $FF80 in signed 8.8
velocity_x_lo: !byte $80
velocity_x_hi: !byte $FF ; Signed -1 in high byte
; Decimal mode must be clear on a 6502/6510. The same binary CLC/ADC sequence works
update_position:
clc
lda player_x_lo
adc velocity_x_lo
sta player_x_lo
lda player_x_hi
adc velocity_x_hi
sta player_x_hi
rts16.16 fixed-point for higher precision
When pixel-level resolution isn’t enough — fast scrollers, sub-pixel collision, or very slow drift — bump to 16.16 fixed-point (16 bits integer + 16 bits fraction = 4 bytes per axis):
| Speed | Fixed value | Bytes |
|---|---|---|
| 0.001 px/frame | 65 | $00000041 |
| 0.5 | 32,768 | $00008000 |
| 1.0 | 65,536 | $00010000 |
| Fastest meaningful | varies by pixel range |
The arithmetic costs more (32-bit add = 4 byte adds) but precision rises to 1/65,536 pixel — useful for parallax layers moving at fractional rates per frame, or for accumulated drift in long-running effects. The 68000 makes this cheap (ADD.L is one instruction); on 8-bit CPUs it’s a 4-byte chained add.