informa
/
Programming
Featured Blog

Procedurally Generating Wrapping World Maps in Unity C# – Part 3

In this article series, we will be teaching you how to create procedurally generated world maps, with the help of Unity and C#. This is a four part series.

This article was originally posted on http://www.jgallant.com

Table of Contents

In Part 1:

  1. Introduction
  2. Noise Generation
  3. Getting Started
  4. Generating the Height Map

In Part 2:

  1. Wrapping the Map on One Axis
  2. Wrapping the Map on Both Axis
  3. Finding Neighbors
  4. Bitmasking
  5. Flood Filling

In Part 3 (this article):

  1. Generating the Heat Map
  2. Generating the Moisture Map
  3. Generating Rivers

In Part 4:

  1. Generating Biomes
  2. Generating Spherical Maps

Generating the Heat Map

A heat map defines the temperature of our generated world. The heat map we are going to create will be based on latitude and height. The latitude portion, can be done with a simple noise gradient. The Accidental Noise library provides us with this function:


ImplicitGradient gradient = new ImplicitGradient (1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1);

Since we are wrapping the world, we only need a single gradient on the y-axis as our heat gradient.

We can add a new function in the TextureGenerator class, to generate a Heat Map texture. This will allow us to visually see what we are doing with the heat map:


public static Texture2D GetHeatMapTexture(int width, int height, Tile[,] tiles)
{
    var texture = new Texture2D(width, height);
    var pixels = new Color[width * height];
     
    for (var x = 0; x < width; x++)
    {
        for (var y = 0; y < height; y++)
        {
            pixels[x + y * width] = Color.Lerp(Color.blue, Color.red, tiles[x,y].HeatValue);
 
            //darken the color if a edge tile
            if (tiles[x,y].Bitmask != 15)
                pixels[x + y * width] = Color.Lerp(pixels[x + y * width], Color.black, 0.4f);
        }
    }
     
    texture.SetPixels(pixels);
    texture.wrapMode = TextureWrapMode.Clamp;
    texture.Apply();
    return texture;
}

Our heat gradient ends up looking like this:

heat1

This data is a great start, as we want to have a warm band in the center of the map, similar to the equator on earth. This will the the base heat map we will build off of.

The next thing we want to do, is define HeatType zones, similar to how we defined HeightType zones in the previous part of this tutorial.


public enum HeatType
{
    Coldest,
    Colder,
    Cold,
    Warm,
    Warmer,
    Warmest
}

These HeatTypes will be adjustable from the Unity inspector, with the help of a few new variables:


float ColdestValue = 0.05f;
float ColderValue = 0.18f;
float ColdValue = 0.4f;
float WarmValue = 0.6f;
float WarmerValue = 0.8f;

In LoadTiles, we set the HeatType of each tile, based on its heat value.


// set heat type
if (heatValue < ColdestValue) 
    t.HeatType = HeatType.Coldest;
else if (heatValue < ColderValue)
    t.HeatType = HeatType.Colder;
else if (heatValue < ColdValue) 
    t.HeatType = HeatType.Cold;
else if (heatValue < WarmValue) 
    t.HeatType = HeatType.Warm;
else if (heatValue < WarmerValue) 
    t.HeatType = HeatType.Warmer;
else
    t.HeatType = HeatType.Warmest;

Finally, we will add some new colors, for each HeatType in our TextureGenerator class:


// Height Map Colors
private static Color Coldest = new Color(0, 1, 1, 1);
private static Color Colder = new Color(170/255f, 1, 1, 1);
private static Color Cold = new Color(0, 229/255f, 133/255f, 1);
private static Color Warm = new Color(1, 1, 100/255f, 1);
private static Color Warmer = new Color(1, 100/255f, 0, 1);
private static Color Warmest = new Color(241/255f, 12/255f, 0, 1);
 
public static Texture2D GetHeatMapTexture(int width, int height, Tile[,] tiles)
{
    var texture = new Texture2D(width, height);
    var pixels = new Color[width * height];
     
    for (var x = 0; x < width; x++)
    {
        for (var y = 0; y < height; y++)
        {
            switch (tiles[x,y].HeatType)
            {
            case HeatType.Coldest:
                pixels[x + y * width] = Coldest;
                break;
            case HeatType.Colder:
                pixels[x + y * width] = Colder;
                break;
            case HeatType.Cold:
                pixels[x + y * width] = Cold;
                break;
            case HeatType.Warm:
                pixels[x + y * width] = Warm;
                break;
            case HeatType.Warmer:
                pixels[x + y * width] = Warmer;
                break;
            case HeatType.Warmest:
                pixels[x + y * width] = Warmest;
                break;
            }
             
            //darken the color if a edge tile
            if (tiles[x,y].Bitmask != 15)
                pixels[x + y * width] = Color.Lerp(pixels[x + y * width], Color.black, 0.4f);
        }
    }
     
    texture.SetPixels(pixels);
    texture.wrapMode = TextureWrapMode.Clamp;
    texture.Apply();
    return texture;
}

