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
master
Valentin Valls 2020-05-25 01:53:04 +02:00 committed by GitHub
parent 186f3718eb
commit b86038301b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 904 additions and 457 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -32,13 +32,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
def _tex_coord(x, y, n=4):
def _tex_coord(x, y, n=8):
""" Return the bounding vertices of the texture square.
"""
m = 1.0 / n
dx = x * m
dy = y * m
dy = 1 - (y + 1) * m
return dx, dy, dx + m, dy, dx + m, dy + m, dx, dy + m
@ -69,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)]

View File

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

View File

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

101
game/noise.py Normal file
View File

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

View File

@ -31,9 +31,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import random
import time
import pyglet
from collections import deque
@ -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

View File

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

548
game/world.py Normal file
View File

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