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()