Generating this heat texture, now gives us the following:

heat2

We can now clearly see our defined HeatType zones. This data, however, is still just bands. It doesn't provide us with anything but latitude based heat data. Since temperature in the real world is reliant on a multitude of factors, we are going to blend in some fractal noise with this gradient noise.

We will add a couple of new variables to our Generator, and a new Fractal:


int HeatOctaves = 4;
double HeatFrequency = 3.0;
  
private void Initialize()
{
    // Initialize the Heat map
    ImplicitGradient gradient  = new ImplicitGradient (1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1);
    ImplicitFractal heatFractal = new ImplicitFractal(FractalType.MULTI, 
                                                      BasisType.SIMPLEX, 
                                                      InterpolationType.QUINTIC, 
                                                      HeatOctaves, 
                                                      HeatFrequency, 
                                                      Seed);
 
        // Combine the gradient with our heat fractal
    HeatMap = new ImplicitCombiner (CombinerType.MULTIPLY);
    HeatMap.AddSource (gradient);
    HeatMap.AddSource (heatFractal);
}

By combining the fractal with the gradient using a Multiply operation, the resulting noise gets multiplied based on the latitude. The Multiply operation is illustrated below:

heat0

Gradient noise on the left, fractal noise in the middle, result of the Multiply operation on the right. As you can see, we now have a much nicer, less bandy heat map.

That takes care of the latitude portion. Next, we need to take the height map into consideration. We want our tallest mountaintops to be cold. The adjustment can easily be done in the LoadTiles function:


// Adjust Heat Map based on Height - Higher == colder
if (t.HeightType == HeightType.Grass) {
    HeatData.Data[t.X, t.Y] -= 0.1f * t.HeightValue;
}
else if (t.HeightType == HeightType.Forest) {
    HeatData.Data[t.X, t.Y] -= 0.2f * t.HeightValue;
}
else if (t.HeightType == HeightType.Rock) {
    HeatData.Data[t.X, t.Y] -= 0.3f * t.HeightValue;
}
else if (t.HeightType == HeightType.Snow) {
    HeatData.Data[t.X, t.Y] -= 0.4f * t.HeightValue;
}

This adjustment gives us our final Heat Map, taking both latitude and height into consideration:

heat5

Generating the Moisture Map

The moisture map is similar to the heat map. We first generate a fractal to give us a random base. Then we adjust this data based on the height map.

We will go through the moisture code quickly, as it is very similar to the heat map code.

First, the Tile class is going to need a new MoistureType


public enum MoistureType
{
    Wettest,
    Wetter,
    Wet,
    Dry,
    Dryer,
    Dryest
}

The Generator class will need new exposed variables, for the Unity Inspector:


int MoistureOctaves = 4;
double MoistureFrequency = 3.0;
float DryerValue = 0.27f;
float DryValue = 0.4f;
float WetValue = 0.6f;
float WetterValue = 0.8f;
float WettestValue = 0.9f;

The TextureGenerator is going to need a new MoistureMap generation function, and associated colors:


//Moisture map
private static Color Dryest = new Color(255/255f, 139/255f, 17/255f, 1);
private static Color Dryer = new Color(245/255f, 245/255f, 23/255f, 1);
private static Color Dry = new Color(80/255f, 255/255f, 0/255f, 1);
private static Color Wet = new Color(85/255f, 255/255f, 255/255f, 1);
private static Color Wetter = new Color(20/255f, 70/255f, 255/255f, 1);
private static Color Wettest = new Color(0/255f, 0/255f, 100/255f, 1);


public static Texture2D GetMoistureMapTexture(int width, int height, Tile[,] tiles)
{
    var texture = new Texture2D(width, height);
    var pixels = new Color[width * height];
     
    for (var x = 0; x < width; x++)
    {
        for (var y = 0; y < height; y++)
        {
            Tile t = tiles[x,y];
             
            if (t.MoistureType == MoistureType.Dryest)           
                pixels[x + y * width] = Dryest;
            else if (t.MoistureType == MoistureType.Dryer)          
                pixels[x + y * width] = Dryer;
            else if (t.MoistureType == MoistureType.Dry)          
                pixels[x + y * width] = Dry;
            else if (t.MoistureType == MoistureType.Wet)          
                pixels[x + y * width] = Wet; 
            else if (t.MoistureType == MoistureType.Wetter)          
                pixels[x + y * width] = Wetter; 
            else     
                pixels[x + y * width] = Wettest; 
        }
    }
     
    texture.SetPixels(pixels);
    texture.wrapMode = TextureWrapMode.Clamp;
    texture.Apply();
    return texture;
}

