diff --git a/assets/images/textures.png b/assets/images/textures.png
index a5d4c91..bcfd964 100644
Binary files a/assets/images/textures.png and b/assets/images/textures.png differ
diff --git a/game/blocks.py b/game/blocks.py
index 4689924..20cc3c7 100755
--- a/game/blocks.py
+++ b/game/blocks.py
@@ -32,13 +32,13 @@ along with this program. If not, see .
"""
-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,20 +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), (1, 3), (1, 3)))
-WOODEN_PLANKS = Block('wooden_planks', _tex_coords((2, 3), (2, 3), (2, 3)))
-CLOUD = Block('snow', _tex_coords((1, 3), (1, 3), (1, 3)))
-DIRT_WITH_SNOW = Block('dirt_with_snow', _tex_coords((1, 3), (0, 1), (0, 3)))
-WATER = Block('dirt', _tex_coords((3, 2), (3, 2), (3, 2)))
+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)]
diff --git a/game/config.py b/game/config.py
index 69ab7b2..c8fc5b7 100755
--- a/game/config.py
+++ b/game/config.py
@@ -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
diff --git a/game/genworld.py b/game/genworld.py
index c3aab80..25d1f92 100644
--- a/game/genworld.py
+++ b/game/genworld.py
@@ -37,38 +37,14 @@ import random
from .blocks import *
from .utilities import *
from game import utilities
-from libs import perlin
-
-
-noise = perlin.SimplexNoise()
-
-
-class Chunk:
- """A chunk of the world, with some helpers"""
-
- def __init__(self, sector):
- self.blocks = {}
-
- self.sector = sector
- assert sector[1] == 0
- """For now a chunk is infinite vertically"""
-
- def empty(self, pos):
- return pos not in self.blocks
-
- def __setitem__(self, pos, value):
- self.blocks[pos] = value
-
- def __getitem__(self, pos):
- return self.blocks[pos]
+from .noise import Noise
+from .world import Sector
class WorldGenerator:
"""Generate a world model"""
- def __init__(self, model):
- self.model = model
-
+ 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."""
@@ -80,16 +56,22 @@ class WorldGenerator:
"""If True the generator uses a procedural generation for the map.
Else, a flat floor will be generated."""
- self.y = 0
+ 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.nb_trees = 3
+ 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."""
@@ -100,6 +82,20 @@ class WorldGenerator:
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):
@@ -160,20 +156,31 @@ class WorldGenerator:
future = self.executor.submit(self.generate, sector)
future.add_done_callback(send_result)
- def _iter_xz(self, sector):
- """Iter all the xz block position from a sector"""
- sx, _sy, sz = sector
- for x in range(sx * SECTOR_SIZE, (sx + 1) * SECTOR_SIZE):
- for z in range(sz * SECTOR_SIZE, (sz + 1) * SECTOR_SIZE):
+ 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 = Chunk(sector)
+ chunk = Sector(sector)
"""Store the content of this sector"""
- self._generate_enclosure(chunk)
+ if self.enclosure:
+ self._generate_enclosure(chunk)
if self.hills_enabled:
self._generate_random_map(chunk)
else:
@@ -182,6 +189,8 @@ class WorldGenerator:
self._generate_clouds(chunk)
if self.nb_trees > 0:
self._generate_trees(chunk)
+ if not self.enclosure:
+ self._generate_underworld(chunk)
return chunk
@@ -191,47 +200,61 @@ class WorldGenerator:
"""
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
+
+ y_pos = self.y - 2
half_size = self.enclosure_size
n = half_size
- for x, z in self._iter_xz(chunk.sector):
+ 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.
- chunk[(x, y_pos, z)] = BEDSTONE
+ pos = (x, y_pos, z)
+ chunk.add_block(pos, BEDSTONE)
- if self.enclosure:
- # 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):
- chunk[(x, y_pos + dy, z)] = 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.sector):
+ for x, z in self._iter_xz(chunk):
if self.enclosure:
- if x <= -n or x >= n - 1 or z <= -n or z >= n - 1:
+ if x <= -n or x >= n or z <= -n or z >= n:
continue
- chunk[(x, y_pos, z)] = DIRT_WITH_GRASS
+ 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
- freq = 38
- for x, z in self._iter_xz(chunk.sector):
+ 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 - 1 or z <= -n or z >= n - 1:
+ if x <= -n or x >= n or z <= -n or z >= n:
continue
- c = noise.noise2(x / freq, z / freq)
- c = int((c + 1) * 0.5 * len(self.lookup_terrain))
- if c < 0:
- c = 0
- nb_block, terrains = self.lookup_terrain[c]
+ 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[(x, y_pos + nb_block - i, z)] = block
+ chunk.add_block((x, y_pos + nb_block - i, z), block)
def _generate_trees(self, chunk):
"""Generate trees in the map
@@ -239,35 +262,41 @@ class WorldGenerator:
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(chunk, x, y, z):
+ def get_biome(x, y, z):
"""Return the biome at a location of the map plus the first empty place."""
- # This loop could be removed using procedural height map
- while not chunk.empty((x, y, z)):
- y = y + 1
- block = chunk[x, y - 1, z]
+ nb_block, terrains = self._get_biome(x, z)
+ y = self.y - 2 + nb_block
+ block = terrains[-1]
return block, y
- sector = chunk.sector
- random.seed(sector[0] + sector[2])
+ 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[0] * utilities.SECTOR_SIZE + 3 + random.randint(0, utilities.SECTOR_SIZE - 7)
- z = sector[2] * utilities.SECTOR_SIZE + 3 + random.randint(0, utilities.SECTOR_SIZE - 7)
+ 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(chunk, x, y_pos + 1, z)
+ 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 > 6:
+ elif start_pos - self.y > 6:
height = random.randint(3, 5)
self._create_fir_tree(chunk, x, start_pos, z, height)
else:
@@ -275,16 +304,16 @@ class WorldGenerator:
self._create_default_tree(chunk, x, start_pos, z, height)
def _create_plus(self, chunk, x, y, z, block):
- chunk[(x, y, z)] = block
- chunk[(x - 1, y, z)] = block
- chunk[(x + 1, y, z)] = block
- chunk[(x, y, z - 1)] = block
- chunk[(x, y, z + 1)] = 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[(x + dx, y, z + dz)] = block
+ chunk.add_block((x + dx, y, z + dz), block)
def _create_default_tree(self, chunk, x, y, z, height):
if height == 0:
@@ -293,13 +322,13 @@ class WorldGenerator:
self._create_plus(x, y, z, LEAVES)
return
if height == 2:
- chunk[(x, y, z)] = TREE
- chunk[(x, y + 1, z)] = LEAVES
+ 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[(x, y + y_tree, z)] = TREE
+ 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
@@ -315,52 +344,82 @@ class WorldGenerator:
self._create_plus(chunk, x, y, z, LEAVES)
return
if height == 2:
- chunk[(x, y, z)] = TREE
- chunk[(x, y + 1, z)] = LEAVES
+ chunk.add_block((x, y, z), TREE)
+ chunk.add_block((x, y + 1, z), LEAVES)
return
y_tree = 0
- chunk[(x, y + y_tree, z)] = TREE
+ chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
self._create_box(chunk, x, y + y_tree, z, LEAVES)
- chunk[(x, y + y_tree, z)] = TREE
+ 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[(x, y + y_tree, z)] = TREE
+ chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
for _ in range(h_layer):
- chunk[(x, y + y_tree, z)] = LEAVES
+ 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[(x, y + y_tree, z)] = TREE
+ chunk.add_block((x, y + y_tree, z), TREE)
y_tree += 1
- chunk[(x + 1, y + y_tree, z)] = LEAVES
- chunk[(x - 1, y + y_tree, z)] = LEAVES
- chunk[(x, y + y_tree, z + 1)] = LEAVES
- chunk[(x, y + y_tree, z - 1)] = LEAVES
+ 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[(x + 2, y + y_tree, z)] = LEAVES
- chunk[(x - 2, y + y_tree, z)] = LEAVES
- chunk[(x, y + y_tree, z + 2)] = LEAVES
- chunk[(x, y + y_tree, z - 2)] = LEAVES
+ 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[(x + 3, y + y_tree, z)] = LEAVES
- chunk[(x - 3, y + y_tree, z)] = LEAVES
- chunk[(x, y + y_tree, z + 3)] = LEAVES
- chunk[(x, y + y_tree, z - 3)] = LEAVES
+ 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 `height` and covering this `half_size`
- centered to 0.
+ """Generate clouds at this `self.y_cloud`.
"""
- y_pos = self.y + 20
- freq = 20
- for x, z in self._iter_xz(chunk.sector):
- c = noise.noise2(x / freq, z / freq)
+ 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[(x, y_pos, z)] = CLOUD
+ 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)
diff --git a/game/noise.py b/game/noise.py
new file mode 100644
index 0000000..cbc886d
--- /dev/null
+++ b/game/noise.py
@@ -0,0 +1,101 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+"""
+ ________ ______ ______ __
+| \ / \ / \ | \
+ \$$$$$$$$______ ______ ______ ______ | $$$$$$\ ______ ______ | $$$$$$\ _| $$_
+ | $$ / \ / \ / \ | \ | $$ \$$ / \ | \ | $$_ \$$| $$ \
+ | $$ | $$$$$$\| $$$$$$\| $$$$$$\ \$$$$$$\| $$ | $$$$$$\ \$$$$$$\| $$ \ \$$$$$$
+ | $$ | $$ $$| $$ \$$| $$ \$$/ $$| $$ __ | $$ \$$/ $$| $$$$ | $$ __
+ | $$ | $$$$$$$$| $$ | $$ | $$$$$$$| $$__/ \| $$ | $$$$$$$| $$ | $$| \
+ | $$ \$$ \| $$ | $$ \$$ $$ \$$ $$| $$ \$$ $$| $$ \$$ $$
+ \$$ \$$$$$$$ \$$ \$$ \$$$$$$$ \$$$$$$ \$$ \$$$$$$$ \$$ \$$$$
+
+
+Copyright (C) 2013 Michael Fogleman
+Copyright (C) 2018/2019 Stefano Peris
+
+Github repository:
+
+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 .
+"""
+
+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
diff --git a/game/scenes.py b/game/scenes.py
index f677861..19f2751 100755
--- a/game/scenes.py
+++ b/game/scenes.py
@@ -31,9 +31,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import random
import time
-import pyglet
from collections import deque
@@ -47,6 +45,7 @@ from .blocks import *
from .utilities import *
from .graphics import BlockGroup
from .genworld import WorldGenerator
+from .world import Model
class AudioEngine:
@@ -226,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 = (SECTOR_SIZE // 2, 0, SECTOR_SIZE // 2)
+ 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
@@ -239,9 +238,6 @@ class GameScene(Scene):
# Which sector the player is currently in.
self.sector = None
- self.received_sectors = []
- # Channel for data received from the the world generator
-
# True if the location of the camera have changed between an update
self.frustum_updated = False
@@ -352,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.
@@ -362,10 +383,6 @@ class GameScene(Scene):
The change in time since the last call.
"""
- if self.received_sectors:
- chunk = self.received_sectors.pop(0)
- self.model.feed_chunk(chunk)
-
if not self.initialized:
self.set_exclusive_mouse(True)
@@ -375,24 +392,11 @@ class GameScene(Scene):
has_save = self.scene_manager.save.load_world(self.model)
if not has_save:
- generator = WorldGenerator(self.model)
- generator.set_callback(self.on_sector_received)
+ generator = WorldGenerator()
+ generator.y = self.position[1]
generator.hills_enabled = HILLS_ON
self.model.generator = generator
-
- # Make sure the sector containing the actor is loaded
- sector = sectorize(self.position)
- chunk = generator.generate(sector)
- self.model.feed_chunk(chunk)
-
- # Move the actor above the terrain
- while not self.model.empty(self.position):
- x, y, z = self.position
- position = x, y + 1, z
- if self.position != position:
- self.position = position
- self.frustum_updated = True
-
+ self.init_player_on_summit()
self.initialized = True
@@ -409,17 +413,6 @@ class GameScene(Scene):
for _ in range(m):
self._update(dt / m)
- 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.received_sectors.append(chunk)
- # Reduce the load of the main thread by delaying the
- # computation between 2 chunks
- time.sleep(0.1)
-
def _update(self, dt):
""" Private implementation of the `update()` method. This is where most
of the motion logic lives, along with gravity and collision detection.
@@ -447,8 +440,7 @@ 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)
+
position = (x, y, z)
if self.position != position:
self.position = position
@@ -490,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):
@@ -498,6 +490,25 @@ 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):
@@ -514,13 +525,13 @@ class GameScene(Scene):
return
sectors_to_show = []
- pad = 4
+ pad = int(FOG_END) // SECTOR_SIZE
for dx in range(-pad, pad + 1):
- for dy in [0]: # range(-pad, pad + 1):
+ for dy in range(-pad, pad + 1):
for dz in range(-pad, pad + 1):
# Manathan distance
- dist = abs(dx) + abs(dz)
- if dist > pad + 2:
+ 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
@@ -559,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)
@@ -677,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.
@@ -707,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)
@@ -726,298 +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 = {}
-
- # 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 blocks.
- self._shown = {}
-
- # Mapping from sector to a list of positions inside that sector.
- self.sectors = {}
-
- # Actual set of shown sectors
- self.shown_sectors = set({})
-
- #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 empty(self, position):
- """ Returns True if given `position` does not contain block.
- """
- return not position in self.world
-
- 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.
-
- """
- block = self._shown.pop(position, None)
- if block:
- block.delete()
-
- def feed_chunk(self, chunk):
- """Add a chunk of the world to the model.
- """
- shown = chunk.sector in self.shown_sectors
- for position, block in chunk.blocks.items():
- self.add_block(position, block, immediate=False)
- if shown:
- self.show_block(position, immediate=False)
-
- def show_sector(self, sector):
- """ Ensure all blocks in the given sector that should be shown are
- drawn to the canvas.
-
- """
- self.shown_sectors.add(sector)
-
- if sector not in self.sectors:
- if self.generator is not None:
- # This sector is about to be loaded
- self.sectors[sector] = []
- self.generator.request_sector(sector)
- return
-
- 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.
-
- """
- self.shown_sectors.discard(sector)
-
- for position in self.sectors.get(sector, []):
- if position in self.shown:
- self.hide_block(position, False)
-
- def show_only_sectors(self, sectors):
- """ Update the shown sectors.
-
- Show the ones which are not part of the list, and hide the others.
- """
- after_set = set(sectors)
- 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 sectors if s not in before_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.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()
-
-
class HelpScene(Scene):
def __init__(self, window):
self.window = window
diff --git a/game/utilities.py b/game/utilities.py
index 99746b9..120fd50 100755
--- a/game/utilities.py
+++ b/game/utilities.py
@@ -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
diff --git a/game/world.py b/game/world.py
new file mode 100644
index 0000000..ec2f7d8
--- /dev/null
+++ b/game/world.py
@@ -0,0 +1,548 @@
+#!/bin/python3
+
+"""
+ ________ ______ ______ __
+| \ / \ / \ | \
+ \$$$$$$$$______ ______ ______ ______ | $$$$$$\ ______ ______ | $$$$$$\ _| $$_
+ | $$ / \ / \ / \ | \ | $$ \$$ / \ | \ | $$_ \$$| $$ \
+ | $$ | $$$$$$\| $$$$$$\| $$$$$$\ \$$$$$$\| $$ | $$$$$$\ \$$$$$$\| $$ \ \$$$$$$
+ | $$ | $$ $$| $$ \$$| $$ \$$/ $$| $$ __ | $$ \$$/ $$| $$$$ | $$ __
+ | $$ | $$$$$$$$| $$ | $$ | $$$$$$$| $$__/ \| $$ | $$$$$$$| $$ | $$| \
+ | $$ \$$ \| $$ | $$ \$$ $$ \$$ $$| $$ \$$ $$| $$ \$$ $$
+ \$$ \$$$$$$$ \$$ \$$ \$$$$$$$ \$$$$$$ \$$ \$$$$$$$ \$$ \$$$$
+
+
+Copyright (C) 2013 Michael Fogleman
+Copyright (C) 2018/2019 Stefano Peris
+
+Github repository:
+
+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 .
+"""
+
+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()