The computer game I played the most growing up was almost certainly Chris Sawyer’s iconic transport network building game Transport Tycoon. As well as being a fun, creative game, it had a basic 2.5D isometric graphics style depicting a unique, highly-constrained landscape. In this blog post I’ll talk about my findings trying to recreate “Transport Tycoon”-like terrain in 3D using WebGL.
Screenshot from OpenTTD, an open-source clone of Transport Tycoon
Defining Transport Tycoon Terrain
The shape of the terrain in Transport Tycoon has very strict rules. As you can see from the screenshot above, the world consists of thousands of tiles representing land or water. There are no cliffs, so tiles are either flat or sloping. If a tile is sloping, then adjacent corners of a tile can only differ by one unit of height. This leads to the following possible tile shapes, which I’ve given arbitrary names:
Each of these tiles can be rotated left or right to create new tiles. These rotations make 19 total unique tiles (not 24 because some tiles like “flat” look the same when rotated).
If we say that the lowest point of a tile is zero height, we can also define the tiles by the heights of their corners. For example we could refer to the flat tile as 0000, or a particular rotation of the ramp tile as 0110, or the steep tile as 1210
Generating Transport Tycoon Terrain
Now that we know the rules, let’s look at how we can generate terrain that matches these constraints. We will need to create a heightmap — a 2D array of numbers which define the heights of all the corners of the tiles. I initially experimented with creating heightmaps using Perlin noise but found it difficult to make it work with the constraints of the terrain. By contrast, the diamond-square algorithm naturally lends itself to grid-like terrains.
The diamond-square algorithm requires height maps that are square in shape shape and have sides of length 2n + 1 (so valid sizes are 3x3, 5x5, 9x9, 17x17 etc). In this example I’ll use 5x5. First you start by setting the corners to random heights:
Diamond step: Set the center to be the average value of the four corners, plus or minus some random value:
Square step: Set the center of each edge to be average value of the two nearest corners and the center, plus or minus some random value.
Repeat: Now you divide the heightmap into sub-squares half as many tiles in length, and repeat the diamond and square steps for each sub-square:
By repeating the two steps and halving the subsquares you can fill any heightmap that has sides of length 2^n + 1. Here’s an example using a large grid and allowing non-discrete heights.
Image generated using Fractal Terrain Generator
The main constraints of Transport Tycoon terrain are:
- Heights must be whole numbers
- Adjacent points can’t differ by more than one unit of height
This second constraint places limits on the heights you can generate for each point based on its distance from previously generated points. For example, if we are generating the center of the left-hand edge of the square in the square-step:
The maximum height we can place in this position is limited by the lowest neighbour. Since the lowest neighbour has height two, and height can only increase at 1 per tile, the maximum height is 2 + 2 = 4. Similarly the minimum height is limited by the highest neighbour. In this case 3 - 2 = 1. Adding in the “whole number” constraint means the allowed heights for this point are 2, 3, and 4.
The constraints work in a similar manner for the diamond step. Let’s say we are on the second iteration of the diamond-step, generating the center-point of a 3x3 square in the lower-right corner:
The constraint that adjacent points can’t differ by more than one unit of height means that opposite points in a tile can increase by two units of height (using the “steep” tile type mentioned previously). The lowest neighbour is 1, making that the maximum height appear to be 1 + 2 = 3. The highest neighbour is 2, which appears to make the minimum height 2 - 2 = 0. This is wrong, as we will see.
When calculating points for the diamond-step, there is an additional “gotcha” because we also need to consider points that have been generated for other sub-squares:
In the image above you can see that we have already run the diamond step for the bottom-left and top-right subsquares. These values place limits on the allowed heights for the bottom-right sub-square. We can see that the bottom-left subsquare has a height of 3. Height can decrease by at most by 1 unit of height per tile, so the actual minimum height for the bottom-right subsquare is 3 - 2 = 1. Similarly the top-left sub-square has a height of 0, restricting the maximum height to 0 + 2 = 2.
Putting it all together
Using this method, we can get allowed height ranges at each step of the diamond-square algorithm that will generate heights that fit Transport Tycoon terrain. If we simply set the height of a point to be a random number in the allowed range you get terrain like this:
Hills are not the friend of trains, and Transport Tycoon terrain is much flatter. We can “smooth” the terrain by making each point the average height of its neighbours. Applying the function once removes some of the bumpiness:
And three passes of the smoothing functions really calms down the terrain:
That’s it for this post. I’m still tweaking the terrain generation algorithm and also trying to think of a simple game I can buld out of this demo code, so I might have more to say about that in the future.