Skip to content

Fixed-Point Math

Fractional values without floating point. Essential for smooth movement and physics on 8/16-bit CPUs.

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

Code
; 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

Code
; 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
    return

Implementation Notes

6502:

Assembly
; 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
    rts

Z80:

Assembly
; 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
    ret

Common 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

Code
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:

Assembly
; 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
    rts

16.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.