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:
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:
- arcade.SpriteList() with 16x16 sprites is cleared
- seed is randomly generated
- for each X and Y on the screen Z dimension is calculated
- depending on Z (height), sprite (water/ grass/rock/snow) is selected
- sprites are added to the arcade.SpriteList()
- back to point 1.
Preview
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.