Tycoon-style Terrain

RollerCoaster Tycoon (and the other similar Tycoon games) had a distinct terrain style, something like this:

Basically, it’s a like a heightmap, except tiles can be extruded if the difference between neighbor heights is not the same. In the case of this terrain, a tile is a collection of four distinct points on the terrain.

In my implementation, I have a basic Tile object which has a few properties:

  • topLeft, topRight, bottomLeft, bottomRight: Absolute heights of the tile. These are clamped so all values are -/+ one of the other.
  • flat, edge: An index into a tile set representing the type of tile rendered. The tile set stores properties like texture and color.

And a map (or Stage in my case) is a collection of these tiles. A 4 × 4 map is thus equivalent to an 8 × 8 heightmap. The biggest different is when the heights of neighbor points diverge, the tile appears extruded and a wall/edge is generated (see image above).

The triangulation process is simple enough. The tiles can be triangulated easily enough: in my case, I form two triangles: (topLeft, topRight, bottomRight) and (topLeft, bottomRight, bottomLeft). The edges, however, are a bit trickier.

Here’s a sample that generates the vertices for a left edge:

local function getLeftVertices(tile, neighbor) local tileRef1 = tile.topLeft local tileRef2 = tile.bottomLeft local neighborRef1 = neighbor.topRight local neighborRef2 = neighbor.bottomRight local difference1 = tileRef1 – neighborRef1 local difference2 = tileRef2 – neighborRef2

local t if difference1 >= 1 and difference2 >= 1 then t = { Vector(-1, tileRef1, -1), Vector(-1, tileRef2, 1), Vector(-1, neighborRef2, 1), Vector(-1, tileRef1, -1), Vector(-1, neighborRef2, 1), Vector(-1, neighborRef1, -1) } elseif difference1 >= 1 then t = { Vector(-1, tileRef1, -1), Vector(-1, tileRef2, 1), Vector(-1, neighborRef1, -1) } elseif difference2 >= 1 then t = { Vector(-1, tileRef2, 1), Vector(-1, neighborRef2, 1), Vector(-1, tileRef1, -1) } end return t, Vector(-1, 0, 0), Vector(0, 0, 1)

Essentially, the logic is: if both reference points (e.g., tile’s topLeft and neighbor’s topRight) are separated by more than 1 unit, it’s considered separate and a triangle needs to be added. There’s three cases: both corners are separate, the first corner is separate, and the second corner is separate.

Of course, we could just brute-force it and add 0-area triangles, but what’s the fun in that? :)

The logic for the other edges are the same, just rotated. I couldn’t think of a nicer way of handling it without creating some weird metafunctions, so I just hardcoded the four permutations (left, right, top, bottom).

(Note: Units are rounded to the nearest integer, so fractional elevations aren’t allowed).

In the end, it creates something nice: