Compare commits

...

5 Commits

Author SHA1 Message Date
Valentin Valls b86038301b
Extend world generation (#51)
* Rework the texture mapping to use human logical indexes (top-down)

* Use a bigger texture

* Add stone blocks with ore

* Rework the generator to also split sectors vertically

* Create a noise module to handle octaves

* Use the new noise class

* Generate underground

* Make the code independant from the size of the section

* Speed up sectorize

* Create a world module for world model and sector

* Use the default world queue to register new sectors

* At start put the player on the summit

* Collide the player with the enclosure

* Add a get_focus_block

* Set sector size to 8

* Improve the world structure for dense maps

* Speed up exposed method

* Tune the visibility distance

* Create a single vertex list per section

* Use constances
2020-05-25 08:53:04 +09:00
Benjamin Moran 186f3718eb Add perlin noise library for use with genworld module. 2020-05-21 17:04:00 +09:00
Valentin Valls 4cbe89aa12
Extend world generation (#50)
* Add clouds

* Use noise lib

* Split snow and dirt with snow

* Add water block

* Clean up import

* Move the actor on top of the map

* Use noise generator to create the map

* Refactor the world generation into a class

* Generate trees

* Generate sectors on the fly

* Add a property to switch to a never endding world

* Rework the sector visibility as a game method

* Use absolute path

- Fix problem when used with cProfile

* Move function arguments as world class attributes

* Thread the WorldGenerator computation
2020-05-21 16:57:25 +09:00
Benjamin dcb201e8a0
Merge pull request #49 from vallsv/patch-1
Replace deprecated time.clock
2020-05-17 09:32:47 +09:00
Valentin Valls 168777fd08
Replace deprecated time.clock 2020-05-09 13:21:00 +02:00
11 changed files with 1582 additions and 340 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -32,13 +32,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
def _tex_coord(x, y, n=4):
def _tex_coord(x, y, n=8):
""" Return the bounding vertices of the texture square.
"""
m = 1.0 / n
dx = x * m
dy = y * m
dy = 1 - (y + 1) * m
return dx, dy, dx + m, dy, dx + m, dy + m, dx, dy + m
@ -69,17 +69,25 @@ class Block:
self.tex_coords = tex_coords
DIRT = Block('dirt', _tex_coords((0, 1), (0, 1), (0, 1)))
DIRT_WITH_GRASS = Block('dirt_with_grass', _tex_coords((1, 0), (0, 1), (0, 0)))
SAND = Block('sand', _tex_coords((1, 1), (1, 1), (1, 1)))
COBBLESTONE = Block('cobblestone', _tex_coords((2, 0), (2, 0), (2, 0)))
BRICK_COBBLESTONE = Block('brick_cobblestone', _tex_coords((3, 0), (3, 0), (3, 0)))
BRICK = Block('brick', _tex_coords((3, 1), (3, 1), (3, 1)))
BEDSTONE = Block('bedstone', _tex_coords((2, 1), (2, 1), (2, 1)))
TREE = Block('tree', _tex_coords((1, 2), (1, 2), (0, 2)))
LEAVES = Block('leaves', _tex_coords((2, 2), (2, 2), (2, 2)))
SNOW = Block('snow', _tex_coords((1, 3), (0, 1), (0, 3)))
WOODEN_PLANKS = Block('wooden_planks', _tex_coords((2, 3), (2, 3), (2, 3)))
DIRT = Block('dirt', _tex_coords((0, 2), (0, 2), (0, 2)))
DIRT_WITH_GRASS = Block('dirt_with_grass', _tex_coords((1, 3), (0, 2), (0, 3)))
SAND = Block('sand', _tex_coords((1, 2), (1, 2), (1, 2)))
COBBLESTONE = Block('cobblestone', _tex_coords((2, 3), (2, 3), (2, 3)))
BRICK_COBBLESTONE = Block('brick_cobblestone', _tex_coords((3, 3), (3, 3), (3, 3)))
BRICK = Block('brick', _tex_coords((3, 2), (3, 2), (3, 2)))
BEDSTONE = Block('bedstone', _tex_coords((2, 2), (2, 2), (2, 2)))
TREE = Block('tree', _tex_coords((1, 1), (1, 1), (0, 1)))
LEAVES = Block('leaves', _tex_coords((2, 1), (2, 1), (2, 1)))
SNOW = Block('snow', _tex_coords((1, 0), (1, 0), (1, 0)))
WOODEN_PLANKS = Block('wooden_planks', _tex_coords((2, 0), (2, 0), (2, 0)))
CLOUD = Block('cloud', _tex_coords((1, 0), (1, 0), (1, 0)))
DIRT_WITH_SNOW = Block('dirt_with_snow', _tex_coords((1, 0), (0, 2), (0, 0)))
WATER = Block('water', _tex_coords((3, 1), (3, 1), (3, 1)))
STONE = Block('stone', _tex_coords((0, 4), (0, 4), (0, 4)))
STONE_WITH_SNOW = Block('stone_with_snow', _tex_coords((1, 0), (0, 4), (0, 5)))
COAL_ORE = Block('coal_ore', _tex_coords((1, 4), (1, 4), (1, 4)))
IRON_ORE = Block('iron_ore', _tex_coords((2, 4), (2, 4), (2, 4)))
GOLD_ORE = Block('gold_ore', _tex_coords((3, 4), (3, 4), (3, 4)))
# A reference to the 6 faces (sides) of the blocks:
FACES = [(0, 1, 0), (0, -1, 0), (-1, 0, 0), (1, 0, 0), (0, 0, 1), (0, 0, -1)]

View File

@ -65,7 +65,7 @@ FOG_START = 20.0
FOG_END = 60.0
# Size of sectors used to ease block loading.
SECTOR_SIZE = 16
SECTOR_SIZE = 8
# Speed
WALKING_SPEED = 3

View File

@ -31,50 +31,395 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import math
import concurrent.futures
import random
from .blocks import *
from .utilities import *
from .graphics import BlockGroup
from game import utilities
from .noise import Noise
from .world import Sector
def generate_world(self):
"""Randomly generate a new world and place all the blocks"""
n = 80 # 1/2 width and height of world
s = 1 # step size
y = 0 # initial y height
for x in range(-n, n + 1, s):
for z in range(-n, n + 1, s):
# create a layer stone an DIRT_WITH_GRASS everywhere.
self.add_block((x, y - 2, z), DIRT_WITH_GRASS, immediate=True)
self.add_block((x, y - 3, z), BEDSTONE, immediate=False)
if x in (-n, n) or z in (-n, n):
# create outer walls.
# Setting values for the Bedrock (depth, and height of the perimeter wall).
for dy in range(-2, 9):
self.add_block((x, y + dy, z), BEDSTONE, immediate=False)
class WorldGenerator:
"""Generate a world model"""
# generate the hills randomly
def __init__(self):
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
"""This thread pool will execute one task at a time. Others are stacked,
waiting for execution."""
if not HILLS_ON:
self.callback = None
"""Callback for the result of the executor"""
self.hills_enabled = True
"""If True the generator uses a procedural generation for the map.
Else, a flat floor will be generated."""
self.y = 4
"""Initial y height"""
self.cloudiness = 0.35
"""The cloudiness can be custom to change the about of clouds generated.
0 means blue sky, and 1 means white sky."""
self.y_cloud = self.y + 20
"""y-position of the clouds."""
self.nb_trees = 6
"""Max number of trees to generate per sectors"""
self.tree_chunk_size = 32
"""The number of tree will be generated in this chunk size (in block)"""
self.enclosure = True
"""If true the world is limited to a fixed size, else the world is infinitely
generated."""
self.enclosure_size = 80
"""1/2 width (in x and z) of the enclosure"""
self.enclosure_height = 12
"""Enclosure height, if generated"""
self.terrain_gen = Noise(frequency=1 / (38 * 256), octaves=4)
"""Raw generator used to create the terrain"""
self.cloud_gen = Noise(frequency=1 / (20 * 256), octaves=3)
"""Raw generator used to create the clouds"""
self.gold_gen = Noise(frequency=1 / (64 * 256), octaves=2, persistence=0.1)
self.iron_gen = Noise(frequency=1 / (32 * 256), octaves=2, persistence=0.1)
self.coal_gen = Noise(frequency=1 / (16 * 256), octaves=2, persistence=0.1)
"""Raw generator for ore"""
self.terrain_gen.randomize()
self.cloud_gen.randomize()
self.lookup_terrain = []
def add_terrain_map(height, terrains):
"""Add a new entry to the height map lookup table.
`height` will be the height at this part of the height map.
and `terrains` contains blocks for each vertical voxels. The last
one is on top, and the first one is used for all the remaining voxels
on bottom.
"""
self.lookup_terrain.append((height, terrains))
add_terrain_map(1, [WATER])
add_terrain_map(1, [WATER])
add_terrain_map(1, [WATER])
add_terrain_map(1, [WATER])
add_terrain_map(1, [WATER])
add_terrain_map(1, [WATER])
add_terrain_map(1, [SAND])
add_terrain_map(1, [SAND])
add_terrain_map(2, [SAND])
add_terrain_map(1, [SAND])
add_terrain_map(1, [SAND])
add_terrain_map(1, [DIRT_WITH_GRASS])
add_terrain_map(1, [DIRT_WITH_GRASS])
add_terrain_map(2, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(2, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(3, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(4, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(4, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(5, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(5, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(6, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(6, [DIRT, DIRT_WITH_GRASS])
add_terrain_map(7, [DIRT])
add_terrain_map(8, [DIRT])
add_terrain_map(9, [DIRT])
add_terrain_map(10, [DIRT, DIRT_WITH_SNOW])
add_terrain_map(11, [DIRT, DIRT_WITH_SNOW, SNOW])
add_terrain_map(12, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
add_terrain_map(13, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
add_terrain_map(14, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
add_terrain_map(15, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
def set_callback(self, callback):
"""Set a callback called when a new sector is computed"""
self.callback = callback
def request_sector(self, sector):
"""Compute the content of a sector asynchronously and return the result to a
callback already specified to this generator.
"""
def send_result(future):
chunk = future.result()
self.callback(chunk)
future = self.executor.submit(self.generate, sector)
future.add_done_callback(send_result)
def _iter_xz(self, chunk):
"""Iterate all the xz block positions from a sector"""
xmin, _, zmin = chunk.min_block
xmax, _, zmax = chunk.max_block
for x in range(xmin, xmax):
for z in range(zmin, zmax):
yield x, z
def _iter_xyz(self, chunk):
"""Iterate all the xyz block positions from a sector"""
xmin, ymin, zmin = chunk.min_block
xmax, ymax, zmax = chunk.max_block
for x in range(xmin, xmax):
for y in range(ymin, ymax):
for z in range(zmin, zmax):
yield x, y, z
def generate(self, sector):
"""Generate a specific sector of the world and place all the blocks"""
chunk = Sector(sector)
"""Store the content of this sector"""
if self.enclosure:
self._generate_enclosure(chunk)
if self.hills_enabled:
self._generate_random_map(chunk)
else:
self._generate_floor(chunk)
if self.cloudiness > 0:
self._generate_clouds(chunk)
if self.nb_trees > 0:
self._generate_trees(chunk)
if not self.enclosure:
self._generate_underworld(chunk)
return chunk
def _generate_enclosure(self, chunk):
"""Generate an enclosure with unbreakable blocks on the floor and
and on the side.
"""
y_pos = self.y - 2
height = self.enclosure_height
if not chunk.contains_y_range(y_pos, y_pos + height):
# Early break, there is no enclosure here
return
o = n - 10
for _ in range(120):
a = random.randint(-o, o) # x position of the hill
b = random.randint(-o, o) # z position of the hill
c = -1 # base of the hill
h = random.randint(1, 6) # height of the hill
s = random.randint(4, 8) # 2 * s is the side length of the hill
d = 1 # how quickly to taper off the hills
block = random.choice([DIRT_WITH_GRASS, SNOW, SAND])
for y in range(c, c + h):
for x in range(a - s, a + s + 1):
for z in range(b - s, b + s + 1):
if (x - a) ** 2 + (z - b) ** 2 > (s + 1) ** 2:
continue
if (x - 0) ** 2 + (z - 0) ** 2 < 5 ** 2: # 6 = flat map
continue
self.add_block((x, y, z), block, immediate=False)
s -= d # decrement side length so hills taper off
y_pos = self.y - 2
half_size = self.enclosure_size
n = half_size
for x, z in self._iter_xz(chunk):
if x < -n or x > n or z < -n or z > n:
continue
# create a layer stone an DIRT_WITH_GRASS everywhere.
pos = (x, y_pos, z)
chunk.add_block(pos, BEDSTONE)
# create outer walls.
# Setting values for the Bedrock (depth, and height of the perimeter wall).
if x in (-n, n) or z in (-n, n):
for dy in range(height):
pos = (x, y_pos + dy, z)
chunk.add_block(pos, BEDSTONE)
def _generate_floor(self, chunk):
"""Generate a standard floor at a specific height"""
y_pos = self.y - 2
if not chunk.contains_y(y_pos):
# Early break, there is no clouds here
return
n = self.enclosure_size
for x, z in self._iter_xz(chunk):
if self.enclosure:
if x <= -n or x >= n or z <= -n or z >= n:
continue
chunk.add_block((x, y_pos, z), DIRT_WITH_GRASS)
def _get_biome(self, x, z):
c = self.terrain_gen.noise2(x, z)
c = int((c + 1) * 0.5 * len(self.lookup_terrain))
if c < 0:
c = 0
nb_block, terrains = self.lookup_terrain[c]
return nb_block, terrains
def _generate_random_map(self, chunk):
n = self.enclosure_size
y_pos = self.y - 2
if not chunk.contains_y_range(y_pos, y_pos + 20):
return
for x, z in self._iter_xz(chunk):
if self.enclosure:
if x <= -n or x >= n or z <= -n or z >= n:
continue
nb_block, terrains = self._get_biome(x, z)
for i in range(nb_block):
block = terrains[-1-i] if i < len(terrains) else terrains[0]
chunk.add_block((x, y_pos + nb_block - i, z), block)
def _generate_trees(self, chunk):
"""Generate trees in the map
For now it do not generate trees between 2 sectors, and use rand
instead of a procedural generation.
"""
if not chunk.contains_y_range(self.y, self.y + 20):
return
def get_biome(x, y, z):
"""Return the biome at a location of the map plus the first empty place."""
nb_block, terrains = self._get_biome(x, z)
y = self.y - 2 + nb_block
block = terrains[-1]
return block, y
sector_pos = chunk.position
# Common root for many chunks
# So what it is easier to generate trees between 2 chunks
sector_root_x = (sector_pos[0] * SECTOR_SIZE // self.tree_chunk_size) * self.tree_chunk_size
sector_root_z = (sector_pos[2] * SECTOR_SIZE // self.tree_chunk_size) * self.tree_chunk_size
random.seed(sector_root_x + sector_root_z)
nb_trees = random.randint(0, self.nb_trees)
n = self.enclosure_size - 3
y_pos = self.y - 2
for _ in range(nb_trees):
x = sector_root_x + 3 + random.randint(0, self.tree_chunk_size - 7)
z = sector_root_z + 3 + random.randint(0, self.tree_chunk_size - 7)
if self.enclosure:
if x < -n + 2 or x > n - 2 or z < -n + 2 or z > n - 2:
continue
biome, start_pos = get_biome(x, y_pos + 1, z)
if biome not in [DIRT, DIRT_WITH_GRASS, SAND]:
continue
if biome == SAND:
height = random.randint(4, 5)
self._create_coconut_tree(chunk, x, start_pos, z, height)
elif start_pos - self.y > 6:
height = random.randint(3, 5)
self._create_fir_tree(chunk, x, start_pos, z, height)
else:
height = random.randint(3, 7 - (start_pos - y_pos) // 3)
self._create_default_tree(chunk, x, start_pos, z, height)
def _create_plus(self, chunk, x, y, z, block):
chunk.add_block((x, y, z), block)
chunk.add_block((x - 1, y, z), block)
chunk.add_block((x + 1, y, z), block)
chunk.add_block((x, y, z - 1), block)
chunk.add_block((x, y, z + 1), block)
def _create_box(self, chunk, x, y, z, block):
for i in range(9):
dx, dz = i // 3 - 1, i % 3 - 1
chunk.add_block((x + dx, y, z + dz), block)
def _create_default_tree(self, chunk, x, y, z, height):
if height == 0:
return
if height == 1:
self._create_plus(x, y, z, LEAVES)
return
if height == 2:
chunk.add_block((x, y, z), TREE)
chunk.add_block((x, y + 1, z), LEAVES)
return
y_tree = 0
root_height = 2 if height >= 4 else 1
for _ in range(root_height):
chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
self._create_plus(chunk, x, y + y_tree, z, LEAVES)
y_tree += 1
for _ in range(height - 4):
self._create_box(chunk, x, y + y_tree, z, LEAVES)
y_tree += 1
self._create_plus(chunk, x, y + y_tree, z, LEAVES)
def _create_fir_tree(self, chunk, x, y, z, height):
if height == 0:
return
if height == 1:
self._create_plus(chunk, x, y, z, LEAVES)
return
if height == 2:
chunk.add_block((x, y, z), TREE)
chunk.add_block((x, y + 1, z), LEAVES)
return
y_tree = 0
chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
self._create_box(chunk, x, y + y_tree, z, LEAVES)
chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
h_layer = (height - 2) // 2
for _ in range(h_layer):
self._create_plus(chunk, x, y + y_tree, z, LEAVES)
chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
for _ in range(h_layer):
chunk.add_block((x, y + y_tree, z), LEAVES)
y_tree += 1
def _create_coconut_tree(self, chunk, x, y, z, height):
y_tree = 0
for _ in range(height - 1):
chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
chunk.add_block((x + 1, y + y_tree, z), LEAVES)
chunk.add_block((x - 1, y + y_tree, z), LEAVES)
chunk.add_block((x, y + y_tree, z + 1), LEAVES)
chunk.add_block((x, y + y_tree, z - 1), LEAVES)
if height >= 5:
chunk.add_block((x + 2, y + y_tree, z), LEAVES)
chunk.add_block((x - 2, y + y_tree, z), LEAVES)
chunk.add_block((x, y + y_tree, z + 2), LEAVES)
chunk.add_block((x, y + y_tree, z - 2), LEAVES)
if height >= 6:
y_tree -= 1
chunk.add_block((x + 3, y + y_tree, z), LEAVES)
chunk.add_block((x - 3, y + y_tree, z), LEAVES)
chunk.add_block((x, y + y_tree, z + 3), LEAVES)
chunk.add_block((x, y + y_tree, z - 3), LEAVES)
def _generate_clouds(self, chunk):
"""Generate clouds at this `self.y_cloud`.
"""
y_pos = self.y_cloud
if not chunk.contains_y(y_pos):
# Early break, there is no clouds here
return
for x, z in self._iter_xz(chunk):
pos = (x, y_pos, z)
if not chunk.empty(pos):
continue
c = self.cloud_gen.noise2(x, z)
if (c + 1) * 0.5 < self.cloudiness:
chunk.add_block(pos, CLOUD)
def _get_stone(self, pos):
"""Returns the expected mineral at a specific location.
The input location have to be already known as a stone location.
"""
v = self.gold_gen.noise3(*pos)
if 0.02 < v < 0.03:
return GOLD_ORE
v = self.iron_gen.noise3(*pos)
if 0.015 < v < 0.03:
return IRON_ORE
v = self.coal_gen.noise3(*pos)
if 0.01 < v < 0.03:
return COAL_ORE
return STONE
def _generate_underworld(self, chunk):
if chunk.min_block[1] > self.y - 3:
return
for x, y, z in self._iter_xyz(chunk):
if y > self.y - 2:
continue
pos = x, y, z
block = self._get_stone(pos)
chunk.add_block(pos, block)

101
game/noise.py Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
________ ______ ______ __
| \ / \ / \ | \
\$$$$$$$$______ ______ ______ ______ | $$$$$$\ ______ ______ | $$$$$$\ _| $$_
| $$ / \ / \ / \ | \ | $$ \$$ / \ | \ | $$_ \$$| $$ \
| $$ | $$$$$$\| $$$$$$\| $$$$$$\ \$$$$$$\| $$ | $$$$$$\ \$$$$$$\| $$ \ \$$$$$$
| $$ | $$ $$| $$ \$$| $$ \$$/ $$| $$ __ | $$ \$$/ $$| $$$$ | $$ __
| $$ | $$$$$$$$| $$ | $$ | $$$$$$$| $$__/ \| $$ | $$$$$$$| $$ | $$| \
| $$ \$$ \| $$ | $$ \$$ $$ \$$ $$| $$ \$$ $$| $$ \$$ $$
\$$ \$$$$$$$ \$$ \$$ \$$$$$$$ \$$$$$$ \$$ \$$$$$$$ \$$ \$$$$
Copyright (C) 2013 Michael Fogleman
Copyright (C) 2018/2019 Stefano Peris <xenonlab.develop@gmail.com>
Github repository: <https://github.com/XenonLab-Studio/TerraCraft>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from libs import perlin
class Noise(perlin.SimplexNoise):
"""Configure a coherent noise generator.
- `frequency`: Frequency of the noise according to the input values (default: 1.0).
A frequency of 1 means that input between 0..1 will cover the period
of the permutation table. After that the pattern is repeated.
- `octaves`: Amount of passes to generate a multi-frequencial noise (default: 1).
- `lacunarity`: If `octaves` is used, coefficient used to multiply the frequency
between two consecutive octaves (default is 2.0).
- `persistence`: If `octaves` is used, coefficient used to multipy the amplitude
between two consecutive octaves (default is 0.5, divide by 2).
"""
def __init__(self, frequency=1.0, octaves=1, lacunarity=2.0, persistence=0.5):
super()
self.frequency = frequency
octaves = int(octaves)
assert octaves >= 1
self.octaves = octaves
self.persistence = persistence
self.lacunarity = lacunarity
def noise2(self, x, y):
"""Generate a noise 2D.
"""
coef = self.period * self.frequency
x = x * coef
y = y * coef
if self.octaves == 1:
return super().noise2(x, y)
else:
frequency = 1.0
amplitude = 1.0
value = 0
maximun = 0
for _ in range(self.octaves):
value += super().noise2(x * frequency, y * frequency) * amplitude
maximun += amplitude;
frequency *= self.lacunarity
amplitude *= self.persistence
return value / maximun
def noise3(self, x, y, z):
"""Generate a noise 3D.
"""
coef = self.period * self.frequency
x = x * coef
y = y * coef
z = z * coef
if self.octaves == 1:
return super().noise3(x, y, z)
else:
frequency = 1.0
amplitude = 1.0
value = 0
maximun = 0
for _ in range(self.octaves):
value += super().noise3(x * frequency,
y * frequency,
z * frequency) * amplitude
maximun += amplitude;
frequency *= self.lacunarity
amplitude *= self.persistence
return value / maximun

View File

@ -31,9 +31,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import random
import time
import pyglet
from collections import deque
@ -46,7 +44,9 @@ from pyglet.graphics import OrderedGroup
from .blocks import *
from .utilities import *
from .graphics import BlockGroup
from .genworld import *
from .genworld import WorldGenerator
from .world import Model
class AudioEngine:
"""A high level audio engine for easily playing SFX and Music."""
@ -225,7 +225,7 @@ class GameScene(Scene):
# Current (x, y, z) position in the world, specified with floats. Note
# that, perhaps unlike in math class, the y-axis is the vertical axis.
self.position = (0, 0, 0)
self.position = (SECTOR_SIZE // 2, 6, SECTOR_SIZE // 2)
# First element is rotation of the player in the x-z plane (ground
# plane) measured from the z-axis down. The second is the rotation
@ -238,6 +238,9 @@ class GameScene(Scene):
# Which sector the player is currently in.
self.sector = None
# True if the location of the camera have changed between an update
self.frustum_updated = False
# Velocity in the y (upward) direction.
self.dy = 0
@ -345,6 +348,31 @@ class GameScene(Scene):
dz = 0.0
return dx, dy, dz
def init_player_on_summit(self):
"""Make sure the sector containing the actor is loaded and the player is on top of it.
"""
generator = self.model.generator
x, y, z = self.position
free_height = 0
limit = 100
while free_height < PLAYER_HEIGHT and limit:
pos = x , y, z
sector_position = sectorize(pos)
if sector_position not in self.model.sectors:
sector = generator.generate(sector_position)
self.model.register_sector(sector)
if self.model.empty(pos):
free_height += 1
else:
free_height = 0
y = y + 1
limit -= 1
position = x, y - PLAYER_HEIGHT + 1, z
if self.position != position:
self.position = position
self.frustum_updated = True
def update(self, dt):
""" This method is scheduled to be called repeatedly by the pyglet
clock.
@ -364,17 +392,22 @@ class GameScene(Scene):
has_save = self.scene_manager.save.load_world(self.model)
if not has_save:
generate_world(self.model)
generator = WorldGenerator()
generator.y = self.position[1]
generator.hills_enabled = HILLS_ON
self.model.generator = generator
self.init_player_on_summit()
self.initialized = True
self.model.process_queue()
sector = sectorize(self.position)
if sector != self.sector:
self.model.change_sectors(self.sector, sector)
# if self.sector is None:
# self.model.process_entire_queue()
if self.frustum_updated:
sector = sectorize(self.position)
self.update_shown_sectors(self.position, self.rotation)
self.sector = sector
self.frustum_updated = False
m = 8
dt = min(dt, 0.2)
for _ in range(m):
@ -407,9 +440,11 @@ class GameScene(Scene):
# collisions
x, y, z = self.position
x, y, z = self.collide((x + dx, y + dy, z + dz), PLAYER_HEIGHT)
# fix bug for jumping outside the wall and falling to infinity.
y = max(-1.25, y)
self.position = (x, y, z)
position = (x, y, z)
if self.position != position:
self.position = position
self.frustum_updated = True
def collide(self, position, height):
""" Checks to see if the player at the given `position` and `height`
@ -447,7 +482,7 @@ class GameScene(Scene):
op = list(np)
op[1] -= dy
op[i] += face[i]
if tuple(op) not in self.model.world:
if self.model.empty(tuple(op), must_be_loaded=True):
continue
p[i] -= (d - pad) * face[i]
if face == (0, -1, 0) or face == (0, 1, 0):
@ -455,8 +490,59 @@ class GameScene(Scene):
# falling / rising.
self.dy = 0
break
generator = self.model.generator
if generator is None:
# colliding with the virtual floor
# to avoid to fall infinitely.
p[1] = max(-1.25, p[1])
else:
if generator.enclosure:
# Force the player inside the enclosure
s = generator.enclosure_size
if p[0] < -s:
p[0] = -s
elif p[0] > s:
p[0] = s
if p[2] < -s:
p[2] = -s
elif p[2] > s:
p[2] = s
return tuple(p)
def update_shown_sectors(self, position, rotation):
"""Update shown sectors according to the actual frustum.
A sector is a contiguous x, y sub-region of world. Sectors are
used to speed up world rendering.
"""
sector = sectorize(position)
if self.sector == sector:
# The following computation is based on the actual sector
# So if there is no changes on the sector, it have to display
# The exact same thing
return
sectors_to_show = []
pad = int(FOG_END) // SECTOR_SIZE
for dx in range(-pad, pad + 1):
for dy in range(-pad, pad + 1):
for dz in range(-pad, pad + 1):
# Manathan distance
dist = abs(dx) + abs(dy) + abs(dz)
if dist > pad + pad // 2:
# Skip sectors outside of the sphere of radius pad+1
continue
x, y, z = sector
sectors_to_show.append((dist, x + dx, y + dy, z + dz))
# Sort by distance to the player in order to
# displayed closest sectors first
sectors_to_show = sorted(sectors_to_show)
sectors_to_show = [s[1:] for s in sectors_to_show]
self.model.show_only_sectors(sectors_to_show)
def on_mouse_press(self, x, y, button, modifiers):
"""Event handler for the Window.on_mouse_press event.
@ -484,7 +570,7 @@ class GameScene(Scene):
if previous:
self.model.add_block(previous, self.block)
elif button == pyglet.window.mouse.LEFT and block:
texture = self.model.world[block]
texture = self.model.get_block(block)
if texture != BEDSTONE:
self.model.remove_block(block)
self.audio.play(self.destroy_sfx)
@ -509,7 +595,10 @@ class GameScene(Scene):
x, y = self.rotation
x, y = x + dx * LOOK_SPEED_X, y + dy * LOOK_SPEED_Y
y = max(-90, min(90, y))
self.rotation = (x, y)
rotation = (x, y)
if self.rotation != rotation:
self.rotation = rotation
self.frustum_updated = True
def on_key_press(self, symbol, modifiers):
"""Event handler for the Window.on_key_press event.
@ -599,6 +688,8 @@ class GameScene(Scene):
self.running = False
elif symbol == key.LSHIFT:
self.dy = 0
elif symbol == key.P:
breakpoint()
def on_resize(self, width, height):
"""Event handler for the Window.on_resize event.
@ -629,13 +720,17 @@ class GameScene(Scene):
if self.toggleLabel:
self.draw_label()
def get_focus_block(self):
vector = self.get_sight_vector()
block = self.model.hit_test(self.position, vector)[0]
return block
def draw_focused_block(self):
""" Draw black edges around the block that is currently under the
crosshairs.
"""
vector = self.get_sight_vector()
block = self.model.hit_test(self.position, vector)[0]
block = self.get_focus_block()
if block:
x, y, z = block
self.highlight.vertices[:] = cube_vertices(x, y, z, 0.51)
@ -648,277 +743,15 @@ class GameScene(Scene):
"""
x, y, z = self.position
self.info_label.text = 'FPS = [%02d] : COORDS = [%.2f, %.2f, %.2f] : %d / %d' % (
pyglet.clock.get_fps(), x, y, z,
self.model.currently_shown, len(self.model.world))
elements = []
elements.append("FPS = [%02d]" % pyglet.clock.get_fps())
elements.append("COORDS = [%.2f, %.2f, %.2f]" % (x, y, z))
elements.append("SECTORS = %d [+%d]" % (len(self.model.sectors), len(self.model.requested)))
elements.append("BLOCKS = %d" % self.model.count_blocks())
self.info_label.text = ' : '.join(elements)
self.info_label.draw()
class Model(object):
def __init__(self, batch, group):
self.batch = batch
self.group = group
# A mapping from position to the texture of the block at that position.
# This defines all the blocks that are currently in the world.
self.world = {}
# Same mapping as `world` but only contains blocks that are shown.
self.shown = {}
# Mapping from position to a pyglet `VertextList` for all shown blocks.
self._shown = {}
# Mapping from sector to a list of positions inside that sector.
self.sectors = {}
#self.generate_world = generate_world(self)
# Simple function queue implementation. The queue is populated with
# _show_block() and _hide_block() calls
self.queue = deque()
@property
def currently_shown(self):
return len(self._shown)
def hit_test(self, position, vector, max_distance=NODE_SELECTOR):
""" Line of sight search from current position. If a block is
intersected it is returned, along with the block previously in the line
of sight. If no block is found, return None, None.
Parameters
----------
position : tuple of len 3
The (x, y, z) position to check visibility from.
vector : tuple of len 3
The line of sight vector.
max_distance : int
How many blocks away to search for a hit.
"""
m = 8
x, y, z = position
dx, dy, dz = vector
previous = None
for _ in range(max_distance * m):
checked_position = normalize((x, y, z))
if checked_position != previous and checked_position in self.world:
return checked_position, previous
previous = checked_position
x, y, z = x + dx / m, y + dy / m, z + dz / m
return None, None
def exposed(self, position):
""" Returns False if given `position` is surrounded on all 6 sides by
blocks, True otherwise.
"""
x, y, z = position
for dx, dy, dz in FACES:
if (x + dx, y + dy, z + dz) not in self.world:
return True
return False
def add_block(self, position, block, immediate=True):
""" Add a block with the given `texture` and `position` to the world.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to add.
block : Block object
An instance of the Block class.
immediate : bool
Whether or not to draw the block immediately.
"""
if position in self.world:
self.remove_block(position, immediate)
self.world[position] = block
self.sectors.setdefault(sectorize(position), []).append(position)
if immediate:
if self.exposed(position):
self.show_block(position)
self.check_neighbors(position)
def remove_block(self, position, immediate=True):
""" Remove the block at the given `position`.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to remove.
immediate : bool
Whether or not to immediately remove block from canvas.
"""
del self.world[position]
self.sectors[sectorize(position)].remove(position)
if immediate:
if position in self.shown:
self.hide_block(position)
self.check_neighbors(position)
def check_neighbors(self, position):
""" Check all blocks surrounding `position` and ensure their visual
state is current. This means hiding blocks that are not exposed and
ensuring that all exposed blocks are shown. Usually used after a block
is added or removed.
"""
x, y, z = position
for dx, dy, dz in FACES:
neighbor = (x + dx, y + dy, z + dz)
if neighbor not in self.world:
continue
if self.exposed(neighbor):
if neighbor not in self.shown:
self.show_block(neighbor)
else:
if neighbor in self.shown:
self.hide_block(neighbor)
def show_block(self, position, immediate=True):
""" Show the block at the given `position`. This method assumes the
block has already been added with add_block()
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to show.
immediate : bool
Whether or not to show the block immediately.
"""
block = self.world[position]
self.shown[position] = block
if immediate:
self._show_block(position, block)
else:
self._enqueue(self._show_block, position, block)
def _show_block(self, position, block):
""" Private implementation of the `show_block()` method.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to show.
block : Block instance
An instance of the Block class
"""
x, y, z = position
vertex_data = cube_vertices(x, y, z, 0.5)
# create vertex list
# FIXME Maybe `add_indexed()` should be used instead
self._shown[position] = self.batch.add(24, GL_QUADS, self.group,
('v3f/static', vertex_data),
('t2f/static', block.tex_coords))
def hide_block(self, position, immediate=True):
""" Hide the block at the given `position`. Hiding does not remove the
block from the world.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to hide.
immediate : bool
Whether or not to immediately remove the block from the canvas.
"""
self.shown.pop(position)
if immediate:
self._hide_block(position)
else:
self._enqueue(self._hide_block, position)
def _hide_block(self, position):
""" Private implementation of the 'hide_block()` method.
"""
self._shown.pop(position).delete()
def show_sector(self, sector):
""" Ensure all blocks in the given sector that should be shown are
drawn to the canvas.
"""
for position in self.sectors.get(sector, []):
if position not in self.shown and self.exposed(position):
self.show_block(position, False)
def hide_sector(self, sector):
""" Ensure all blocks in the given sector that should be hidden are
removed from the canvas.
"""
for position in self.sectors.get(sector, []):
if position in self.shown:
self.hide_block(position, False)
def change_sectors(self, before, after):
""" Move from sector `before` to sector `after`. A sector is a
contiguous x, y sub-region of world. Sectors are used to speed up
world rendering.
"""
before_set = set()
after_set = set()
pad = 4
for dx in range(-pad, pad + 1):
for dy in [0]: # range(-pad, pad + 1):
for dz in range(-pad, pad + 1):
if dx ** 2 + dy ** 2 + dz ** 2 > (pad + 1) ** 2:
continue
if before:
x, y, z = before
before_set.add((x + dx, y + dy, z + dz))
if after:
x, y, z = after
after_set.add((x + dx, y + dy, z + dz))
show = after_set - before_set
hide = before_set - after_set
for sector in show:
self.show_sector(sector)
for sector in hide:
self.hide_sector(sector)
def _enqueue(self, func, *args):
""" Add `func` to the internal queue.
"""
self.queue.append((func, args))
def _dequeue(self):
""" Pop the top function from the internal queue and call it.
"""
func, args = self.queue.popleft()
func(*args)
def process_queue(self):
""" Process the entire queue while taking periodic breaks. This allows
the game loop to run smoothly. The queue contains calls to
_show_block() and _hide_block() so this method should be called if
add_block() or remove_block() was called with immediate=False
"""
start = time.clock()
while self.queue and time.clock() - start < 1.0 / TICKS_PER_SEC:
self._dequeue()
def process_entire_queue(self):
""" Process the entire queue with no breaks.
"""
while self.queue:
self._dequeue()
class HelpScene(Scene):
def __init__(self, window):
self.window = window

View File

@ -66,5 +66,5 @@ def sectorize(position):
:param position: tuple of len 3
:return: tuple of len 3 representing the sector
"""
x, y, z = normalize(position)
return x//SECTOR_SIZE, 0, z//SECTOR_SIZE
x, y, z = position
return int(x) // SECTOR_SIZE, int(y) // SECTOR_SIZE, int(z) // SECTOR_SIZE

548
game/world.py Normal file
View File

@ -0,0 +1,548 @@
#!/bin/python3
"""
________ ______ ______ __
| \ / \ / \ | \
\$$$$$$$$______ ______ ______ ______ | $$$$$$\ ______ ______ | $$$$$$\ _| $$_
| $$ / \ / \ / \ | \ | $$ \$$ / \ | \ | $$_ \$$| $$ \
| $$ | $$$$$$\| $$$$$$\| $$$$$$\ \$$$$$$\| $$ | $$$$$$\ \$$$$$$\| $$ \ \$$$$$$
| $$ | $$ $$| $$ \$$| $$ \$$/ $$| $$ __ | $$ \$$/ $$| $$$$ | $$ __
| $$ | $$$$$$$$| $$ | $$ | $$$$$$$| $$__/ \| $$ | $$$$$$$| $$ | $$| \
| $$ \$$ \| $$ | $$ \$$ $$ \$$ $$| $$ \$$ $$| $$ \$$ $$
\$$ \$$$$$$$ \$$ \$$ \$$$$$$$ \$$$$$$ \$$ \$$$$$$$ \$$ \$$$$
Copyright (C) 2013 Michael Fogleman
Copyright (C) 2018/2019 Stefano Peris <xenonlab.develop@gmail.com>
Github repository: <https://github.com/XenonLab-Studio/TerraCraft>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import time
from collections import deque
from pyglet.gl import *
from .blocks import *
from .utilities import *
def iter_neighbors(position):
"""Iterate all the positions neighboring this position"""
x, y, z = position
for face in FACES:
dx, dy, dz = face
neighbor = x + dx, y + dy, z + dz
yield neighbor, face
class Sector:
"""A sector is a chunk of the world of the size SECTOR_SIZE in each directions.
It contains the block description of a sector. As it is initially generated.
"""
def __init__(self, position):
self.blocks = {}
"""Location and kind of the blocks in this sector."""
self.visible = set({})
"""Set of visible blocks if we look at this sector alone"""
self.outline = set({})
"""Blocks on the outline of the section"""
self.face_full_cache = set({})
self.position = position
"""Location of this sector."""
self.min_block = [i * SECTOR_SIZE for i in position]
"""Minimum location (included) of block in this section."""
self.max_block = [(i + 1) * SECTOR_SIZE for i in position]
"""Maximum location (excluded) of block in this section."""
def is_face_full(self, direction):
"""Check if one of the face of this section is full of blocks.
The direction is a normalized vector from `FACES`."""
return direction in self.face_full_cache
def contains(self, pos):
"""True if the position `pos` is inside this sector."""
return (self.min_block[0] <= pos[0] < self.max_block[0]
and self.min_block[1] <= pos[1] < self.max_block[1]
and self.min_block[2] <= pos[2] < self.max_block[2])
def contains_y(self, y):
"""True if the horizontal plan `y` is inside this sector."""
return self.min_block[1] <= y < self.max_block[1]
def contains_y_range(self, ymin, ymax):
"""True if the horizontal plan between `ymin` and `ymax` is inside this
sector."""
return self.min_block[1] <= ymax and ymin <= self.max_block[1]
def blocks_from_face(self, face):
"""Iterate all blocks from a face"""
axis = 0 if face[0] != 0 else (1 if face[1] != 0 else 2)
if face[axis] == -1:
pos = self.min_block[axis]
else:
pos = self.max_block[axis] - 1
for block in self.outline:
if block[axis] == pos:
yield block
def empty(self, pos):
"""Return false if there is no block at this position in this chunk"""
return pos not in self.blocks
def get_block(self, position):
"""Return the block stored at this position of this sector. Else None."""
return self.blocks[position]
def add_block(self, position, block):
"""Add a block to this chunk only if the `position` is part of this chunk."""
if not self.contains(position):
return
self.blocks[position] = block
if self.exposed(position):
self.visible.add(position)
self.check_neighbors(position)
for axis in range(3):
if position[axis] == self.min_block[axis]:
self.outline.add(position)
face = [0] * 3
face[axis] = -1
face = tuple(face)
if self.check_face_full(face):
self.face_full_cache.add(face)
elif position[axis] == self.max_block[axis] - 1:
self.outline.add(position)
face = [0] * 3
face[axis] = 1
face = tuple(face)
if self.check_face_full(face):
self.face_full_cache.add(face)
def check_face_full(self, face):
axis = (face[1] != 0) * 1 + (face[2] != 0) * 2
if face[axis] == -1:
fixed_pos = self.min_block[axis]
else:
fixed_pos = self.max_block[axis] - 1
axis2 = (axis + 1) % 3
axis3 = (axis + 2) % 3
pos = [None] * 3
pos[axis] = fixed_pos
for a2 in range(self.min_block[axis2], self.max_block[axis2]):
for a3 in range(self.min_block[axis3], self.max_block[axis3]):
pos[axis2] = a2
pos[axis3] = a3
block_pos = tuple(pos)
if block_pos not in self.blocks:
return False
return True
def remove_block(self, position):
"""Remove a block from this sector at the `position`.
Returns discarded full faces in case.
"""
del self.blocks[position]
self.check_neighbors(position)
self.visible.discard(position)
self.outline.discard(position)
discarded = set({})
# Update the full faces
for face in list(self.face_full_cache):
axis = (face[1] != 0) * 1 + (face[2] != 0) * 2
if face[axis] == -1:
border = self.min_block
else:
x, y, z = self.max_block
border = x - 1, y - 1, z - 1
if position[axis] == border[axis]:
self.face_full_cache.discard(face)
discarded.add(face)
return discarded
def exposed(self, position):
""" Returns False if given `position` is surrounded on all 6 sides by
blocks, True otherwise.
"""
for neighbor, _face in iter_neighbors(position):
if self.empty(neighbor):
return True
return False
def check_neighbors(self, position):
""" Check all blocks surrounding `position` and ensure their visual
state is current. This means hiding blocks that are not exposed and
ensuring that all exposed blocks are shown. Usually used after a block
is added or removed.
"""
for neighbor, _face in iter_neighbors(position):
if self.empty(neighbor):
continue
if self.exposed(neighbor):
if neighbor not in self.visible:
self.visible.add(neighbor)
else:
if neighbor in self.visible:
self.visible.remove(neighbor)
class Model(object):
def __init__(self, batch, group):
self.batch = batch
self.group = group
# Procedural generator
self._generator = None
# Same mapping as `world` but only contains blocks that are shown.
self.shown = {}
# Mapping from position to a pyglet `VertextList` for all shown sections.
self._shown = {}
# Mapping from sector index a list of positions inside that sector.
self.sectors = {}
# Actual set of shown sectors
self.shown_sectors = set({})
# List of sectors requested but not yet received
self.requested = set({})
# Simple function queue implementation. The queue is populated with
# _show_block() and _hide_block() calls
self.queue = deque()
def count_blocks(self):
"""Return the number of blocks in this model"""
return sum([len(s.blocks) for s in self.sectors.values()])
@property
def generator(self):
return self._generator
@generator.setter
def generator(self, generator):
assert self._generator is None
generator.set_callback(self.on_sector_received)
self._generator = generator
def on_sector_received(self, chunk):
"""Called when a part of the world is returned.
This is not executed by the main thread. So the result have to be passed
to the main thread.
"""
self._enqueue(self.register_sector, chunk)
# This sleep looks to be needed to reduce the load of the main thread.
# Maybe it also release the GIL and reduce the coupling with the main thread.
time.sleep(0.01)
def hit_test(self, position, vector, max_distance=NODE_SELECTOR):
""" Line of sight search from current position. If a block is
intersected it is returned, along with the block previously in the line
of sight. If no block is found, return None, None.
Parameters
----------
position : tuple of len 3
The (x, y, z) position to check visibility from.
vector : tuple of len 3
The line of sight vector.
max_distance : int
How many blocks away to search for a hit.
"""
m = 8
x, y, z = position
dx, dy, dz = vector
previous = None
for _ in range(max_distance * m):
checked_position = normalize((x, y, z))
if checked_position != previous and not self.empty(checked_position):
return checked_position, previous
previous = checked_position
x, y, z = x + dx / m, y + dy / m, z + dz / m
return None, None
def empty(self, position, must_be_loaded=False):
""" Returns True if given `position` does not contain block.
If `must_be_loaded` is True, this returns False if the block is not yet loaded.
"""
sector_pos = sectorize(position)
sector = self.sectors.get(sector_pos, None)
if sector is None:
return not must_be_loaded
return sector.empty(position)
def exposed(self, position):
""" Returns False if given `position` is surrounded on all 6 sides by
blocks, True otherwise.
"""
x, y, z = position
for dx, dy, dz in FACES:
pos = (x + dx, y + dy, z + dz)
if self.empty(pos, must_be_loaded=True):
return True
return False
def add_block(self, position, block, immediate=True):
""" Add a block with the given `texture` and `position` to the world.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to add.
block : Block object
An instance of the Block class.
immediate : bool
Whether or not to draw the block immediately.
"""
sector_pos = sectorize(position)
sector = self.sectors.get(sector_pos, None)
if sector is None:
# Sector not yet loaded
# It would be better to create it
# and then to merge it when the sector is loaded
return
if position in sector.blocks:
self.remove_block(position, immediate)
sector.add_block(position, block)
self._enqueue(self.update_batch_sector, sector)
def remove_block(self, position, immediate=True):
""" Remove the block at the given `position`.
Parameters
----------
position : tuple of len 3
The (x, y, z) position of the block to remove.
immediate : bool
Whether or not to immediately remove block from canvas.
"""
sector_pos = sectorize(position)
sector = self.sectors.get(sector_pos)
if sector is None:
# Nothing to do
return
if position not in sector.blocks:
# Nothing to do
return
discarded = sector.remove_block(position)
# Removing a block can make a neighbor section visible
if discarded:
x, y, z = sector.position
for dx, dy, dz in discarded:
neighbor_pos = x + dx, y + dy, z + dz
if neighbor_pos in self.sectors:
continue
if neighbor_pos in self.requested:
continue
if neighbor_pos not in self.shown_sectors:
continue
neighbor = self.generator.generate(neighbor_pos)
self.register_sector(neighbor)
self._enqueue(self.update_batch_sector, sector)
def get_block(self, position):
"""Return a block from this position.
If no blocks, None is returned.
"""
sector_pos = sectorize(position)
sector = self.sectors.get(sector_pos)
if sector is None:
return None
return sector.blocks.get(position, None)
def update_batch_sector(self, sector):
visible = sector.position in self.shown_sectors
# Clean up previous description
block = self._shown.pop(sector.position, None)
if block:
block.delete()
if visible:
points = len(sector.visible) * 24
vertex_data = []
tex_coords = []
# Merge all the blocks together
for position in sector.visible:
x, y, z = position
vertex_data.extend(cube_vertices(x, y, z, 0.5))
block = sector.get_block(position)
tex_coords.extend(block.tex_coords)
# create vertex list
# FIXME Maybe `add_indexed()` should be used instead
vertex_list = self.batch.add(points, GL_QUADS, self.group,
('v3f/static', vertex_data),
('t2f/static', tex_coords))
self._shown[sector.position] = vertex_list
def register_sector(self, sector):
"""Add a new sector to this world definition.
"""
# Assert if the sector is already there.
# It also could be skipped, or merged together.
assert sector.position not in self.sectors
self.requested.discard(sector.position)
self.sectors[sector.position] = sector
if sector.position not in self.shown_sectors:
return
# Update the displayed blocks
self._enqueue(self.update_batch_sector, sector)
# Is sector around have to be loaded too?
x, y, z = sector.position
for face in FACES:
# The sector have to be accessible
if sector.is_face_full(face):
continue
pos = x + face[0], y + face[1], z + face[2]
# Must not be already loaded
if pos in self.sectors:
continue
# Must be shown actually
if pos not in self.shown_sectors:
continue
# Must not be already requested
if pos in self.requested:
continue
# Then request the sector
if self.generator is not None:
self.requested.add(pos)
self.generator.request_sector(pos)
def show_sector(self, sector_pos):
""" Ensure all blocks in the given sector that should be shown are
drawn to the canvas.
"""
self.shown_sectors.add(sector_pos)
sector = self.sectors.get(sector_pos, None)
if sector is None:
if sector_pos in self.requested:
# Already requested
return
# If sectors around not yet loaded
if not self.is_sector_visible(sector_pos):
return
if self.generator is not None:
# This sector is about to be loaded
self.requested.add(sector_pos)
self.generator.request_sector(sector_pos)
return
self._enqueue(self.update_batch_sector, sector)
def is_sector_visible(self, sector_pos):
"""Check if a sector is visible.
For now only check if no from a sector position.
"""
x, y, z = sector_pos
for dx, dy, dz in FACES:
pos = (x + dx, y + dy, z + dz)
neighbor = self.sectors.get(pos, None)
if neighbor is not None:
neighbor_face = (-dx, -dy, -dz)
if not neighbor.is_face_full(neighbor_face):
return True
return False
def hide_sector(self, sector_pos):
""" Ensure all blocks in the given sector that should be hidden are
removed from the canvas.
"""
self.shown_sectors.discard(sector_pos)
sector = self.sectors.get(sector_pos, None)
if sector is not None:
self._enqueue(self.update_batch_sector, sector)
def show_only_sectors(self, sector_positions):
""" Update the shown sectors.
Show the ones which are not part of the list, and hide the others.
"""
after_set = set(sector_positions)
before_set = self.shown_sectors
hide = before_set - after_set
# Use a list to respect the order of the sectors
show = [s for s in sector_positions if s not in before_set]
for sector_pos in show:
self.show_sector(sector_pos)
for sector_pos in hide:
self.hide_sector(sector_pos)
def _enqueue(self, func, *args):
""" Add `func` to the internal queue.
"""
self.queue.append((func, args))
def _dequeue(self):
""" Pop the top function from the internal queue and call it.
"""
func, args = self.queue.popleft()
func(*args)
def process_queue(self):
""" Process the entire queue while taking periodic breaks. This allows
the game loop to run smoothly. The queue contains calls to
_show_block() and _hide_block() so this method should be called if
add_block() or remove_block() was called with immediate=False
"""
start = time.perf_counter()
while self.queue and time.perf_counter() - start < 1.0 / TICKS_PER_SEC:
self._dequeue()
def process_entire_queue(self):
""" Process the entire queue with no breaks.
"""
while self.queue:
self._dequeue()

21
libs/perlin-LICENSE Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2008 Casey Duncan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

383
libs/perlin.py Normal file
View File

@ -0,0 +1,383 @@
# Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com)
# see LICENSE.txt for details
"""Perlin noise -- pure python implementation"""
__version__ = '$Id: perlin.py 521 2008-12-15 03:03:52Z casey.duncan $'
from math import floor, fmod, sqrt
from random import randint
# 3D Gradient vectors
_GRAD3 = ((1, 1, 0), (-1, 1, 0), (1, -1, 0), (-1, -1, 0),
(1, 0, 1), (-1, 0, 1), (1, 0, -1), (-1, 0, -1),
(0, 1, 1), (0, -1, 1), (0, 1, -1), (0, -1, -1),
(1, 1, 0), (0, -1, 1), (-1, 1, 0), (0, -1, -1),
)
# 4D Gradient vectors
_GRAD4 = ((0, 1, 1, 1), (0, 1, 1, -1), (0, 1, -1, 1), (0, 1, -1, -1),
(0, -1, 1, 1), (0, -1, 1, -1), (0, -1, -1, 1), (0, -1, -1, -1),
(1, 0, 1, 1), (1, 0, 1, -1), (1, 0, -1, 1), (1, 0, -1, -1),
(-1, 0, 1, 1), (-1, 0, 1, -1), (-1, 0, -1, 1), (-1, 0, -1, -1),
(1, 1, 0, 1), (1, 1, 0, -1), (1, -1, 0, 1), (1, -1, 0, -1),
(-1, 1, 0, 1), (-1, 1, 0, -1), (-1, -1, 0, 1), (-1, -1, 0, -1),
(1, 1, 1, 0), (1, 1, -1, 0), (1, -1, 1, 0), (1, -1, -1, 0),
(-1, 1, 1, 0), (-1, 1, -1, 0), (-1, -1, 1, 0), (-1, -1, -1, 0))
# A lookup table to traverse the simplex around a given point in 4D.
# Details can be found where this table is used, in the 4D noise method.
_SIMPLEX = (
(0, 1, 2, 3), (0, 1, 3, 2), (0, 0, 0, 0), (0, 2, 3, 1), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (1, 2, 3, 0),
(0, 2, 1, 3), (0, 0, 0, 0), (0, 3, 1, 2), (0, 3, 2, 1), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (1, 3, 2, 0),
(0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0),
(1, 2, 0, 3), (0, 0, 0, 0), (1, 3, 0, 2), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (2, 3, 0, 1), (2, 3, 1, 0),
(1, 0, 2, 3), (1, 0, 3, 2), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (2, 0, 3, 1), (0, 0, 0, 0), (2, 1, 3, 0),
(0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0),
(2, 0, 1, 3), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (3, 0, 1, 2), (3, 0, 2, 1), (0, 0, 0, 0), (3, 1, 2, 0),
(2, 1, 0, 3), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (3, 1, 0, 2), (0, 0, 0, 0), (3, 2, 0, 1), (3, 2, 1, 0))
# Simplex skew constants
_F2 = 0.5 * (sqrt(3.0) - 1.0)
_G2 = (3.0 - sqrt(3.0)) / 6.0
_F3 = 1.0 / 3.0
_G3 = 1.0 / 6.0
class BaseNoise:
"""Noise abstract base class"""
permutation = (151, 160, 137, 91, 90, 15,
131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
129, 22, 39, 253, 9, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180)
period = len(permutation)
# Double permutation array so we don't need to wrap
permutation = permutation * 2
randint_function = randint
def __init__(self, period=None, permutation_table=None, randint_function=None):
"""Initialize the noise generator. With no arguments, the default
period and permutation table are used (256). The default permutation
table generates the exact same noise pattern each time.
An integer period can be specified, to generate a random permutation
table with period elements. The period determines the (integer)
interval that the noise repeats, which is useful for creating tiled
textures. period should be a power-of-two, though this is not
enforced. Note that the speed of the noise algorithm is indpendent of
the period size, though larger periods mean a larger table, which
consume more memory.
A permutation table consisting of an iterable sequence of whole
numbers can be specified directly. This should have a power-of-two
length. Typical permutation tables are a sequnce of unique integers in
the range [0,period) in random order, though other arrangements could
prove useful, they will not be "pure" simplex noise. The largest
element in the sequence must be no larger than period-1.
period and permutation_table may not be specified together.
A substitute for the method random.randint(a, b) can be chosen. The
method must take two integer parameters a and b and return an integer N
such that a <= N <= b.
"""
if randint_function is not None: # do this before calling randomize()
if not hasattr(randint_function, '__call__'):
raise TypeError(
'randint_function has to be a function')
self.randint_function = randint_function
if period is None:
period = self.period # enforce actually calling randomize()
if period is not None and permutation_table is not None:
raise ValueError(
'Can specify either period or permutation_table, not both')
if period is not None:
self.randomize(period)
elif permutation_table is not None:
self.permutation = tuple(permutation_table) * 2
self.period = len(permutation_table)
def randomize(self, period=None):
"""Randomize the permutation table used by the noise functions. This
makes them generate a different noise pattern for the same inputs.
"""
if period is not None:
self.period = period
perm = list(range(self.period))
perm_right = self.period - 1
for i in list(perm):
j = self.randint_function(0, perm_right)
perm[i], perm[j] = perm[j], perm[i]
self.permutation = tuple(perm) * 2
class SimplexNoise(BaseNoise):
"""Perlin simplex noise generator
Adapted from Stefan Gustavson's Java implementation described here:
http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
To summarize:
"In 2001, Ken Perlin presented 'simplex noise', a replacement for his classic
noise algorithm. Classic 'Perlin noise' won him an academy award and has
become an ubiquitous procedural primitive for computer graphics over the
years, but in hindsight it has quite a few limitations. Ken Perlin himself
designed simplex noise specifically to overcome those limitations, and he
spent a lot of good thinking on it. Therefore, it is a better idea than his
original algorithm. A few of the more prominent advantages are:
* Simplex noise has a lower computational complexity and requires fewer
multiplications.
* Simplex noise scales to higher dimensions (4D, 5D and up) with much less
computational cost, the complexity is O(N) for N dimensions instead of
the O(2^N) of classic Noise.
* Simplex noise has no noticeable directional artifacts. Simplex noise has
a well-defined and continuous gradient everywhere that can be computed
quite cheaply.
* Simplex noise is easy to implement in hardware."
"""
def noise2(self, x, y):
"""2D Perlin simplex noise.
Return a floating point value from -1 to 1 for the given x, y coordinate.
The same value is always returned for a given x, y pair unless the
permutation table changes (see randomize above).
"""
# Skew input space to determine which simplex (triangle) we are in
s = (x + y) * _F2
i = floor(x + s)
j = floor(y + s)
t = (i + j) * _G2
x0 = x - (i - t) # "Unskewed" distances from cell origin
y0 = y - (j - t)
if x0 > y0:
i1 = 1
j1 = 0 # Lower triangle, XY order: (0,0)->(1,0)->(1,1)
else:
i1 = 0
j1 = 1 # Upper triangle, YX order: (0,0)->(0,1)->(1,1)
x1 = x0 - i1 + _G2 # Offsets for middle corner in (x,y) unskewed coords
y1 = y0 - j1 + _G2
x2 = x0 + _G2 * 2.0 - 1.0 # Offsets for last corner in (x,y) unskewed coords
y2 = y0 + _G2 * 2.0 - 1.0
# Determine hashed gradient indices of the three simplex corners
perm = self.permutation
ii = int(i) % self.period
jj = int(j) % self.period
gi0 = perm[ii + perm[jj]] % 12
gi1 = perm[ii + i1 + perm[jj + j1]] % 12
gi2 = perm[ii + 1 + perm[jj + 1]] % 12
# Calculate the contribution from the three corners
tt = 0.5 - x0 ** 2 - y0 ** 2
if tt > 0:
g = _GRAD3[gi0]
noise = tt ** 4 * (g[0] * x0 + g[1] * y0)
else:
noise = 0.0
tt = 0.5 - x1 ** 2 - y1 ** 2
if tt > 0:
g = _GRAD3[gi1]
noise += tt ** 4 * (g[0] * x1 + g[1] * y1)
tt = 0.5 - x2 ** 2 - y2 ** 2
if tt > 0:
g = _GRAD3[gi2]
noise += tt ** 4 * (g[0] * x2 + g[1] * y2)
return noise * 70.0 # scale noise to [-1, 1]
def noise3(self, x, y, z):
"""3D Perlin simplex noise.
Return a floating point value from -1 to 1 for the given x, y, z coordinate.
The same value is always returned for a given x, y, z pair unless the
permutation table changes (see randomize above).
"""
# Skew the input space to determine which simplex cell we're in
s = (x + y + z) * _F3
i = floor(x + s)
j = floor(y + s)
k = floor(z + s)
t = (i + j + k) * _G3
x0 = x - (i - t) # "Unskewed" distances from cell origin
y0 = y - (j - t)
z0 = z - (k - t)
# For the 3D case, the simplex shape is a slightly irregular tetrahedron.
# Determine which simplex we are in.
if x0 >= y0:
if y0 >= z0:
i1 = 1
j1 = 0
k1 = 0
i2 = 1
j2 = 1
k2 = 0
elif x0 >= z0:
i1 = 1
j1 = 0
k1 = 0
i2 = 1
j2 = 0
k2 = 1
else:
i1 = 0
j1 = 0
k1 = 1
i2 = 1
j2 = 0
k2 = 1
else: # x0 < y0
if y0 < z0:
i1 = 0
j1 = 0
k1 = 1
i2 = 0
j2 = 1
k2 = 1
elif x0 < z0:
i1 = 0
j1 = 1
k1 = 0
i2 = 0
j2 = 1
k2 = 1
else:
i1 = 0
j1 = 1
k1 = 0
i2 = 1
j2 = 1
k2 = 0
# Offsets for remaining corners
x1 = x0 - i1 + _G3
y1 = y0 - j1 + _G3
z1 = z0 - k1 + _G3
x2 = x0 - i2 + 2.0 * _G3
y2 = y0 - j2 + 2.0 * _G3
z2 = z0 - k2 + 2.0 * _G3
x3 = x0 - 1.0 + 3.0 * _G3
y3 = y0 - 1.0 + 3.0 * _G3
z3 = z0 - 1.0 + 3.0 * _G3
# Calculate the hashed gradient indices of the four simplex corners
perm = self.permutation
ii = int(i) % self.period
jj = int(j) % self.period
kk = int(k) % self.period
gi0 = perm[ii + perm[jj + perm[kk]]] % 12
gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12
gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12
gi3 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12
# Calculate the contribution from the four corners
noise = 0.0
tt = 0.6 - x0 ** 2 - y0 ** 2 - z0 ** 2
if tt > 0:
g = _GRAD3[gi0]
noise = tt ** 4 * (g[0] * x0 + g[1] * y0 + g[2] * z0)
else:
noise = 0.0
tt = 0.6 - x1 ** 2 - y1 ** 2 - z1 ** 2
if tt > 0:
g = _GRAD3[gi1]
noise += tt ** 4 * (g[0] * x1 + g[1] * y1 + g[2] * z1)
tt = 0.6 - x2 ** 2 - y2 ** 2 - z2 ** 2
if tt > 0:
g = _GRAD3[gi2]
noise += tt ** 4 * (g[0] * x2 + g[1] * y2 + g[2] * z2)
tt = 0.6 - x3 ** 2 - y3 ** 2 - z3 ** 2
if tt > 0:
g = _GRAD3[gi3]
noise += tt ** 4 * (g[0] * x3 + g[1] * y3 + g[2] * z3)
return noise * 32.0
def lerp(t, a, b):
return a + t * (b - a)
def grad3(hash, x, y, z):
g = _GRAD3[hash % 16]
return x * g[0] + y * g[1] + z * g[2]
class TileableNoise(BaseNoise):
"""Tileable implemention of Perlin "improved" noise. This
is based on the reference implementation published here:
http://mrl.nyu.edu/~perlin/noise/
"""
def noise3(self, x, y, z, repeat, base=0.0):
"""Tileable 3D noise.
repeat specifies the integer interval in each dimension
when the noise pattern repeats.
base allows a different texture to be generated for
the same repeat interval.
"""
i = int(fmod(floor(x), repeat))
j = int(fmod(floor(y), repeat))
k = int(fmod(floor(z), repeat))
ii = (i + 1) % repeat
jj = (j + 1) % repeat
kk = (k + 1) % repeat
if base:
i += base
j += base
k += base
ii += base
jj += base
kk += base
x -= floor(x)
y -= floor(y)
z -= floor(z)
fx = x ** 3 * (x * (x * 6 - 15) + 10)
fy = y ** 3 * (y * (y * 6 - 15) + 10)
fz = z ** 3 * (z * (z * 6 - 15) + 10)
perm = self.permutation
A = perm[i]
AA = perm[A + j]
AB = perm[A + jj]
B = perm[ii]
BA = perm[B + j]
BB = perm[B + jj]
return lerp(fz, lerp(fy, lerp(fx, grad3(perm[AA + k], x, y, z),
grad3(perm[BA + k], x - 1, y, z)),
lerp(fx, grad3(perm[AB + k], x, y - 1, z),
grad3(perm[BB + k], x - 1, y - 1, z))),
lerp(fy, lerp(fx, grad3(perm[AA + kk], x, y, z - 1),
grad3(perm[BA + kk], x - 1, y, z - 1)),
lerp(fx, grad3(perm[AB + kk], x, y - 1, z - 1),
grad3(perm[BB + kk], x - 1, y - 1, z - 1))))

View File

@ -33,6 +33,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import pyglet
import os.path
from game.graphics import *
from game.scenemanager import SceneManager
@ -40,7 +41,9 @@ from game.scenemanager import SceneManager
def main():
# The pyglet.resource module handles efficient loading of assets:
pyglet.resource.path = ['assets', 'assets/images', 'assets/sounds']
path = ['assets', 'assets/images', 'assets/sounds']
path = [os.path.abspath(p) for p in path]
pyglet.resource.path = path
pyglet.resource.reindex()
# Create the main game Window, and set it's icon: