There is now exactly one way up onto a plateau, and everybody knows where it is. A cliff stops ground units cold. A ramp is the only route through it, so the ramp is where the fight happens. That chokepoint was never placed. It fell out of the terrain.
Standing on top of the plateau buys you something too. A unit with sight radius 6 on a level-2 plateau reveals 184 tiles instead of 113. The same unit at the foot of that cliff sees 91 instead of 113, and loses it directionally: the full disc across level ground, a crescent bitten out of it where the terrain rises. The army that took the high ground can see the army that has not. The army below is looking at a wall.
Flyers ignore all of it and go straight over, which is what finally makes air units a different kind of unit rather than faster ground ones.

What Adam asked for, and what the world used to be
Until August the world was flat. Every height in the 3D scene was a constant measured against one global ground plane, and anywhere you could not walk was painted as rock, so a cliff and a boulder field were the same thing to the engine and to the eye.
The brief was three sentences long. A cliff should stop an army. High ground should see further. A hill should roll rather than step. That third one is the interesting constraint, and it is why this was the second attempt rather than the first.
What a player meets, in order:
- A cliff stops ground units and lets flyers straight over. Across a test batch of 1,500 move orders issued over a cliff line, 1,500 routed via a ramp and 0 climbed.
- A ramp is a chokepoint the map author did not have to author.
- Standing on high ground reveals more map, and standing under it reveals less.
- A base on a rolling hill is open and buildable, while contested high ground near an expansion stays terraced. Between 28% and 44% of slope tiles on a generated map are ramped into a rolling slope rather than left as a cliff, and the choice is made per symmetry orbit, so one player does not get the walkable hill while the other gets the walled one.
Sight is the only combat-adjacent effect. High ground buys information and does not buy reach: acquisition range is untouched and there is no damage bonus for shooting downhill. That is a deliberate limit. Nothing on an overworld map has ever blocked line of sight, and combat, the approach planner and the walkability grid all quietly assume it stays that way.
Two height systems, and only one of them blocks anything
Before writing any of this I parsed the terrain out of 94 map files from a previous generation of RTS and measured it, because "how does that game do height" has a more interesting answer than the usual summary. The method was to decode each file's terrain chunk into two arrays and run summary statistics over the pair: per-corner height, per-corner cliff level, then every 4-adjacent corner pair grouped by level difference. The measurements cover 1,096,302 terrain corners.
The format carries two independent fields. One is a continuous signed height, authored with raise, lower, plateau, noise and smooth tools. The other is a discrete four-bit cliff level, authored with a separate cliff palette. Different editor tools write them and at runtime they simply sum. Only the discrete one gates movement, and it does not even do that directly, because the engine reads a baked walkability bitmap and nothing else.
The measurement that mattered is the one the usual summary gets wrong. "Flat plateaus connected by ramps" is half true. Across 41,795 pairs of neighbouring corners exactly one cliff level apart, the real height difference averages 1.065 tile widths, so a step is close to a perfect cube. But within a single cliff level on a single map, the median plateau rolls by 2.02 tile widths across its middle 90%, which is twice the height of a whole cliff step. Only 15% of plateaus are flat to within a quarter of a tile.
So the model to copy is discrete tiers for movement and continuous terrain for looks. The first pass here rolled 0.23 tiles within a level, which is exactly why terraces read as slabs. Raising the smoothing amplitude from 8 to 32 pixels along with the feature size from 6 to 24 tiles gives roughly 4.3 times the undulation at an unchanged gradient, so walkability is unaffected: the steepest 1% of steps between adjacent corners moved from 2.5 to 2.7 pixels per tile. In the same pass the height of one tier came down from 40 pixels to 32, which is exactly one tile width, matching the classic quantum where one cliff step is one tile.
The data shape, if you are adding elevation to a tile game
Two fields, not one, and they are different types on purpose. Everything above falls out of that split, so it is the part worth copying.
interface Elevation {
/** Discrete tier per tile. Integer, serialised, and the only elevation value
* the simulation is ever allowed to read. */
readonly level: Uint8Array; // width * height, 0..7 here
/** Ramp flag per tile, same indexing. A step between tiers is a CLIFF unless
* both tiles are ramped, which makes "no ramp data" mean "all cliffs". */
readonly ramp: Uint8Array;
/** Continuous offset at tile CORNERS, so adjacent tiles share their edges.
* Derived from the map seed, render-only, never serialised, never read by
* the simulation. */
readonly smooth: Float32Array; // (width + 1) * (height + 1)
}Three properties come out of that shape. Only integers cross into the simulation, so a lockstep multiplayer match stays bit-identical across browsers even though the terrain is built from floating-point noise. The continuous field is derived from a seed rather than stored, so it never has to be transmitted or trusted, because both peers regenerate the map from the seed. And a map with no elevation layer allocates none of it, so every map made before this feature existed takes the same path it always did.
The rule for crossing a boundary is small enough to state in a sentence. Same level, always. One level apart, only if both tiles are flagged as ramp. Two or more apart, never. What matters is that the rule lives in exactly one function and nowhere else.
Movement calls it. So does corruption spread, the creep that certain factions grow outward from their buildings, because two copies of the rule that disagree by one tile put creep where units cannot walk. So, less obviously, does the ground texture blending described in the coverage field both renderers read. That one shares it for the mirror-image reason: a distance transform is two-dimensional, so left alone, a patch of dirt at the foot of a cliff climbs the face and reappears on the terrace above. Ground appears to flow uphill. Running the distance passes per level under the same rule costs about 190 ms extra at load on a map with height.
A terrain feature that is passable to some movement types and not others has to take its pathing answer and its visual from the same source, or the two drift and the disagreement is invisible until a player walks into it. The check is short: count the places in your codebase that decide whether something can move from one tile to the next. If the answer is more than one, the extra copies are not redundancy, they are a schedule. Texture blending is the caller that proves it here, because a colour blend belongs on no list of movement consumers until the day it starts painting ground up a cliff face.
The units were never in the wrong place. Their shadows were
The first build looked wrong in a very specific way: units on high ground appeared to float. The obvious explanation is that placement was not sampling the terrain, so I measured placement against the real mesh triangles. Worst error across the sample was 1.6 pixels, or 4% of one level height. Placement was fine.
What was wrong was everything attached to a unit that had never needed to know about terrain. Contact shadows were still drawn on the zero plane, up to 280 pixels below the feet casting them. Floating combat text put "+8 gold" 200 pixels underneath a miner. A unit with its shadow stranded far below reads as levitating, because that is precisely what levitating looks like.
That pattern is why an earlier attempt at elevation was abandoned: subdividing the ground broke rock rendering, unit and building placement and the resource-deposit decals, because every other object in the 3D scene was anchored to zero. The terrain mesh was about 5% of the work. Roughly 45 hard-coded zero-plane sites were the other 95%.
So take that count before you start rather than after. Search your scene code for every literal zero standing in for ground height, and the number of hits is the honest estimate for the feature. The sites that hurt are never the ones that place the object. They are the ones attached to an object that has never needed to know about terrain: shadows, floating text, decals, selection rings, health bars.
The generator produced the same shape of problem once more, and this time the report came from Adam at the screen. He said the terrain read as "blocky like Minecraft". The obvious diagnosis, ragged level boundaries, was wrong. Where the height field is steep, two level thresholds fire inside a single tile and leave one-tile ribbons, which is what a staircase is. Level sets are now built as eroded regions, which makes the slope limit true by construction rather than filtering the symptom afterwards.
A picking bug I wrote, and why fixing the number would have hidden it
Working out which tile the mouse is over used to be done by intersecting a flat plane. On a hilly map at a 30 degree camera pitch, a three-tile hill put the cursor about five tiles away from where the player thought they were clicking, silently, on every click. That was replaced with a ray-march against the real height field.
Then I raised the height of a tier and broke it, because the march used a fixed 12 pixel step tuned when a tier was 24 pixels tall. The tempting fix is to re-tune the number. Sweeping the march across a range of tier heights showed that the error is not monotonic in tier height, so any fixed value that passes at one setting fails at another and re-arms the trap the next time a tier height moves. The step size is now derived from the clearance to the terrain, which bounds the error at 1 pixel regardless of pitch, tier height and distance. It also got faster, at 421 ns down to 304 ns per pick, and 22.9 terrain samples down to 11.4.

Reading elevation back out of an imported map
The engine half was useless without the importer half, which landed the same evening. A tile's cliff level comes from its four corner values, taking the minimum, so a cliff face belongs to the ground at its foot and never invents standable ground on top. Ramps come from a per-corner flag, any corner winning, which the measurements had already shown to be a well calibrated signal: flagged cliff tiles are 71.4% walkable against 0.1% for unflagged ones. Source levels 0 to 14 are renumbered densely into the engine's 0 to 7, so a map using 2, 4 and 7 becomes 0, 1 and 2 rather than losing the gaps to a clamp.
The behavioural change is that a tile which is unwalkable because it is a cliff no longer becomes rock. Emitting rock there blocks it twice over: the elevation already stops ground units, and rock would additionally make the plateau above unbuildable forever. Rock coverage on imported maps fell from 14.3% to 6.6%, against 2.0% on a generated map. There is more on that converter in reading two decades-old map formats.
The 2D renderer gets no geometry at all. It gets a drawn cliff lip and a hillshade, both computed from the same constants the 3D shader uses, which is enough to read a terrace from above.
The next question a terraced map asks is what water does when the ground can sit below it, and that is where water you can wade through starts.





