I've made a "heightmap" terrain generator similar to my previous one, except this one has some improvements, and some new features.
The way this one works is similar to my last one, but slightly different, so I'll run through it again.
- First, a list of
NoneTypes
is generated with a setwidth
andheight
. - Next, the generator "seeds" certain areas in this list with random values between the minimum world height, and maximum world height.
- After that, the generator iterates over this data a set amount of times, and determines neighbor tile values based on the height value of the current tile. It is determined like this:
tile + randint(self.min_change, self.max_change)
. - After the generator is done iterating, it does one loop through the data and replaces any remaining
NoneTypes
with the minimum height value. - Finally, if
smoothing=True
, then the smoother iterates over the data once, and adjusts the values of neighbor tiles to make height distances "less extreme".
"""
A basic library containing a class for
generating basic terrain data.
"""
from random import randint
class Terrain(object):
"""
Terrain object for storing and generating
"realistic" looking terrain in an array.
"""
def __init__(self,
min_height, max_height,
min_change, max_change,
data_width, data_height,
seeding_iterations, generation_iterations,
smoothing=True, replace_NoneTypes=True):
self.min_height = min_height
self.max_height = max_height
self.min_change = min_change
self.max_change = max_change
self.data_width = data_width
self.data_height = data_height
self.seeding_iterations = seeding_iterations
self.generation_iterations = generation_iterations
self.smoothing = smoothing
self.replace_NoneTypes = replace_NoneTypes
self.world_data = None
def _assert_arguments(self):
"""
Assert the provided arguments to __init__ and
make sure that they are valid.
"""
assert self.max_height > self.min_height, "Maximum height must be larger than minimum height."
assert self.max_change > self.min_change, "Maximum change must be larger than minimum change."
assert self.data_width > 0, "Width must be greater than zero."
assert self.data_height > 0, "Height must be greater than zero."
assert self.seeding_iterations > 0, "Seeding iterations must be greater than zero."
assert self.generation_iterations > 0, "Generation iterations must be greater than zero."
def _generate_inital_terrain(self):
"""
Initalizes the self.world_data array with a
NoneType value.
"""
self.world_data = [[
None for _ in range(self.data_width)
] for _ in range(self.data_height)
]
def _seed_terrain(self):
"""
"Seeds" the terrain by choosing a random spot
in self.world_data, and setting it to a random
value between self.min_height and self.max_height.
"""
for _ in range(self.seeding_iterations):
random_x = randint(0, self.data_width - 1)
random_y = randint(0, self.data_height - 1)
self.world_data[random_x][random_y] = randint(
self.min_height, self.max_height
)
def _generate_iterate(self):
"""
Generates the terrain by iterating n times
and iterating over the world data and changing
terrain values based on tile values.
"""
for _ in range(self.generation_iterations):
for index_y, tile_row in enumerate(self.world_data):
for index_x, tile in enumerate(tile_row):
upper_index = index_y + 1
lower_index = index_y - 1
right_index = index_x + 1
left_index = index_x - 1
if tile is not None:
upper_value = tile + randint(self.min_change, self.max_change)
lower_value = tile + randint(self.min_change, self.max_change)
right_value = tile + randint(self.min_change, self.max_change)
left_value = tile + randint(self.min_change, self.max_change)
try:
if self.min_height <= tile <= self.max_height:
self.world_data[upper_index][index_x] = upper_value
self.world_data[lower_index][index_x] = lower_value
self.world_data[index_y][right_index] = right_value
self.world_data[index_y][left_index] = left_value
except IndexError:
continue
def _replace_NoneTypes(self):
"""
Iterates over the world data one last time
and replaces any remaining NoneType's with
the minimum height value.
"""
if self.replace_NoneTypes:
for index_y, tile_row in enumerate(self.world_data):
for index_x, tile in enumerate(tile_row):
if tile is None:
self.world_data[index_y][index_x] = self.min_height
def _smooth_terrain(self):
"""
"Smoothes" the terrain a into slightly more
"realistic" landscape.
"""
if self.smoothing:
for index_y, tile_row in enumerate(self.world_data):
for index_x, tile in enumerate(tile_row):
try:
upper_tile = self.world_data[index_y - 1][index_x]
lower_tile = self.world_data[index_y + 1][index_x]
right_tile = self.world_data[index_y][index_x + 1]
left_tile = self.world_data[index_y][index_x - 1]
if upper_tile - tile >= 3:
self.world_data[index_y - 1][index_x] -= abs(upper_tile - tile) + 1
if tile - upper_tile <= -3:
self.world_data[index_y - 1][index_x] += abs(tile - upper_tile) - 1
if lower_tile - tile >= 3:
self.world_data[index_y + 1][index_x] -= abs(lower_tile - tile) + 1
if tile - lower_tile <= -3:
self.world_data[index_y + 1][index_x] += abs(tile - lower_tile) - 1
if right_tile - tile >= 3:
self.world_data[index_y][index_x + 1] -= abs(right_tile - tile) + 1
if tile - right_tile <= -3:
self.world_data[index_y][index_x + 1] += abs(tile - right_tile) - 1
if left_tile - tile >= 3:
self.world_data[index_y][index_x - 1] -= abs(left_tile - tile) + 1
if tile - left_tile <= -3:
self.world_data[index_y][index_x - 1] += abs(tile - left_tile) - 1
except IndexError:
continue
def generate_data(self):
"""
Puts together all the functions required
for generation into one container for running.
"""
self._assert_arguments()
self._generate_inital_terrain()
self._seed_terrain()
self._generate_iterate()
self._replace_NoneTypes()
self._smooth_terrain()
def return_data(self):
"""
Returns the world data as a list.
"""
return self.world_data
def debug_data(self):
"""
Prints out the height values in self.world_data
for debugging and visuals.
"""
for tile_row in self.world_data:
print ' '.join([str(height_value) for height_value in tile_row])
Here's an example of how this is used.
terr = Terrain(1, 8, -1, 1, 20, 20, 3, 10, smoothing=True)
terr.generate_data()
terr.debug_data()
Anyways, here are a few things, that if you want to answer, you can, that I'd like suggestions for.
- How can I make this faster? The larger the world size, the longer the generation time.
- What am I doing that is "un-pythonic"?
- Are there any issues that I missed?