| name | ressort-top-down-world |
| description | Build a top-down (overhead) world on the machin-ressort engine — an irregular city or map generated offline into a tile grid, flood-field routing, lane-holding traffic, and the handling model that makes a top-down vehicle feel like a vehicle. Use when the camera looks straight down rather than from the side, when a map needs generating rather than authoring, or when anything in the world has to find its way somewhere. |
The world seen from above
A side-scroller's world is a heightfield and a parallax stack. A top-down
world is a plan: a grid of cells, most of them solid, with the playable
space cut through them. Almost everything below follows from that one
difference, and most of it was learned twice before it was written down.
The engine parts are engine/25_grid.src (grid, flood routing, sight lines,
file format) and engine/65_bake.src (the full-circle sprite bake).
Rule 1 — generate the plan OFFLINE, into a file
The first version of our city was a function: which cell you stood on was
arithmetic on a fixed block grid. That is elegant, costs nothing to store, and
produces a town where every block is the same square and every junction is the
same junction — a street plan a player has seen in full after ten seconds.
Generate it once, write it out, load it back:
./bin/game gen --seed 7 --out city.jsonl # a separate program
./bin/game --city city.jsonl # play it
The payoff is not file size, it is that the plan becomes data you can look
at: diffed between seeds, hand-edited, regenerated, replaced wholesale, and
re-skinned without touching the code that draws it. grid_encode/grid_decode
do it as one header line and one run-length row per line — 168² cells is 14 kB.
Keep a fallback that generates at startup when handed no file, or the game has
a bootstrapping problem the first time someone clones it.
Rule 2 — irregular means SUBDIVISION, not jitter
Do not build a grid and then perturb it; cut the map up instead. Take the whole
rectangle, put a street across it somewhere that is not the middle, and do
the same to both halves until the pieces are block-sized.
Because a cut only ever spans the piece it divides, streets do not line up
across the map. That single property is what gives you T-junctions, streets
that run for three blocks and stop, and a block twice the size of its
neighbour — none of which you have to ask for.
Then dress each leaf: a pavement ring, buildings in lots divided by a one-cell
alley, a notch out of a corner, and every so often a park or a yard instead of
buildings. Two dozen lines, and it is the difference between a plan and a grid.
Assert the irregularity, or it will quietly regress to a grid: walk several
scanlines, collect the gaps between streets, and require more than a couple of
distinct values.
Rule 3 — one integer per cell, and passability is a BITMASK
Store the cell's kind and its decoration in one integer — TL_BLDG + tone
rather than two parallel arrays. Then the engine's Grid is a view over the
same array rather than a copy, and the file format falls out for free.
Order the values so that everything from the first solid kind upward is
solid. Passability is then a bitmask over cell values, and one map serves
every kind of mover without a second copy:
MASK_DRIVE = 1 // roads only
MASK_WALK = 7 // roads, pavements, parks
The trap this creates, which you will hit: once a cell carries a tone,
cell == TL_BLDG() matches only tone zero. One place in our generator still
compared a raw cell to a kind and five sixths of the kerbs silently stopped
being generated — the pavement count fell 4% and nothing else complained. Ask
for the kind through an accessor everywhere, and assert both halves: that
buildings do carry tones, and that a cell's kind never leaks one.
Rule 4 — routing is a FLOOD, not a heuristic
Anything that has to get somewhere floods outward from the destination,
breadth-first, once; every reachable cell gets its distance, and everything
heading there reads the cell under it and walks downhill.
f := flow_build(city_grid(c), tx, ty, MASK_DRIVE())
ax, ay, ok := flow_aim(f, city_grid(c), x, y, TILE(), 5) // 5 cells ahead
We wrote the other thing twice — travel along the street you are on until you
are level with the target, then turn. On a regular grid it is optimal and
free. On an irregular plan it is a guess, and it fails by pressing into a dead
end it cannot see round, forever. It also fails invisibly: a stopped
vehicle looks like a stopped vehicle, and two rounds of screenshots found
nothing. One --trace line a second found it immediately.
One flood is about a frame of drawing and is shared by everything with the
same destination — which is what makes it affordable for a crowd. Rebuild
only when the destination changes cell; police chasing a player need one
rebuild per cell the player crosses, not per tick.
Aim several cells downhill rather than at the next one, or the mover twitches
instead of steering. Off the network entirely (shunted onto a kerb),
flow_aim heads for the nearest cell that is on it — without that, anything
pushed off the road is stuck for good.
Rule 5 — traffic holds a lane it has to MEASURE
With irregular streets there is no block size to take a modulo of, so a lane
centre cannot be computed — it has to be found. Scan out along one axis for the
nearest road cell, widen to the whole contiguous run, take its middle, and
offset to one side so oncoming traffic passes instead of colliding.
city_road_x originally returned the block's own carriageway. On the far
pavement the nearest road is the next block's, so everything self-driving
was steered through the building between them. Nearest, always — and assert it:
the middle of a street must itself be a street.
The same measurement tells you which way a street runs (compare the run length
in x against y), which is what a mover needs to decide whether it may travel
along the road it is standing on.
Rule 6 — a top-down vehicle goes where it POINTS
Three numbers, and all three were wrong in our first cut.
Grip. Velocity leans toward the nose at a rate, rather than snapping to it:
v += (fwd·(v·fwd) - v) · clamp(GRIP·dt). Snap it and the car is on rails —
no drift, no handbrake turn, every corner identical. Omit it and it is a
spaceship. This one blend is most of what "feels like driving" means.
Steering must fade at BOTH ends. It has to build from a standstill, or a
parked car pirouettes and is a tank; and it has to fall away with speed, or the
fast vehicles are unsteerable at exactly the speed you most want to steer them.
Drag is for coasting only. A linear drag term makes the real top speed
power/drag, approached exponentially — see ressort-side-scroll-world, where
the same trap cost a saloon two thirds of its rated speed.
Braking acts against travel, not against the nose, or braking mid-slide
accelerates you sideways. Move one axis at a time against the map so a glancing
hit slides along a wall instead of stopping dead.
Rule 7 — the crowd needs separating, and the pedestrians need a cooldown
Once the streets are busy enough to look alive, two things start looking
broken.
Vehicles interpenetrate. A circle test is enough — a box test on a rotated
sprite wants the separating-axis theorem and nobody can tell at this size.
Push apart by mass ratio so the bus shoves and the sports car gets shoved, and
scale the damage of a shunt well below a wall impact: at full rate a truck
clipping you at 30 m/s removes half a car, and every mission vehicle dies in
traffic.
Pedestrians spin. Ours turned 90° on every blocked tick — 5400°/s, which
reads as a figure drilling into the pavement. Turn at most a few times a
second, toward a direction that is actually free (try left, then right, then
back), and stop re-aiming a fleeing one when the player is standing on top of
it: "away from you" flips every tick at zero distance, and a figure that
reverses at 60 Hz reads as two figures.
Rule 8 — density is per YARD, not per magic number
Population as a fraction of the map: cars = len(road_cells)/42,
people = len(pavement_cells)/13. A city twice the size then needs no
retuning, and a screen shows the same number of things wherever you are. Ours
was a constant, and tripling the map turned a busy street into an empty one
with no code change to blame.
What to assert without a window
- every open cell can reach every other (an unreachable street is an
unfinishable objective)
- following the gradient from the far corner arrives
- an aim point is on the network, and far enough ahead to steer at
- the middle of a street is a street
- no two cells of the same block have the same size gap on every scanline
- spawns land on ground the thing spawned can actually occupy
- a written plan reads back cell for cell
- a blocked pedestrian turns a handful of times a second, not sixty
Then render it and look, because numbers can be right while the render is
wrong. The lane markings were absent for a week: the code asked whether a
cell's centre was the centre line, which is never true on a road of even
width.