Procedural 2D map generation

Published on
3 min read
Cover image for Procedural 2D map generation

Introduction

In this article, I will show you how to create simple 2-dimensional map based on Simplex Noise. We are going to write a map generator in Python using The Python Arcade as a game engine and OpenSimplex library as a noise generator. Let's go!

Prerequisites

  • Basic knowledge about python.
  • Basic knowledge about game engines.

What is this noise about?

I don't want to make a math science here. Let's keep things simple here.

We want our terrain:

  • to make it looks like a real terrain
  • to make it repetitive while using same seed
  • to make it unique while using different seeds

Basic concept looks like this: there are known coordinates like X and Y and random constant called seed. We want to obtain Z coordinate (height) for each (X, Y) point. The magic is that with same seed there will be always same result. In my example I generate 2D map, but there is no obstacles to make it 3D. I created ten 16x16 sprites, each for different representation of Z coordinate. Z is going to have values from -1.0 to 1.0. Everything below 0.1 is going to be ocean, then I made some greenery, rocks and snow on top of mountains.

Preview: Colors

Code review

Let me explain best parts:

import arcade
from opensimplex import OpenSimplex
import uuid

Why reinvent the wheel? We are going to use opensimplex library which gives freedom in creation of Simplex noise. In this example I use uuid to generate a truly random grain. Let's move on to main methods:

def generate_seed(self):
    return (uuid.uuid1().int >> 64)

def generate_world(self, x, y, seed):
    tmp = OpenSimplex(seed)
    z = tmp.noise2d(x=x, y=y)
    return z

Random seed generation is optional. In this example we are going to view maps generated by different seeds.

def setup(self):

    # Set the background color
    arcade.set_background_color(arcade.color.AMAZON)
    # Sprite lists
    self.tiles_list = arcade.SpriteList()
    self.seed = self.generate_seed()

def update(self, delta_time):
    self.seed = self.generate_seed()
    self.tiles_list = arcade.SpriteList()
    for x in range(self.x_start, self.x_end + 8, 8):
        for y in range(self.y_start, self.y_end + 8, 8):
            z = self.generate_world(x=x * 0.01, y=y * 0.01, seed=self.seed)
            if z >= 0.9:
                tile = arcade.Sprite("sprites/map/biom-regular-height-90.png", SPRITE_SCALING_TILE)
            elif z >= 0.8:
                tile = arcade.Sprite("sprites/map/biom-regular-height-80.png", SPRITE_SCALING_TILE)
            elif z >= 0.7:
                tile = arcade.Sprite("sprites/map/biom-regular-height-70.png", SPRITE_SCALING_TILE)
            elif z >= 0.6:
                tile = arcade.Sprite("sprites/map/biom-regular-height-60.png", SPRITE_SCALING_TILE)
            elif z >= 0.5:
                tile = arcade.Sprite("sprites/map/biom-regular-height-50.png", SPRITE_SCALING_TILE)
            elif z >= 0.4:
                tile = arcade.Sprite("sprites/map/biom-regular-height-40.png", SPRITE_SCALING_TILE)
            elif z >= 0.3:
                tile = arcade.Sprite("sprites/map/biom-regular-height-30.png", SPRITE_SCALING_TILE)
            elif z >= 0.2:
                tile = arcade.Sprite("sprites/map/biom-regular-height-20.png", SPRITE_SCALING_TILE)
            elif z >= 0.1:
                tile = arcade.Sprite("sprites/map/biom-regular-height-10.png", SPRITE_SCALING_TILE)
            else:
                tile = arcade.Sprite("sprites/map/biom-regular-height-00.png", SPRITE_SCALING_TILE)
            tile.center_x = x
            tile.center_y = y
            self.tiles_list.append(tile)

def on_draw(self):
    arcade.start_render()
    self.tiles_list.draw()

In each update:

  1. arcade.SpriteList() with 16x16 sprites is cleared
  2. seed is randomly generated
  3. for each X and Y on the screen Z dimension is calculated
  4. depending on Z (height), sprite (water/ grass/rock/snow) is selected
  5. sprites are added to the arcade.SpriteList()
  6. back to point 1.

Preview

procedural-map-generation

Code with sprites is avaivable on my github here.

TO-DO: It would be cool to also have an example where seed is static and we only move on single axis.