Unity Tile with scripted terrain generation

Complete Guide to Procedural Level Generation in Unity – Part 1

In this tutorial series we are going to procedurally generate levels using Unity. In the first tutorial we are going to use pseudorandom noise to generate height maps and choose terrain types according to the height in each part of our level. In the next tutorials, we are going to assign biomes for each part of the level and in the end, we are going to generate a level map object that can still be manually edited according to the game needs.

This tutorial series takes inspiration from, and expands on techniques presented by Sebastian Lague, Holistic3D and this GDC talk on procedural generation in No Man Sky.

Rest of the series: part 2, part 3.

Don't miss out! Offer ends in
  • Access all 200+ courses
  • New courses added monthly
  • Cancel anytime
  • Certificates of completion

In order to follow this tutorial, you are expected to be familiar with the following concepts:

  • C# programming
  • Basic Unity concepts, such as importing assets, creating prefabs and adding components

Before starting reading the tutorial, create a new Unity project.

Looking for procedural generation on 2d maps instead? Check out this article here instead!

Source code files

You can download the tutorial source code files here.

Generating noise

In order to procedurally generate levels, we are going to use noise functions in our code. A noise function is basically a function that generates pseudorandom values based on some input arguments. By pseudorandom it means the values look random, despite being algorithmically generated. In practice, you can see this noise as a variable, which can be used as the height, temperature or moisture values of a level region. In our case, we are going to use it as the height of different coordinates of our level.

There are different noise functions, but we are going to use a specially popular one called Perlin Noise. You don’t need to fully understand Perlin Noise, since Unity provides an implemention it. What I’m going to explain in this tutorial is how you can use it in your code.

So, let’s start by creating a script we are going to generate a noise map. First, create a new script called NoiseMapGeneration. This script will have a function called GenerateNoiseMap, which will receive as parameters the map height, map width and a scale. Then, it will generate a matrix representing a noise map, with the noise in each coordinate of the level.

For each coordinate, we generate the noise vaue using the Mathf.PerlinNoise function. This function receives two parameters and generate a value between 0 and 1, representing the noise. Notice that we are going to use the x and z axes to access the level coordinates, while the y axis will be used to change the height of our level in a given coordinate. So, the parameters for the PerlinNoise function will be the x and z indices divided by the level scale. In practice, the level scale parameter acts as a zoom parameter in the level. In the end, it returns the noise map.

Now we need some way to visualize this noise map. What we are going to do is creating a Plane GameObject to represent a tile in our level. Then, we can show the generated noise values by painting the tile vertices according to their corresponding noise values. Again, this noise can represent anything you want, such as height, temperatura, moisture, etc. In our case, it will represent the height of that vertex.

Creating level tile

Let’s start by creating a new Plane (3D Object -> Plane) object called Level Tile. This will create the object below, already with a Mesh Renderer, which we are going to use to show the noise map.

Level Tile object in the Unity Inspector

Before showing the noise map in the tile, it is important that you understand how a Plane mesh looks like. The figure below shows the a Plane created in Unity along with its Mesh. Notice that the mesh vertices are not only the four Plane vertices. Instead, the Mesh contains several intermediate vertices that are connected inside the Plane. Basically, what we are going to do is using each one of those vertices as a coordinate for our noise map later. This way, we can assign a color to each vertex (which will be a height later) according to each generated noise value.

Grid tile in Unity Scene view

Now, we create the following Script called TileGeneration. This script will be responsible for generating a noise map for the tile and then assigning a texture to it according to the noise map. As this noise map will be used to assign heights to each vertex, from now on I’m going to call it a height map.

First of all, notice that the script has the following attributes:

  • noiseMapGeneration: the script which will be used to generate the height map
  • tileRenderer: the mesh renderer, used to show the height map
  • meshFilter: the mesh filter component, used to access the mesh vertices
  • meshCollider: the mesh collider component, used to handle collisions with the tile
  • levelScale: the scale of the height map

Basically, in the Start method it will call the GenerateTile method, which will do all this stuff. The first thing it does is calculating the depth and width of the height map. Since we are using a square plane, the number of vertices should be a perfect square (in our case, the default Unity Plane has 121 vertices). So, if we take the square root of the number of vertices, we will get the map depth and width, which will be 11. Then, it calls the GenerateNoiseMap method with this depth and width, as well as the levelScale.

