A while back I published a post about how to go about creating random 2d game world maps. I was looking to create something that could be used for a game similar to those old Might and Magic games.

In my first post, I posted an image of a generated map that went like this:

World created using a heightmap

Since then, I improved my maps and I also added some rivers and lakes.

To create rivers and lakes, I created a river filter. Since my height maps were already accepting filters for my Gaussian Blur filters I used the same underlying mechanic.

Here’s my filter method on my heightmap:

# Return a new height map, created by using the filter parameter on the current height map
def filter(filter)
	filtered_height_map = HeightMap.new
	filtered_height_map.load(filter.filter(@data), @size_x)
	return filtered_height_map

This returns a new heightmap which has been filtered by the parameter.

River filter


To create the river filter, I create a new height map which I call a rain map. This is a normal height map, but instead of interpreting the values as the height of the terrain, we simply interpret them as the quantity of water that fell over an area. The rain map is of the same size as my height map, which means I can superimpose the two to find out where rain has fallen on my generated world.

In the river filter I use a rain level threshold. Wherever it has rained sufficiently to go over that threshold value, I will use that spot to start a river/lake.

If you are using the height maps parameters from my post on generating heightmaps here are the configuration values I am using for a 80×80 rain map.

  # 80 x 80
  RainMap_medium_world = Proc.new do |height_map|
    height_map.number_of_drop_points = 30
    height_map.min_particles = 400
    height_map.max_particles = 800
    height_map.number_of_passes = 4
    height_map.particle_stability_radius = 1

There is no need for the particles to cover the whole map as we are only interested in knowing the places where it has rained a lot. We can thus save time by using configuration values to generate a few heavy spots of rain (lighter spots of rain wouldn’t have crossed the rain level threshold anyway and would have been ignored).

This is a sample 80×80 rain map:


Note that I use a single pass of Gaussian filter blurring on my rain maps.

To get similar results on larger maps calculate the difference in total pixels of the larger height map and modify the number_of_drop_points, min_particles and max_particles by that factor.

Starting points for rivers


In my implementation of the river filter, I decided to only start rivers in mountains where a sufficient rain threshold had been achieved. This way all my rivers are coming down from mountains.

The first step is to find the different river starting points. It is best to leave out some spaces between each starting point to make sure we do not cover too much of the map with our rivers.

# superimpose rain map with array (normal height map) to choose river locations
    (0...@size).each do |y|
      # @size is used since we assume the array is square 2d matrix
      (0...@size).each do |x|
        if filtered_rain_map.data[x + y * @size] >= 80 && filtered_array[x + y * @size] >= 68 &&
            river_starting_points.none? {|p| (p[0] > x - 4 && p[0] < x + 3) &&
                                             (p[1] > y - 4 && p[1] < y + 3)}
          river_starting_points.push([x, y])

In this last code sample my river threshold is at 80 and I added the additional condition that my height map height should be at 68. For reference, when creating a world, I consider height values of 70 and up as mountains. After some experimentation, I ended using a slightly lower value (68). Not only does it allows for rivers to start next to mountains but the overall result is more pleasing.

Creating river paths

Now that we have our starting points we can create river paths. We need to store those river paths inside arrays. Once we have all our river paths in memory we can dig the rivers afterwards. If we dig our rivers as we go, it will be difficult not to keep going back into spots we have already dug.

The system to create river paths uses a “find lowest neighbor” method to find out where the river should be flowing next.

When finding our lowest neighbor we must skip our current location, of course, and the pixels in diagonal. Because diagonal rivers look really unnatural when converted into 2d tiles.

The following picture illustrates with check marks which pixels must looked at to find the lowest neighbor.

findLowestNeighbor diagram

By always choosing the lowest neighbor we end up following a path as such:

River path illustration

Of course a more realistic river filter would fill up all adjacent pixels up to a certain level with water. In this last picture, with a more realistic system, the pixel with a value of 55 would get filled before the one with a value of 65. While this would make for a more realistic simulation of rain accumulation, we do not need to go this far unless we actually wish too.

You will still get broad rivers and lakes with the former approach when you have groups of even pixels next to each other.


You stop your river path when you reach a body of water.

Digging the rivers

Once all your river paths have been generated independently, you can dig rivers by replacing the pixels on the river paths with 0 (or what ever value you use to signify water).


Here is a full world map which uses the technique described above to create rivers and lakes:

World created using a heightmap and rain map

Furthermore, the existing rain map could be used for other purposes, like creating biomes. A lower altitude area that didn’t get any precipitations could become a desert, while another which got a lot of precipitations over a large flat expanse could be turned into a swamp.

That’s all for now. Keep tuned for other posts on creating a random 2d world in the future.

UPDATE: I published a part 3 to this article: Cities, caves and snow.

3 thoughts on “Creating a random 2d game world map, Part 2: Adding rivers and lakes

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s