The previous articles covered how Exile (BBC Micro, 1988) generates its world and draws it. This part is about the thing the game was actually famous for: the physics. Exile’s marketing called it the first game with realistic physics, and for once the marketing undersold the engineering — gravity, momentum, drag, buoyancy, wind, bouncing, all computed for every active object, in 8-bit arithmetic on the same 2MHz 6502 that was also generating the world and drawing the screen.
As before, the code here is reimplemented from the disassembly in TypeScript and verified bit-identical against the emulated original. The tests run one hundred thousand fuzzed object states, one full physics tick each, zero mismatches (plus twenty-five thousand more with real object update routines running). And as before there’s an interactive demo on this page — a physics sandbox on the real map, steppable one tick at a time, with overlays and a live register panel showing what the machine is doing.
The number format
An object’s position is a pair per axis: a tile coordinate and an 8-bit fraction. 256 fractions to a tile, which is 16 fractions per pixel horizontally and 8 vertically, because tiles are 16×32. Velocities are signed 8-bit fractions-per-tick, so the fastest anything normally moves is a quarter of a tile per tick.
Adding a velocity to a position is where the carry flag earns its keep again:
&2a6b CLC
&2a6c ADC &4f,X ; fraction += velocity
&2a6e STA &4f,X
&2a70 BCC skip
&2a72 INC &53,X ; carry ripples into the tile coordinate
A negative velocity pre-decrements the tile coordinate and lets the carry from the fraction add correct it back if there was no underflow. Two instructions of cleverness per axis, every object, every tick.
One tick
▶ demo: drop a boulder down a real shaft ⌗ 6502 source
Each of the sixteen object slots gets the same treatment per update
(update_objects, &1a2f):
- Copy the slot into a zero-page working mirror
- Look up the object’s weight (three bits of its type flags) and sprite extents
- Integrate velocity into position
- Collide against water and tiles
- Resolve support/wedging
- Apply per-type behaviour
- Finally apply accelerations to velocities
It’s in that last step that gravity lives, in the most 6502 way imaginable:
&1f36 CPX #&02 ; X = 2 on the y pass: sets the carry
&1f38 LDA &40,X ; acceleration
&1f3b ADC &43,X ; velocity + acceleration + CARRY
The y-axis pass compares the loop index against 2, which sets the carry flag, which the add silently includes. Gravity is one carry flag per tick with no constant, no table, basically the cheapest possible downward pull. In the drop demo you can watch it happen a tick at a time: the velocity byte counts up by exactly one per tick, courtesy of a compare instruction.
The same routine (apply_acceleration_to_velocities, &1f2e) enforces a
soft speed limit with a subtlety that took a while to figure out: if the new
velocity crosses ±&40 the code reloads the old velocity and keeps it —
objects don’t accelerate past the limit, they freeze at whatever speed
they’d already reached (terminal velocity for free), unless they were under
the limit, in which case they clamp to exactly ±&40. And every sixteen
ticks, every velocity loses 1 toward zero — a universal inertia that makes
everything eventually settle. A problem modern games still struggle with!
Water and wind
▶ demo: splash into the flooded caverns ⌗ 6502 source
Water. Each x-range of the world has a waterline (the
first article’s water sidebar).
The tick computes how much of the object is below it, or inside actual water
tiles, and then runs a beautifully compact buoyancy loop (&2f86): up to
four iterations, each subtracting a quarter of the object’s height from the
submerged amount; each surviving iteration decrements the vertical velocity
once or twice depending on weight. Short objects are more buoyant; light
objects are more buoyant; heavy ones sink. Every four ticks, anything in
water has both velocities multiplied by ⅞ — water drag as two shifts and a
subtract.
▶ demo: fight the downdraft ⌗ 6502 source
Wind. Wind isn’t a global system, and this surprised me, it’s tiles. I mean it makes sense but never occurred to me as a player. Variable-wind tiles (the windy square caverns) compute a wind vector whose angle rotates through a full circle every 64 ticks and whose magnitude mixes a random byte with the tile’s position; the two caverns below the west stone door are horizontally flipped variable-wind tiles, which the code special-cases into a constant downdraft. Flowing water tiles are the same mechanism with the current vector taken from the tile’s flip bits. All of it funnels into one routine that nudges velocity toward the wind vector, scaled by weight, clamped to a maximum acceleration of &0c.
Above ground there’s also the surface wind (apply_surface_wind, &2e5c):
a weight-scaled gale centred on the crash site that doubles in strength as
you climb, which is why flying high with a heavy object is such hard work.
Colliding with a function
▶ demo: grenade off the cavern walls ⌗ 6502 source
The landscape doesn’t exist, so what do you collide with? The first
article’s answer: get_tile is queried for the tiles the object overlaps.
But tiles aren’t solid boxes, each tile type has an 8-byte obstruction
profile (one height per two-pixel column, stored in the bottom of the
stack page at &0100), which is how slopes are walkable and half-tiles
obstruct only half a tile.
The collision pass checks the object’s left and right edges against the profiles, counts obstructed sections along the top and bottom edges, and condenses everything into four numbers
- How obstructed the object is on each side, the differences form a vector
- A binary arctangent turns it into a collision angle
- The object is pushed out along the least-obstructed axis
- Then, if it was moving into the surface, it bounces: the reflection angle is softened by an eighth of the impact angle, the speed is capped, loses two, and is scaled by ⅞ — through a binary sine routine and back into velocity components.
If the object is embedded with no way out, the code restores last tick’s position and halves the velocities: squirming out of rock.
Everything here is that same five-instruction 8-bit style: the angle
arithmetic is a shift-and-subtract division building five bits of arctangent
(calculate_angle_from_vector), the bounce trig is a five-bit multiply
(calculate_vector_from_magnitude_and_angle). Nothing is floating point and
nothing is even 16-bit. Crazy.
The player is just another object
Everything above applies to the player, the grenades you throw, the creatures, the furniture. The player object adds an intent layer on top — a state machine reading the keys, aiming angle, thrust from the jetpack (dice-rolled for reliability every sixteen ticks!), energy and damage — which then simply writes accelerations for the same pipeline. Creatures do the same with AI instead of keys. That’s the deep reason Exile’s world feels coherent: there is exactly one notion of motion, and everything obeys it.
The intent layer which includes the keys, the angle state machine, walking, the dice-rolled jetpack is in a later article, where you can fly the real suit yourself. The famous consequences are already visible in the free physics, though: grenade arcs you must judge against wind, objects that float or sink by weight, the downdraft traps, and bounces that steal just enough energy that things come to rest naturally.
Rebuilding it
The port followed the same process that I used to build parts one and two — transliterate line by line first, let an emulator running the actual code bytes arbitrate, and only then rewrite into ordinary pure functions with the flags as explicit values, under the same gate. Every bug the fuzzing caught was flag threading, exactly the failure class the first article warned about (and that continued to be troublesome right to the end):
- The velocity limiter’s discarded result (above): a single
LDYthat reloads the old velocity, easy to read straight past. - The surface wind computes each axis’s distance with a subtract whose borrow comes from the comparison chain of the previous axis, the carry crosses the loop iteration, just as f7’s carry crossed into f8 in the landscape hash.
- The push-out direction is decided by a processor flag captured after a conditional increment, capture it one instruction early and every top-or-left collision pushes the wrong way.
The physics demo on this page runs the validated kernel with step-by-step controls, force toggles, and a live register panel — try switching gravity or wind off mid-flight; the toggles are demo affordances around code paths the game itself never turns off. The next article puts you in the suit: the player’s intent layer, validated the same way, with the real spacesuit sprites.