After generating the height map, the script will call the BuildTexture method, which will create the Texture2D for this tile. Then, we assign this texture to the tile material.

The BuildTexture method will create a Color array, which will be used to create the Texture2D. Then, for each coordinate of the height map, it will choose a shade of grey based on the height value. We can do this by using the Color.Lerp function. This function receives as parameters two colors (in our case black and white) and a float value between 0 and 1 (in our case the noise value). Then, it chooses a color between the two ones selected according to the float value. Basically, the lower the height, darker will be the color. In the end, it creates a Texture2D using this Color array and return it.

Now that the TileGeneration script is complete, we add both our scripts to the Level Tile object.

Level Tile in Unity Inspector with Tile Generation script

And then, we can try playing our game to visualize the height map. The image below shows a height map generated using a scale of 3. Hint: it is easier to visualize the tile if you switch back to the Scene view after starting the game. This way you can easily move the camera to see tile by different angles.

Tile game object with height map applied

Assigning terrains types

Our next step is to assign terrain types (such as water, grass, rock, mountain) to different height values. Also, each terrain type will have a color associated with it, so that we can add colors to our Level Tile.

First, we need to create a TerrainType class in the TileGeneration Script. Each terrain type will have a name, height and color. Also, we need to add the [System.Serializable] tag before the class declaration. This will allow us to set the TerrainType attributes in the editor later. Finally, we are going to add another attribute to the TileGeneration Script, which will be an Array of TerrainTypes. Those will be the available terrain types for our level.

Then, we can set the terrain types for the Level Tile in the editor. Those are the terrain types I’m going to use. You can set them as you prefer, but it is important that the terrain types are in a ascending order of height values, as this will be necessary soon.

Level Tile in the Unity Inspector with Terrain Types settings

Now let’s change the TileGeneration script to use those terrain types to assign colors to the tile. Basically we are going to change the BuildTexture method to, instead of picking a shade of grey for the color, we are going to choose a terrain according to the noise value (using a ChooseTerrainType method). Then we assign the terrain type color to that level tile coordinate.

The ChooseTerrainType method will simply iterate through the terrainTypes array and return the first terrain type whose height is greater than the height value (that’s is why it is important that the terrain types are in ascending order of height).

Now we can try playing the game again to see our tile with colors. You will notice that the tile looks a little blurred, but don’t worry about that now. It will look better once we have multiple tiles in the level.

Level Tile with earth-like colors applied

Changing mesh heights

We have assigned terrain types to the tile coordinates. However, it is still a plane, even in the mountain regions. What we are going to do now is using the height map to assign different heights to the tile vertices. We can do that by changing the y coordinate of the vertices.

In order to do so, we are going to create a new method in the TileGeneration Script called UpdateMeshVertices.  This method will be responsible for changing the Plane Mesh vertices according to the height map, and it will be called in the end of the GenerateTile method. Basically, it will iterate through all the tile coordinates and change the corresponding vertex y coordinate to be the noise value multiplied by a heightMultiplier. By changing the heightMultiplier we can change how the level looks like. In the end, it updates the vertices array in the Mesh and call the RecalculateBounds and RecalculateNormals methods. Those methods must be called every time you change vertices in the mesh. We also need to update the Mesh in the MeshCollider.

Then, we can assign a heightMultiplier to the Level Tile and try running the game. The figure below shows a tile using a heightMultiplier of 3. We can see the mountains on it, but there is still a problem. We are applying heights even for water regions, which make them look weird, since they should be plane. That’s what we are going to fix now.

Level Tile object in the Unity Inspector Level Tile with greater height map applied

What we are going to do is creating a custom function that receives as input height values from our height map and returns corrected height values. This function should return a 0 value for all height values below 0.4, so that water regions are plane. We can do that by adding another attribute in the TileGeneration Script which is an AnimationCurve. Then, when assiging the y coordinate value of each vertex, we evaluate the height value in this function, before multiplying it by the heightMultiplier.

Now let’s create this heightCurve. We can do this in the Editor, by selecting it in the Level Tile object. The curve should look similar to the one below. You can create a curve like this by selecting the third one in the menu, then adding a Key (right-click -> Add Key) in the 0.4 point and then dragging it to 0. You will also need to change the borders of the Key so that it is plane up to 0.4.