Finally, our LoadTiles function will set a MoistureType based on its MoistureValue:



//Moisture Map Analyze  
float moistureValue = MoistureData.Data[x,y];
moistureValue = (moistureValue - MoistureData.Min) / (MoistureData.Max - MoistureData.Min);
t.MoistureValue = moistureValue;
 
//set moisture type
if (moistureValue < DryerValue) t.MoistureType = MoistureType.Dryest;
else if (moistureValue < DryValue) t.MoistureType = MoistureType.Dryer;
else if (moistureValue < WetValue) t.MoistureType = MoistureType.Dry;
else if (moistureValue < WetterValue) t.MoistureType = MoistureType.Wet;
else if (moistureValue < WettestValue) t.MoistureType = MoistureType.Wetter;
else t.MoistureType = MoistureType.Wettest;

Rendering the initial noise for the MoistureMap, gives us the following:

moisture1

The only thing left to do, is adjust the moisture map according to height map. We make the adjustment in the LoadTiles function:


//adjust moisture based on height
if (t.HeightType == HeightType.DeepWater) {
    MoistureData.Data[t.X, t.Y] += 8f * t.HeightValue;
}
else if (t.HeightType == HeightType.ShallowWater) {
    MoistureData.Data[t.X, t.Y] += 3f * t.HeightValue;
}
else if (t.HeightType == HeightType.Shore) {
    MoistureData.Data[t.X, t.Y] += 1f * t.HeightValue;
}
else if (t.HeightType == HeightType.Sand) {
    MoistureData.Data[t.X, t.Y] += 0.25f * t.HeightValue;
}      

Since we now adjusted our moisture data based on the height of certain tiles, our updated moisture map looks a lot nicer:

moisture3

Generating Rivers

The river generation method I will describe is really just a brute force attempt by my part, to make convincing looking rivers.

The first step of the algorithm, is to select a random tile on the map. The selected tile must be land, and must also have a height value that is over a specified threshold.

From this tile, we determine which neighboring tile is the lowest, and navigate towards it. We create a path in this fashion, until a water tile is reached.

If the generated path meets our criterias (river length, number of turns, number of intersections), we save the path for later use.

Otherwise, we discard the path, and try again. The following code gets us started:


private void GenerateRivers()
{
    int attempts = 0;
    int rivercount = RiverCount;
    Rivers = new List ();
 
    // Generate some rivers
    while (rivercount > 0 && attempts < MaxRiverAttempts) {
 
        // Get a random tile
        int x = UnityEngine.Random.Range (0, Width);
        int y = UnityEngine.Random.Range (0, Height);           
        Tile tile = Tiles[x,y];
 
        // validate the tile
        if (!tile.Collidable) continue;
        if (tile.Rivers.Count > 0) continue;
 
        if (tile.HeightValue > MinRiverHeight)
        {               
            // Tile is good to start river from
            River river = new River(rivercount);
 
            // Figure out the direction this river will try to flow
            river.CurrentDirection = tile.GetLowestNeighbor ();
 
            // Recursively find a path to water
            FindPathToWater(tile, river.CurrentDirection, ref river);
 
            // Validate the generated river 
            if (river.TurnCount < MinRiverTurns || river.Tiles.Count < MinRiverLength || river.Intersections > MaxRiverIntersections)
            {
                //Validation failed - remove this river
                for (int i = 0; i < river.Tiles.Count; i++)
                {
                    Tile t = river.Tiles[i];
                    t.Rivers.Remove (river);
                }
            }
            else if (river.Tiles.Count >= MinRiverLength)
            {
                //Validation passed - Add river to list
                Rivers.Add (river);
                tile.Rivers.Add (river);
                rivercount--;   
            }
        }       
        attempts++;
    }
}

The recursive FindPathToWater() function, determines the best path to take based on the terrain height, existing rivers, and its preferred direction. It will eventually find a water tile. We recursively call the function until the path is complete.


