Compare commits
5 Commits
d397a80ccd
...
b86038301b
Author | SHA1 | Date |
---|---|---|
Valentin Valls | b86038301b | |
Benjamin Moran | 186f3718eb | |
Valentin Valls | 4cbe89aa12 | |
Benjamin | dcb201e8a0 | |
Valentin Valls | 168777fd08 |
Binary file not shown.
Before Width: | Height: | Size: 7.3 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,17 +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), (0, 1), (0, 3)))
|
||||
WOODEN_PLANKS = Block('wooden_planks', _tex_coords((2, 3), (2, 3), (2, 3)))
|
||||
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
|
||||
|
|
419
game/genworld.py
419
game/genworld.py
|
@ -31,50 +31,395 @@ 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 math
|
||||
import concurrent.futures
|
||||
import random
|
||||
|
||||
from .blocks import *
|
||||
from .utilities import *
|
||||
from .graphics import BlockGroup
|
||||
from game import utilities
|
||||
from .noise import Noise
|
||||
from .world import Sector
|
||||
|
||||
def generate_world(self):
|
||||
"""Randomly generate a new world and place all the blocks"""
|
||||
n = 80 # 1/2 width and height of world
|
||||
s = 1 # step size
|
||||
y = 0 # initial y height
|
||||
|
||||
for x in range(-n, n + 1, s):
|
||||
for z in range(-n, n + 1, s):
|
||||
# create a layer stone an DIRT_WITH_GRASS everywhere.
|
||||
self.add_block((x, y - 2, z), DIRT_WITH_GRASS, immediate=True)
|
||||
self.add_block((x, y - 3, z), BEDSTONE, immediate=False)
|
||||
if x in (-n, n) or z in (-n, n):
|
||||
# create outer walls.
|
||||
# Setting values for the Bedrock (depth, and height of the perimeter wall).
|
||||
for dy in range(-2, 9):
|
||||
self.add_block((x, y + dy, z), BEDSTONE, immediate=False)
|
||||
class WorldGenerator:
|
||||
"""Generate a world model"""
|
||||
|
||||
# generate the hills randomly
|
||||
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."""
|
||||
|
||||
if not HILLS_ON:
|
||||
self.callback = None
|
||||
"""Callback for the result of the executor"""
|
||||
|
||||
self.hills_enabled = True
|
||||
"""If True the generator uses a procedural generation for the map.
|
||||
Else, a flat floor will be generated."""
|
||||
|
||||
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.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."""
|
||||
|
||||
self.enclosure_size = 80
|
||||
"""1/2 width (in x and z) of the enclosure"""
|
||||
|
||||
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):
|
||||
"""Add a new entry to the height map lookup table.
|
||||
|
||||
`height` will be the height at this part of the height map.
|
||||
and `terrains` contains blocks for each vertical voxels. The last
|
||||
one is on top, and the first one is used for all the remaining voxels
|
||||
on bottom.
|
||||
"""
|
||||
self.lookup_terrain.append((height, terrains))
|
||||
|
||||
add_terrain_map(1, [WATER])
|
||||
add_terrain_map(1, [WATER])
|
||||
add_terrain_map(1, [WATER])
|
||||
add_terrain_map(1, [WATER])
|
||||
add_terrain_map(1, [WATER])
|
||||
add_terrain_map(1, [WATER])
|
||||
add_terrain_map(1, [SAND])
|
||||
add_terrain_map(1, [SAND])
|
||||
add_terrain_map(2, [SAND])
|
||||
add_terrain_map(1, [SAND])
|
||||
add_terrain_map(1, [SAND])
|
||||
add_terrain_map(1, [DIRT_WITH_GRASS])
|
||||
add_terrain_map(1, [DIRT_WITH_GRASS])
|
||||
add_terrain_map(2, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(2, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(3, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(4, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(4, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(5, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(5, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(6, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(6, [DIRT, DIRT_WITH_GRASS])
|
||||
add_terrain_map(7, [DIRT])
|
||||
add_terrain_map(8, [DIRT])
|
||||
add_terrain_map(9, [DIRT])
|
||||
add_terrain_map(10, [DIRT, DIRT_WITH_SNOW])
|
||||
add_terrain_map(11, [DIRT, DIRT_WITH_SNOW, SNOW])
|
||||
add_terrain_map(12, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
|
||||
add_terrain_map(13, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
|
||||
add_terrain_map(14, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
|
||||
add_terrain_map(15, [DIRT, DIRT_WITH_SNOW, SNOW, SNOW])
|
||||
|
||||
def set_callback(self, callback):
|
||||
"""Set a callback called when a new sector is computed"""
|
||||
self.callback = callback
|
||||
|
||||
def request_sector(self, sector):
|
||||
"""Compute the content of a sector asynchronously and return the result to a
|
||||
callback already specified to this generator.
|
||||
"""
|
||||
|
||||
def send_result(future):
|
||||
chunk = future.result()
|
||||
self.callback(chunk)
|
||||
|
||||
future = self.executor.submit(self.generate, sector)
|
||||
future.add_done_callback(send_result)
|
||||
|
||||
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 = Sector(sector)
|
||||
"""Store the content of this sector"""
|
||||
|
||||
if self.enclosure:
|
||||
self._generate_enclosure(chunk)
|
||||
if self.hills_enabled:
|
||||
self._generate_random_map(chunk)
|
||||
else:
|
||||
self._generate_floor(chunk)
|
||||
if self.cloudiness > 0:
|
||||
self._generate_clouds(chunk)
|
||||
if self.nb_trees > 0:
|
||||
self._generate_trees(chunk)
|
||||
if not self.enclosure:
|
||||
self._generate_underworld(chunk)
|
||||
|
||||
return chunk
|
||||
|
||||
def _generate_enclosure(self, chunk):
|
||||
"""Generate an enclosure with unbreakable blocks on the floor and
|
||||
and on the side.
|
||||
"""
|
||||
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
|
||||
|
||||
o = n - 10
|
||||
for _ in range(120):
|
||||
a = random.randint(-o, o) # x position of the hill
|
||||
b = random.randint(-o, o) # z position of the hill
|
||||
c = -1 # base of the hill
|
||||
h = random.randint(1, 6) # height of the hill
|
||||
s = random.randint(4, 8) # 2 * s is the side length of the hill
|
||||
d = 1 # how quickly to taper off the hills
|
||||
block = random.choice([DIRT_WITH_GRASS, SNOW, SAND])
|
||||
for y in range(c, c + h):
|
||||
for x in range(a - s, a + s + 1):
|
||||
for z in range(b - s, b + s + 1):
|
||||
if (x - a) ** 2 + (z - b) ** 2 > (s + 1) ** 2:
|
||||
continue
|
||||
if (x - 0) ** 2 + (z - 0) ** 2 < 5 ** 2: # 6 = flat map
|
||||
continue
|
||||
self.add_block((x, y, z), block, immediate=False)
|
||||
s -= d # decrement side length so hills taper off
|
||||
y_pos = self.y - 2
|
||||
half_size = self.enclosure_size
|
||||
n = half_size
|
||||
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.
|
||||
pos = (x, y_pos, z)
|
||||
chunk.add_block(pos, 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):
|
||||
if self.enclosure:
|
||||
if x <= -n or x >= n or z <= -n or z >= n:
|
||||
continue
|
||||
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
|
||||
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 or z <= -n or z >= n:
|
||||
continue
|
||||
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.add_block((x, y_pos + nb_block - i, z), block)
|
||||
|
||||
def _generate_trees(self, chunk):
|
||||
"""Generate trees in the map
|
||||
|
||||
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(x, y, z):
|
||||
"""Return the biome at a location of the map plus the first empty place."""
|
||||
nb_block, terrains = self._get_biome(x, z)
|
||||
y = self.y - 2 + nb_block
|
||||
block = terrains[-1]
|
||||
return block, y
|
||||
|
||||
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_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(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 - self.y > 6:
|
||||
height = random.randint(3, 5)
|
||||
self._create_fir_tree(chunk, x, start_pos, z, height)
|
||||
else:
|
||||
height = random.randint(3, 7 - (start_pos - y_pos) // 3)
|
||||
self._create_default_tree(chunk, x, start_pos, z, height)
|
||||
|
||||
def _create_plus(self, chunk, x, y, z, 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.add_block((x + dx, y, z + dz), block)
|
||||
|
||||
def _create_default_tree(self, chunk, x, y, z, height):
|
||||
if height == 0:
|
||||
return
|
||||
if height == 1:
|
||||
self._create_plus(x, y, z, LEAVES)
|
||||
return
|
||||
if height == 2:
|
||||
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.add_block((x, y + y_tree, z), TREE)
|
||||
y_tree += 1
|
||||
self._create_plus(chunk, x, y + y_tree, z, LEAVES)
|
||||
y_tree += 1
|
||||
for _ in range(height - 4):
|
||||
self._create_box(chunk, x, y + y_tree, z, LEAVES)
|
||||
y_tree += 1
|
||||
self._create_plus(chunk, x, y + y_tree, z, LEAVES)
|
||||
|
||||
def _create_fir_tree(self, chunk, x, y, z, height):
|
||||
if height == 0:
|
||||
return
|
||||
if height == 1:
|
||||
self._create_plus(chunk, x, y, z, LEAVES)
|
||||
return
|
||||
if height == 2:
|
||||
chunk.add_block((x, y, z), TREE)
|
||||
chunk.add_block((x, y + 1, z), LEAVES)
|
||||
return
|
||||
y_tree = 0
|
||||
chunk.add_block((x, y + y_tree, z), TREE)
|
||||
y_tree += 1
|
||||
self._create_box(chunk, x, y + y_tree, z, LEAVES)
|
||||
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.add_block((x, y + y_tree, z), TREE)
|
||||
y_tree += 1
|
||||
for _ in range(h_layer):
|
||||
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.add_block((x, y + y_tree, z), TREE)
|
||||
y_tree += 1
|
||||
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.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.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 `self.y_cloud`.
|
||||
"""
|
||||
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.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
|
405
game/scenes.py
405
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
|
||||
|
||||
|
@ -46,7 +44,9 @@ from pyglet.graphics import OrderedGroup
|
|||
from .blocks import *
|
||||
from .utilities import *
|
||||
from .graphics import BlockGroup
|
||||
from .genworld import *
|
||||
from .genworld import WorldGenerator
|
||||
from .world import Model
|
||||
|
||||
|
||||
class AudioEngine:
|
||||
"""A high level audio engine for easily playing SFX and Music."""
|
||||
|
@ -225,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 = (0, 0, 0)
|
||||
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
|
||||
|
@ -238,6 +238,9 @@ class GameScene(Scene):
|
|||
# Which sector the player is currently in.
|
||||
self.sector = None
|
||||
|
||||
# True if the location of the camera have changed between an update
|
||||
self.frustum_updated = False
|
||||
|
||||
# Velocity in the y (upward) direction.
|
||||
self.dy = 0
|
||||
|
||||
|
@ -345,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.
|
||||
|
@ -364,17 +392,22 @@ class GameScene(Scene):
|
|||
has_save = self.scene_manager.save.load_world(self.model)
|
||||
|
||||
if not has_save:
|
||||
generate_world(self.model)
|
||||
generator = WorldGenerator()
|
||||
generator.y = self.position[1]
|
||||
generator.hills_enabled = HILLS_ON
|
||||
self.model.generator = generator
|
||||
self.init_player_on_summit()
|
||||
|
||||
self.initialized = True
|
||||
|
||||
self.model.process_queue()
|
||||
sector = sectorize(self.position)
|
||||
if sector != self.sector:
|
||||
self.model.change_sectors(self.sector, sector)
|
||||
# if self.sector is None:
|
||||
# self.model.process_entire_queue()
|
||||
|
||||
if self.frustum_updated:
|
||||
sector = sectorize(self.position)
|
||||
self.update_shown_sectors(self.position, self.rotation)
|
||||
self.sector = sector
|
||||
self.frustum_updated = False
|
||||
|
||||
m = 8
|
||||
dt = min(dt, 0.2)
|
||||
for _ in range(m):
|
||||
|
@ -407,9 +440,11 @@ 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)
|
||||
self.position = (x, y, z)
|
||||
|
||||
position = (x, y, z)
|
||||
if self.position != position:
|
||||
self.position = position
|
||||
self.frustum_updated = True
|
||||
|
||||
def collide(self, position, height):
|
||||
""" Checks to see if the player at the given `position` and `height`
|
||||
|
@ -447,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):
|
||||
|
@ -455,8 +490,59 @@ 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):
|
||||
"""Update shown sectors according to the actual frustum.
|
||||
|
||||
A sector is a contiguous x, y sub-region of world. Sectors are
|
||||
used to speed up world rendering.
|
||||
"""
|
||||
sector = sectorize(position)
|
||||
if self.sector == sector:
|
||||
# The following computation is based on the actual sector
|
||||
# So if there is no changes on the sector, it have to display
|
||||
# The exact same thing
|
||||
return
|
||||
|
||||
sectors_to_show = []
|
||||
pad = int(FOG_END) // SECTOR_SIZE
|
||||
for dx in range(-pad, pad + 1):
|
||||
for dy in range(-pad, pad + 1):
|
||||
for dz in range(-pad, pad + 1):
|
||||
# Manathan distance
|
||||
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
|
||||
sectors_to_show.append((dist, x + dx, y + dy, z + dz))
|
||||
|
||||
# Sort by distance to the player in order to
|
||||
# displayed closest sectors first
|
||||
sectors_to_show = sorted(sectors_to_show)
|
||||
sectors_to_show = [s[1:] for s in sectors_to_show]
|
||||
self.model.show_only_sectors(sectors_to_show)
|
||||
|
||||
def on_mouse_press(self, x, y, button, modifiers):
|
||||
"""Event handler for the Window.on_mouse_press event.
|
||||
|
||||
|
@ -484,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)
|
||||
|
@ -509,7 +595,10 @@ class GameScene(Scene):
|
|||
x, y = self.rotation
|
||||
x, y = x + dx * LOOK_SPEED_X, y + dy * LOOK_SPEED_Y
|
||||
y = max(-90, min(90, y))
|
||||
self.rotation = (x, y)
|
||||
rotation = (x, y)
|
||||
if self.rotation != rotation:
|
||||
self.rotation = rotation
|
||||
self.frustum_updated = True
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
"""Event handler for the Window.on_key_press event.
|
||||
|
@ -599,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.
|
||||
|
@ -629,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)
|
||||
|
@ -648,277 +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 = {}
|
||||
|
||||
# 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 = {}
|
||||
|
||||
#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 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.
|
||||
|
||||
"""
|
||||
self._shown.pop(position).delete()
|
||||
|
||||
def show_sector(self, sector):
|
||||
""" Ensure all blocks in the given sector that should be shown are
|
||||
drawn to the canvas.
|
||||
|
||||
"""
|
||||
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.
|
||||
|
||||
"""
|
||||
for position in self.sectors.get(sector, []):
|
||||
if position in self.shown:
|
||||
self.hide_block(position, False)
|
||||
|
||||
def change_sectors(self, before, after):
|
||||
""" Move from sector `before` to sector `after`. A sector is a
|
||||
contiguous x, y sub-region of world. Sectors are used to speed up
|
||||
world rendering.
|
||||
|
||||
"""
|
||||
before_set = set()
|
||||
after_set = set()
|
||||
pad = 4
|
||||
for dx in range(-pad, pad + 1):
|
||||
for dy in [0]: # range(-pad, pad + 1):
|
||||
for dz in range(-pad, pad + 1):
|
||||
if dx ** 2 + dy ** 2 + dz ** 2 > (pad + 1) ** 2:
|
||||
continue
|
||||
if before:
|
||||
x, y, z = before
|
||||
before_set.add((x + dx, y + dy, z + dz))
|
||||
if after:
|
||||
x, y, z = after
|
||||
after_set.add((x + dx, y + dy, z + dz))
|
||||
show = after_set - before_set
|
||||
hide = before_set - after_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.clock()
|
||||
while self.queue and time.clock() - 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()
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
Copyright (c) 2008 Casey Duncan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,383 @@
|
|||
# Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com)
|
||||
# see LICENSE.txt for details
|
||||
|
||||
"""Perlin noise -- pure python implementation"""
|
||||
|
||||
__version__ = '$Id: perlin.py 521 2008-12-15 03:03:52Z casey.duncan $'
|
||||
|
||||
from math import floor, fmod, sqrt
|
||||
from random import randint
|
||||
|
||||
# 3D Gradient vectors
|
||||
_GRAD3 = ((1, 1, 0), (-1, 1, 0), (1, -1, 0), (-1, -1, 0),
|
||||
(1, 0, 1), (-1, 0, 1), (1, 0, -1), (-1, 0, -1),
|
||||
(0, 1, 1), (0, -1, 1), (0, 1, -1), (0, -1, -1),
|
||||
(1, 1, 0), (0, -1, 1), (-1, 1, 0), (0, -1, -1),
|
||||
)
|
||||
|
||||
# 4D Gradient vectors
|
||||
_GRAD4 = ((0, 1, 1, 1), (0, 1, 1, -1), (0, 1, -1, 1), (0, 1, -1, -1),
|
||||
(0, -1, 1, 1), (0, -1, 1, -1), (0, -1, -1, 1), (0, -1, -1, -1),
|
||||
(1, 0, 1, 1), (1, 0, 1, -1), (1, 0, -1, 1), (1, 0, -1, -1),
|
||||
(-1, 0, 1, 1), (-1, 0, 1, -1), (-1, 0, -1, 1), (-1, 0, -1, -1),
|
||||
(1, 1, 0, 1), (1, 1, 0, -1), (1, -1, 0, 1), (1, -1, 0, -1),
|
||||
(-1, 1, 0, 1), (-1, 1, 0, -1), (-1, -1, 0, 1), (-1, -1, 0, -1),
|
||||
(1, 1, 1, 0), (1, 1, -1, 0), (1, -1, 1, 0), (1, -1, -1, 0),
|
||||
(-1, 1, 1, 0), (-1, 1, -1, 0), (-1, -1, 1, 0), (-1, -1, -1, 0))
|
||||
|
||||
# A lookup table to traverse the simplex around a given point in 4D.
|
||||
# Details can be found where this table is used, in the 4D noise method.
|
||||
_SIMPLEX = (
|
||||
(0, 1, 2, 3), (0, 1, 3, 2), (0, 0, 0, 0), (0, 2, 3, 1), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (1, 2, 3, 0),
|
||||
(0, 2, 1, 3), (0, 0, 0, 0), (0, 3, 1, 2), (0, 3, 2, 1), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (1, 3, 2, 0),
|
||||
(0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0),
|
||||
(1, 2, 0, 3), (0, 0, 0, 0), (1, 3, 0, 2), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (2, 3, 0, 1), (2, 3, 1, 0),
|
||||
(1, 0, 2, 3), (1, 0, 3, 2), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (2, 0, 3, 1), (0, 0, 0, 0), (2, 1, 3, 0),
|
||||
(0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0),
|
||||
(2, 0, 1, 3), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (3, 0, 1, 2), (3, 0, 2, 1), (0, 0, 0, 0), (3, 1, 2, 0),
|
||||
(2, 1, 0, 3), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (3, 1, 0, 2), (0, 0, 0, 0), (3, 2, 0, 1), (3, 2, 1, 0))
|
||||
|
||||
# Simplex skew constants
|
||||
_F2 = 0.5 * (sqrt(3.0) - 1.0)
|
||||
_G2 = (3.0 - sqrt(3.0)) / 6.0
|
||||
_F3 = 1.0 / 3.0
|
||||
_G3 = 1.0 / 6.0
|
||||
|
||||
|
||||
class BaseNoise:
|
||||
"""Noise abstract base class"""
|
||||
|
||||
permutation = (151, 160, 137, 91, 90, 15,
|
||||
131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
|
||||
190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
|
||||
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
|
||||
77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
|
||||
102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
|
||||
135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
|
||||
5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
|
||||
223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
|
||||
129, 22, 39, 253, 9, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
|
||||
251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
|
||||
49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
|
||||
138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180)
|
||||
|
||||
period = len(permutation)
|
||||
|
||||
# Double permutation array so we don't need to wrap
|
||||
permutation = permutation * 2
|
||||
|
||||
randint_function = randint
|
||||
|
||||
def __init__(self, period=None, permutation_table=None, randint_function=None):
|
||||
"""Initialize the noise generator. With no arguments, the default
|
||||
period and permutation table are used (256). The default permutation
|
||||
table generates the exact same noise pattern each time.
|
||||
|
||||
An integer period can be specified, to generate a random permutation
|
||||
table with period elements. The period determines the (integer)
|
||||
interval that the noise repeats, which is useful for creating tiled
|
||||
textures. period should be a power-of-two, though this is not
|
||||
enforced. Note that the speed of the noise algorithm is indpendent of
|
||||
the period size, though larger periods mean a larger table, which
|
||||
consume more memory.
|
||||
|
||||
A permutation table consisting of an iterable sequence of whole
|
||||
numbers can be specified directly. This should have a power-of-two
|
||||
length. Typical permutation tables are a sequnce of unique integers in
|
||||
the range [0,period) in random order, though other arrangements could
|
||||
prove useful, they will not be "pure" simplex noise. The largest
|
||||
element in the sequence must be no larger than period-1.
|
||||
|
||||
period and permutation_table may not be specified together.
|
||||
|
||||
A substitute for the method random.randint(a, b) can be chosen. The
|
||||
method must take two integer parameters a and b and return an integer N
|
||||
such that a <= N <= b.
|
||||
"""
|
||||
if randint_function is not None: # do this before calling randomize()
|
||||
if not hasattr(randint_function, '__call__'):
|
||||
raise TypeError(
|
||||
'randint_function has to be a function')
|
||||
self.randint_function = randint_function
|
||||
if period is None:
|
||||
period = self.period # enforce actually calling randomize()
|
||||
if period is not None and permutation_table is not None:
|
||||
raise ValueError(
|
||||
'Can specify either period or permutation_table, not both')
|
||||
if period is not None:
|
||||
self.randomize(period)
|
||||
elif permutation_table is not None:
|
||||
self.permutation = tuple(permutation_table) * 2
|
||||
self.period = len(permutation_table)
|
||||
|
||||
def randomize(self, period=None):
|
||||
"""Randomize the permutation table used by the noise functions. This
|
||||
makes them generate a different noise pattern for the same inputs.
|
||||
"""
|
||||
if period is not None:
|
||||
self.period = period
|
||||
perm = list(range(self.period))
|
||||
perm_right = self.period - 1
|
||||
for i in list(perm):
|
||||
j = self.randint_function(0, perm_right)
|
||||
perm[i], perm[j] = perm[j], perm[i]
|
||||
self.permutation = tuple(perm) * 2
|
||||
|
||||
|
||||
class SimplexNoise(BaseNoise):
|
||||
"""Perlin simplex noise generator
|
||||
|
||||
Adapted from Stefan Gustavson's Java implementation described here:
|
||||
|
||||
http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
|
||||
|
||||
To summarize:
|
||||
|
||||
"In 2001, Ken Perlin presented 'simplex noise', a replacement for his classic
|
||||
noise algorithm. Classic 'Perlin noise' won him an academy award and has
|
||||
become an ubiquitous procedural primitive for computer graphics over the
|
||||
years, but in hindsight it has quite a few limitations. Ken Perlin himself
|
||||
designed simplex noise specifically to overcome those limitations, and he
|
||||
spent a lot of good thinking on it. Therefore, it is a better idea than his
|
||||
original algorithm. A few of the more prominent advantages are:
|
||||
|
||||
* Simplex noise has a lower computational complexity and requires fewer
|
||||
multiplications.
|
||||
* Simplex noise scales to higher dimensions (4D, 5D and up) with much less
|
||||
computational cost, the complexity is O(N) for N dimensions instead of
|
||||
the O(2^N) of classic Noise.
|
||||
* Simplex noise has no noticeable directional artifacts. Simplex noise has
|
||||
a well-defined and continuous gradient everywhere that can be computed
|
||||
quite cheaply.
|
||||
* Simplex noise is easy to implement in hardware."
|
||||
"""
|
||||
|
||||
def noise2(self, x, y):
|
||||
"""2D Perlin simplex noise.
|
||||
|
||||
Return a floating point value from -1 to 1 for the given x, y coordinate.
|
||||
The same value is always returned for a given x, y pair unless the
|
||||
permutation table changes (see randomize above).
|
||||
"""
|
||||
# Skew input space to determine which simplex (triangle) we are in
|
||||
s = (x + y) * _F2
|
||||
i = floor(x + s)
|
||||
j = floor(y + s)
|
||||
t = (i + j) * _G2
|
||||
x0 = x - (i - t) # "Unskewed" distances from cell origin
|
||||
y0 = y - (j - t)
|
||||
|
||||
if x0 > y0:
|
||||
i1 = 1
|
||||
j1 = 0 # Lower triangle, XY order: (0,0)->(1,0)->(1,1)
|
||||
else:
|
||||
i1 = 0
|
||||
j1 = 1 # Upper triangle, YX order: (0,0)->(0,1)->(1,1)
|
||||
|
||||
x1 = x0 - i1 + _G2 # Offsets for middle corner in (x,y) unskewed coords
|
||||
y1 = y0 - j1 + _G2
|
||||
x2 = x0 + _G2 * 2.0 - 1.0 # Offsets for last corner in (x,y) unskewed coords
|
||||
y2 = y0 + _G2 * 2.0 - 1.0
|
||||
|
||||
# Determine hashed gradient indices of the three simplex corners
|
||||
perm = self.permutation
|
||||
ii = int(i) % self.period
|
||||
jj = int(j) % self.period
|
||||
gi0 = perm[ii + perm[jj]] % 12
|
||||
gi1 = perm[ii + i1 + perm[jj + j1]] % 12
|
||||
gi2 = perm[ii + 1 + perm[jj + 1]] % 12
|
||||
|
||||
# Calculate the contribution from the three corners
|
||||
tt = 0.5 - x0 ** 2 - y0 ** 2
|
||||
if tt > 0:
|
||||
g = _GRAD3[gi0]
|
||||
noise = tt ** 4 * (g[0] * x0 + g[1] * y0)
|
||||
else:
|
||||
noise = 0.0
|
||||
|
||||
tt = 0.5 - x1 ** 2 - y1 ** 2
|
||||
if tt > 0:
|
||||
g = _GRAD3[gi1]
|
||||
noise += tt ** 4 * (g[0] * x1 + g[1] * y1)
|
||||
|
||||
tt = 0.5 - x2 ** 2 - y2 ** 2
|
||||
if tt > 0:
|
||||
g = _GRAD3[gi2]
|
||||
noise += tt ** 4 * (g[0] * x2 + g[1] * y2)
|
||||
|
||||
return noise * 70.0 # scale noise to [-1, 1]
|
||||
|
||||
def noise3(self, x, y, z):
|
||||
"""3D Perlin simplex noise.
|
||||
|
||||
Return a floating point value from -1 to 1 for the given x, y, z coordinate.
|
||||
The same value is always returned for a given x, y, z pair unless the
|
||||
permutation table changes (see randomize above).
|
||||
"""
|
||||
# Skew the input space to determine which simplex cell we're in
|
||||
s = (x + y + z) * _F3
|
||||
i = floor(x + s)
|
||||
j = floor(y + s)
|
||||
k = floor(z + s)
|
||||
t = (i + j + k) * _G3
|
||||
x0 = x - (i - t) # "Unskewed" distances from cell origin
|
||||
y0 = y - (j - t)
|
||||
z0 = z - (k - t)
|
||||
|
||||
# For the 3D case, the simplex shape is a slightly irregular tetrahedron.
|
||||
# Determine which simplex we are in.
|
||||
if x0 >= y0:
|
||||
if y0 >= z0:
|
||||
i1 = 1
|
||||
j1 = 0
|
||||
k1 = 0
|
||||
i2 = 1
|
||||
j2 = 1
|
||||
k2 = 0
|
||||
elif x0 >= z0:
|
||||
i1 = 1
|
||||
j1 = 0
|
||||
k1 = 0
|
||||
i2 = 1
|
||||
j2 = 0
|
||||
k2 = 1
|
||||
else:
|
||||
i1 = 0
|
||||
j1 = 0
|
||||
k1 = 1
|
||||
i2 = 1
|
||||
j2 = 0
|
||||
k2 = 1
|
||||
else: # x0 < y0
|
||||
if y0 < z0:
|
||||
i1 = 0
|
||||
j1 = 0
|
||||
k1 = 1
|
||||
i2 = 0
|
||||
j2 = 1
|
||||
k2 = 1
|
||||
elif x0 < z0:
|
||||
i1 = 0
|
||||
j1 = 1
|
||||
k1 = 0
|
||||
i2 = 0
|
||||
j2 = 1
|
||||
k2 = 1
|
||||
else:
|
||||
i1 = 0
|
||||
j1 = 1
|
||||
k1 = 0
|
||||
i2 = 1
|
||||
j2 = 1
|
||||
k2 = 0
|
||||
|
||||
# Offsets for remaining corners
|
||||
x1 = x0 - i1 + _G3
|
||||
y1 = y0 - j1 + _G3
|
||||
z1 = z0 - k1 + _G3
|
||||
x2 = x0 - i2 + 2.0 * _G3
|
||||
y2 = y0 - j2 + 2.0 * _G3
|
||||
z2 = z0 - k2 + 2.0 * _G3
|
||||
x3 = x0 - 1.0 + 3.0 * _G3
|
||||
y3 = y0 - 1.0 + 3.0 * _G3
|
||||
z3 = z0 - 1.0 + 3.0 * _G3
|
||||
|
||||
# Calculate the hashed gradient indices of the four simplex corners
|
||||
perm = self.permutation
|
||||
ii = int(i) % self.period
|
||||
jj = int(j) % self.period
|
||||
kk = int(k) % self.period
|
||||
gi0 = perm[ii + perm[jj + perm[kk]]] % 12
|
||||
gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12
|
||||
gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12
|
||||
gi3 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12
|
||||
|
||||
# Calculate the contribution from the four corners
|
||||
noise = 0.0
|
||||
tt = 0.6 - x0 ** 2 - y0 ** 2 - z0 ** 2
|
||||
if tt > 0:
|
||||
g = _GRAD3[gi0]
|
||||
noise = tt ** 4 * (g[0] * x0 + g[1] * y0 + g[2] * z0)
|
||||
else:
|
||||
noise = 0.0
|
||||
|
||||
tt = 0.6 - x1 ** 2 - y1 ** 2 - z1 ** 2
|
||||
if tt > 0:
|
||||
g = _GRAD3[gi1]
|
||||
noise += tt ** 4 * (g[0] * x1 + g[1] * y1 + g[2] * z1)
|
||||
|
||||
tt = 0.6 - x2 ** 2 - y2 ** 2 - z2 ** 2
|
||||
if tt > 0:
|
||||
g = _GRAD3[gi2]
|
||||
noise += tt ** 4 * (g[0] * x2 + g[1] * y2 + g[2] * z2)
|
||||
|
||||
tt = 0.6 - x3 ** 2 - y3 ** 2 - z3 ** 2
|
||||
if tt > 0:
|
||||
g = _GRAD3[gi3]
|
||||
noise += tt ** 4 * (g[0] * x3 + g[1] * y3 + g[2] * z3)
|
||||
|
||||
return noise * 32.0
|
||||
|
||||
|
||||
def lerp(t, a, b):
|
||||
return a + t * (b - a)
|
||||
|
||||
|
||||
def grad3(hash, x, y, z):
|
||||
g = _GRAD3[hash % 16]
|
||||
return x * g[0] + y * g[1] + z * g[2]
|
||||
|
||||
|
||||
class TileableNoise(BaseNoise):
|
||||
"""Tileable implemention of Perlin "improved" noise. This
|
||||
is based on the reference implementation published here:
|
||||
|
||||
http://mrl.nyu.edu/~perlin/noise/
|
||||
"""
|
||||
|
||||
def noise3(self, x, y, z, repeat, base=0.0):
|
||||
"""Tileable 3D noise.
|
||||
|
||||
repeat specifies the integer interval in each dimension
|
||||
when the noise pattern repeats.
|
||||
|
||||
base allows a different texture to be generated for
|
||||
the same repeat interval.
|
||||
"""
|
||||
i = int(fmod(floor(x), repeat))
|
||||
j = int(fmod(floor(y), repeat))
|
||||
k = int(fmod(floor(z), repeat))
|
||||
ii = (i + 1) % repeat
|
||||
jj = (j + 1) % repeat
|
||||
kk = (k + 1) % repeat
|
||||
if base:
|
||||
i += base
|
||||
j += base
|
||||
k += base
|
||||
ii += base
|
||||
jj += base
|
||||
kk += base
|
||||
|
||||
x -= floor(x)
|
||||
y -= floor(y)
|
||||
z -= floor(z)
|
||||
fx = x ** 3 * (x * (x * 6 - 15) + 10)
|
||||
fy = y ** 3 * (y * (y * 6 - 15) + 10)
|
||||
fz = z ** 3 * (z * (z * 6 - 15) + 10)
|
||||
|
||||
perm = self.permutation
|
||||
A = perm[i]
|
||||
AA = perm[A + j]
|
||||
AB = perm[A + jj]
|
||||
B = perm[ii]
|
||||
BA = perm[B + j]
|
||||
BB = perm[B + jj]
|
||||
|
||||
return lerp(fz, lerp(fy, lerp(fx, grad3(perm[AA + k], x, y, z),
|
||||
grad3(perm[BA + k], x - 1, y, z)),
|
||||
lerp(fx, grad3(perm[AB + k], x, y - 1, z),
|
||||
grad3(perm[BB + k], x - 1, y - 1, z))),
|
||||
lerp(fy, lerp(fx, grad3(perm[AA + kk], x, y, z - 1),
|
||||
grad3(perm[BA + kk], x - 1, y, z - 1)),
|
||||
lerp(fx, grad3(perm[AB + kk], x, y - 1, z - 1),
|
||||
grad3(perm[BB + kk], x - 1, y - 1, z - 1))))
|
5
main.py
5
main.py
|
@ -33,6 +33,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
"""
|
||||
|
||||
import pyglet
|
||||
import os.path
|
||||
|
||||
from game.graphics import *
|
||||
from game.scenemanager import SceneManager
|
||||
|
@ -40,7 +41,9 @@ from game.scenemanager import SceneManager
|
|||
|
||||
def main():
|
||||
# The pyglet.resource module handles efficient loading of assets:
|
||||
pyglet.resource.path = ['assets', 'assets/images', 'assets/sounds']
|
||||
path = ['assets', 'assets/images', 'assets/sounds']
|
||||
path = [os.path.abspath(p) for p in path]
|
||||
pyglet.resource.path = path
|
||||
pyglet.resource.reindex()
|
||||
|
||||
# Create the main game Window, and set it's icon:
|
||||
|
|
Loading…
Reference in New Issue