Height curve with a steep climb after 0.4

Finally, if you try playing the game it should show the level with plane water areas, which should look much better.

Level Tile object with heightCurve applied

Building a level with multiple tiles

The last thing we are going to do in this tutorial is adding multiple tiles to build a whole level. Each level tile will generate its own height values and the neighbor tiles should have continuous heights. So, the first thing we are going to do is making sure that Level Tiles will have the same height values in their borders.

We can do that by making sure that we are calling the PerlinNoise function with the same argument values for the border pixels. In order to do so, we are going to add offset parameters in the GenerateNoiseMap function. Those offsets are added to the x and z indices when calculating the x and z samples. Later, those offsets will correspond to the Level Tile position, so that the height is continuous along the tiles.

Then, we need to add those offset parameters when calling GenerateNoiseMap in the TileGeneration Script. The value of those parameters will be the opposite of the x and z coordinates of the Level Tile.

Before creating the whole level with several tiles, let’s create just two of them and put them side by side to check if the heights are continuous on the borders. For example, I created the two tiles below.

Level Tile 1 object in the Unity Inspector Level Tile 2 object in the Unity Inspector

Then, when running the game, they should look like a single level with two tiles.

Level Tile objects next to each other with height maps applied

What we are going to do now is generalizing this to a level with any number of tiles. First, save the Level Tile object as a prefab, so that we can instantiate copies of it later. Then, let’s create a new Script called LevelGeneration. This script will be responsible for creating multiple Level Tiles. It will have the following attributes:

  • mapWidthInTiles: number of tiles in the x axis
  • mapDepthInTiles: number of tiles in the z axis
  • tilePrefab: Level Tile prefab, used to instantiate the tiles

Then, the GenerateLevel method will create the Level Tiles by iterating through all the tile coordinates. For each tile, it calculates its position based on the tile coordinate and then instantiate a copy of it from the Level Tile prefab. In the end, this GenerateLevel method is called inside the Start method.

Now, remove the Level Tiles from your scene and add a single Level object, with some tiles in the x and z axis. The figure below shows an example of a Level with 10 tiles in each axis. You may also want to change the Level Scale and Height Multiplier for the Level Tiles to make the level look better. In the figure below I used a Level Scale of 10 and a Height Multiplier of 5. However, you can still see some repeating patterns in the level, as well as some weird-shaped regions. So, our last step in this tutorial will be to polish a little bit the Noise Map generation to make the level look more natural.

Level object with Level Generation script attachedLarge level map with terrain generation applied

Adding multiple waves

What we are going to in order to polish the noise map generation is adding more noise waves. Basically, when you call Mathf.PerlinNoise, you’re sampling points from a noise wave. So, if we change this wave frequency and amplitude we change the noise result. Another way of changing the noise values is adding a random seed in the samples. By creating multiple waves with different frequency, amplitude, we can generate more interesting noise, which will led to levels that look more natural. The different seed values, by their turn, allows us to remove the repetitions in the level.

So, first, let’s create a Wave class inside the NoiseMapGeneration Script. Like the TerainType, the Wave will be a Serializable class with a few attributes. The attributes we are going to use are the seed, frequency and amplitude of the wave, as discussed earlier.

Then, we change the GenerateNoiseMap method to receive an Array of Waves as parameter. Then, instead of calling Math.PerlinNoise a single time, we call it once for each Wave, using the wave seed, frequency and amplitude. Notice that the frequency is multiplied by the sample value, while the amplitude is multiplied by the noise result. In the end, we need to divide the noise by the sum of amplitudes, so that its result will remain between 0 and 1.

Now, we need to add an Array of Waves as a new attribute of the TileGeneration Script, so that we can send it to the GenerateNoiseMap method.

Finally, we add some Wave values in the Level Tile prefab and then we can play the game again to see the new level. The figure below shows the values I’m using in this tutorial to generate the level in the righthand figure. Notice that I changed the Level Scale and Height Multiplier again, to make the level look better. You may have to try different values until you find the one that looks better to you.

Level Tile object in the Unity Inspector Level Tile object with numerous generated waves

And this concludes this procedural level generation tutorial! In the next one we are going to generate tempereatures and moisture values for our level, so that we can select biomes for different level areas.

Access part 2 here