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 constancesmaster
parent
186f3718eb
commit
b86038301b
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
263
game/genworld.py
263
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)
|
||||
|
|
|
@ -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
|
406
game/scenes.py
406
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 <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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue