From b86038301bcf743e230989dea732fe190220741b Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Mon, 25 May 2020 01:53:04 +0200 Subject: [PATCH] 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 --- assets/images/textures.png | Bin 19229 -> 19656 bytes game/blocks.py | 37 +-- game/config.py | 2 +- game/genworld.py | 263 +++++++++++------- game/noise.py | 101 +++++++ game/scenes.py | 406 +++++---------------------- game/utilities.py | 4 +- game/world.py | 548 +++++++++++++++++++++++++++++++++++++ 8 files changed, 904 insertions(+), 457 deletions(-) create mode 100644 game/noise.py create mode 100644 game/world.py diff --git a/assets/images/textures.png b/assets/images/textures.png index a5d4c913f39dfe4fc2bdfe54772a06629c8ff37f..bcfd9646f579da5104adae7be7c5860365096efb 100644 GIT binary patch literal 19656 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 literal 19229 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 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()