private void FindPathToWater(Tile tile, Direction direction, ref River river)
{
    if (tile.Rivers.Contains (river))
        return;
 
    // check if there is already a river on this tile
    if (tile.Rivers.Count > 0)
        river.Intersections++;
 
    river.AddTile (tile);
 
    // get neighbors
    Tile left = GetLeft (tile);
    Tile right = GetRight (tile);
    Tile top = GetTop (tile);
    Tile bottom = GetBottom (tile);
     
    float leftValue = int.MaxValue;
    float rightValue = int.MaxValue;
    float topValue = int.MaxValue;
    float bottomValue = int.MaxValue;
     
    // query height values of neighbors
    if (left.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(left)) 
        leftValue = left.HeightValue;
    if (right.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(right)) 
        rightValue = right.HeightValue;
    if (top.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(top)) 
        topValue = top.HeightValue;
    if (bottom.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(bottom)) 
        bottomValue = bottom.HeightValue;
     
    // if neighbor is existing river that is not this one, flow into it
    if (bottom.Rivers.Count == 0 && !bottom.Collidable)
        bottomValue = 0;
    if (top.Rivers.Count == 0 && !top.Collidable)
        topValue = 0;
    if (left.Rivers.Count == 0 && !left.Collidable)
        leftValue = 0;
    if (right.Rivers.Count == 0 && !right.Collidable)
        rightValue = 0;
     
    // override flow direction if a tile is significantly lower
    if (direction == Direction.Left)
        if (Mathf.Abs (rightValue - leftValue) < 0.1f)
            rightValue = int.MaxValue;
    if (direction == Direction.Right)
        if (Mathf.Abs (rightValue - leftValue) < 0.1f)
            leftValue = int.MaxValue;
    if (direction == Direction.Top)
        if (Mathf.Abs (topValue - bottomValue) < 0.1f)
            bottomValue = int.MaxValue;
    if (direction == Direction.Bottom)
        if (Mathf.Abs (topValue - bottomValue) < 0.1f)
            topValue = int.MaxValue;
     
    // find mininum
    float min = Mathf.Min (Mathf.Min (Mathf.Min (leftValue, rightValue), topValue), bottomValue);
     
    // if no minimum found - exit
    if (min == int.MaxValue)
        return;
     
    //Move to next neighbor
    if (min == leftValue) {
        if (left.Collidable)
        {
            if (river.CurrentDirection != Direction.Left){
                river.TurnCount++;
                river.CurrentDirection = Direction.Left;
            }
            FindPathToWater (left, direction, ref river);
        }
    } else if (min == rightValue) {
        if (right.Collidable)
        {
            if (river.CurrentDirection != Direction.Right){
                river.TurnCount++;
                river.CurrentDirection = Direction.Right;
            }
            FindPathToWater (right, direction, ref river);
        }
    } else if (min == bottomValue) {
        if (bottom.Collidable)
        {
            if (river.CurrentDirection != Direction.Bottom){
                river.TurnCount++;
                river.CurrentDirection = Direction.Bottom;
            }
            FindPathToWater (bottom, direction, ref river);
        }
    } else if (min == topValue) {
        if (top.Collidable)
        {
            if (river.CurrentDirection != Direction.Top){
                river.TurnCount++;
                river.CurrentDirection = Direction.Top;
            }
            FindPathToWater (top, direction, ref river);
        }
    }
}

After running this river path generation process, we are left with a bunch of paths that lead to water. This resembles the following:

rivers1 rivers2

A lot of the paths intersect, and if we were to dig these rivers out now, they might look a little strange if their sizes didn't match up at the point of intersection. Because of this, we are going to need to determine which rivers are intersecting, and group them together.

We will need a RiverGroup class:


public class RiverGroup
{
    public List Rivers = new List();
}

And the code to group the river paths together, if they intersect:


private void BuildRiverGroups()
{
    //loop each tile, checking if it belongs to multiple rivers
    for (var x = 0; x < Width; x++) {
        for (var y = 0; y < Height; y++) {
            Tile t = Tiles[x,y];
 
            if (t.Rivers.Count > 1)
            {
                // multiple rivers == intersection

Latest Jobs

Sucker Punch Productions

Bellevue, Washington
08.27.21
Combat Designer

Xbox Graphics

Redmond, Washington
08.27.21
Senior Software Engineer: GPU Compilers

Insomniac Games

Burbank, California
08.27.21
Systems Designer

Deep Silver Volition

Champaign, Illinois
08.27.21
Senior Environment Artist
More Jobs   

CONNECT WITH US

Register for a
Subscribe to
Follow us

Game Developer Account

Game Developer Newsletter

@gamedevdotcom

Register for a

Game Developer Account

Gain full access to resources (events, white paper, webinars, reports, etc)
Single sign-on to all Informa products

Register
Subscribe to

Game Developer Newsletter

Get daily Game Developer top stories every morning straight into your inbox

Subscribe
Follow us

@gamedevdotcom

Follow us @gamedevdotcom to stay up-to-date with the latest news & insider information about events & more