TerraCraft/game/scenes.py

1064 lines
38 KiB
Python
Executable File

#!/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 random
import time
import pyglet
from collections import deque
from pyglet.gl import *
from pyglet.media import Player
from pyglet.window import key, mouse
from pyglet.sprite import Sprite
from pyglet.graphics import OrderedGroup
from .blocks import *
from .utilities import *
from .graphics import BlockGroup
from .genworld import WorldGenerator
class AudioEngine:
"""A high level audio engine for easily playing SFX and Music."""
def __init__(self, channels=5):
self.sfx_players = deque([Player() for _ in range(channels)], maxlen=channels)
self.music_player = Player()
def set_volume(self, percentage):
"""Set the audio volume, as a percentage of 1 to 100.
:param percentage: int: The volume, as a percentage.
"""
volume = max(min(1, percentage / 100), 0)
for player in self.sfx_players:
player.volume = volume
self.music_player.volume = volume
def play(self, source, position=(0, 0, 0)):
"""Play a sound effect on the next available channel
:param source: A pyglet audio Source
:param position: Optional spacial position for the sound.
"""
player = self.sfx_players[0]
player.position = position
player.queue(source=source)
if not player.playing:
player.play()
else:
player.next_source()
self.sfx_players.rotate()
def play_music(self, source):
"""Play a music track, or switch to a new one.
:param source: A pyglet audio Source
"""
self.music_player.queue(source=source)
if not self.music_player.playing:
self.music_player.play()
else:
self.music_player.next_source()
class Scene:
"""A base class for all Scenes to inherit from.
All Scenes must contain an `update` method. In addition,
you can also define event handlers for any of the events
dispatched by the `Window`. Any Scene methods that match
the Window event names will be automatically set when
changing to the Scene.
"""
scene_manager = None # This is assigned when adding the Scene
audio = AudioEngine() # All Scenes share the same AudioEngine
def update(self, dt):
raise NotImplementedError
class MenuScene(Scene):
def __init__(self, window):
self.window = window
self.batch = pyglet.graphics.Batch()
# Create a
title_image = pyglet.resource.image('TerraCraft.png')
title_image.anchor_x = title_image.width // 2
title_image.anchor_y = title_image.height + 10
position = self.window.width // 2, self.window.height
self.title_graphic = Sprite(img=title_image, x=position[0], y=position[1], batch=self.batch)
self.start_label = pyglet.text.Label('Select save & press Enter to start', font_size=25,
x=self.window.width // 2, y=self.window.height // 2,
anchor_x='center', anchor_y='center', batch=self.batch)
# Create labels for three save slots:
self.save_slot_labels = []
for save_slot in [1, 2, 3]:
self.scene_manager.save.save_slot = save_slot
# indicate if an existing save exists
if self.scene_manager.save.has_save_game():
label_text = f"{save_slot}: load"
else:
label_text = f"{save_slot}: new game"
y_pos = 190 - 50 * save_slot
label = pyglet.text.Label(
label_text, font_size=20, x=40, y=y_pos, batch=self.batch)
self.save_slot_labels.append(label)
# Highlight the default save slot
self.scene_manager.save.save_slot = 1
self._highlight_save_slot()
def update(self, dt):
pass
def _highlight_save_slot(self):
# First reset all labels to white
for label in self.save_slot_labels:
label.color = 255, 255, 255, 255
# Highlight the selected slot
index = self.scene_manager.save.save_slot - 1
self.save_slot_labels[index].color = 50, 50, 50, 255
def on_key_press(self, symbol, modifiers):
"""Event handler for the Window.on_key_press event."""
if symbol == key.ENTER:
self.scene_manager.change_scene('GameScene')
elif symbol == key.ESCAPE:
self.window.set_exclusive_mouse(False)
return pyglet.event.EVENT_HANDLED
if symbol in (key._1, key._2, key._3):
if symbol == key._1:
self.scene_manager.save.save_slot = 1
elif symbol == key._2:
self.scene_manager.save.save_slot = 2
elif symbol == key._3:
self.scene_manager.save.save_slot = 3
self._highlight_save_slot()
def on_mouse_press(self, x, y, button, modifiers):
"""Event handler for the Window.on_resize event."""
self.window.set_exclusive_mouse(True)
def on_resize(self, width, height):
"""Event handler for the Window.on_resize event."""
# Keep the graphics centered on resize
self.title_graphic.position = width//2, height
self.start_label.x = width // 2
self.start_label.y = height // 2
def on_draw(self):
"""Event handler for the Window.on_draw event."""
self.window.clear()
self.batch.draw()
class GameScene(Scene):
def __init__(self, window):
self.window = window
# A Batch is a collection of vertex lists for batched rendering.
self.batch = pyglet.graphics.Batch()
# pyglet Groups manages setting/unsetting OpenGL state.
self.block_group = BlockGroup(
self.window, pyglet.resource.texture('textures.png'), order=0)
self.hud_group = OrderedGroup(order=1)
# Whether or not the window exclusively captures the mouse.
self.exclusive = False
# When flying gravity has no effect and speed is increased.
self.flying = FLYING
# Determine if player is running. If false, then player is walking.
self.running = RUNNING
# Wether or not all gui elements are drawn.
self.toggleGui = TOGGLE_GUI
# Wether or not the fps counter and player coordinates are drawn.
self.toggleLabel = TOGGLE_INFO_LABEL
# Strafing is moving lateral to the direction you are facing,
# e.g. moving to the left or right while continuing to face forward.
#
# First element is -1 when moving forward, 1 when moving back, and 0
# otherwise. The second element is -1 when moving left, 1 when moving
# right, and 0 otherwise.
self.strafe = [0, 0]
# 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)
# 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
# angle from the ground plane up. Rotation is in degrees.
#
# The vertical plane rotation ranges from -90 (looking straight down) to
# 90 (looking straight up). The horizontal rotation range is unbounded.
self.rotation = (0, 0)
# 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
# Velocity in the y (upward) direction.
self.dy = 0
# A list of blocks the player can place. Hit num keys to cycle.
self.inventory = [DIRT, DIRT_WITH_GRASS, SAND, SNOW, COBBLESTONE,
BRICK_COBBLESTONE, BRICK, TREE, LEAVES, WOODEN_PLANKS]
# The current block the user can place. Hit num keys to cycle.
self.block = self.inventory[0]
# Convenience list of num keys.
self.num_keys = [key._1, key._2, key._3, key._4, key._5,
key._6, key._7, key._8, key._9, key._0]
# Instance of the model that handles the world.
self.model = Model(batch=self.batch, group=self.block_group)
# The crosshairs at the center of the screen.
self.reticle = self.batch.add(4, GL_LINES, self.hud_group, 'v2i', ('c3B', [0]*12))
# The highlight around focused block.
indices = [0, 1, 1, 2, 2, 3, 3, 0, 4, 7, 7, 6, 6, 5, 5, 4, 0, 4, 1, 7, 2, 6, 3, 5]
self.highlight = self.batch.add_indexed(24, GL_LINES, self.block_group, indices,
'v3f/dynamic', ('c3B', [0]*72))
# The label that is displayed in the top left of the canvas.
self.info_label = pyglet.text.Label('', font_name='Arial', font_size=INFO_LABEL_FONTSIZE,
x=10, y=self.window.height - 10, anchor_x='left',
anchor_y='top', color=(0, 0, 0, 255))
# Boolean whether to display loading screen.
self.initialized = False
# Some environmental SFX to preload:
self.jump_sfx = pyglet.resource.media('jump.wav', streaming=False)
self.destroy_sfx = pyglet.resource.media('dirt.wav', streaming=False)
self.on_resize(*self.window.get_size())
def set_exclusive_mouse(self, exclusive):
""" If `exclusive` is True, the game will capture the mouse, if False
the game will ignore the mouse.
"""
self.window.set_exclusive_mouse(exclusive)
self.exclusive = exclusive
def get_sight_vector(self):
""" Returns the current line of sight vector indicating the direction
the player is looking.
"""
x, y = self.rotation
# y ranges from -90 to 90, or -pi/2 to pi/2, so m ranges from 0 to 1 and
# is 1 when looking ahead parallel to the ground and 0 when looking
# straight up or down.
m = math.cos(math.radians(y))
# dy ranges from -1 to 1 and is -1 when looking straight down and 1 when
# looking straight up.
dy = math.sin(math.radians(y))
dx = math.cos(math.radians(x - 90)) * m
dz = math.sin(math.radians(x - 90)) * m
return dx, dy, dz
def get_motion_vector(self):
""" Returns the current motion vector indicating the velocity of the
player.
Returns
-------
vector : tuple of len 3
Tuple containing the velocity in x, y, and z respectively.
"""
if any(self.strafe):
x, y = self.rotation
strafe = math.degrees(math.atan2(*self.strafe))
y_angle = math.radians(y)
x_angle = math.radians(x + strafe)
if self.flying:
m = math.cos(y_angle)
dy = math.sin(y_angle)
if self.strafe[1]:
# Moving left or right.
dy = 0.0
m = 1
if self.strafe[0] > 0:
# Moving backwards.
dy *= -1
# When you are flying up or down, you have less left and right
# motion.
dx = math.cos(x_angle) * m
dz = math.sin(x_angle) * m
else:
dy = 0.0
dx = math.cos(x_angle)
dz = math.sin(x_angle)
elif self.flying and not self.dy == 0:
dx = 0.0
dy = self.dy
dz = 0.0
else:
dy = 0.0
dx = 0.0
dz = 0.0
return dx, dy, dz
def update(self, dt):
""" This method is scheduled to be called repeatedly by the pyglet
clock.
Parameters
----------
dt : float
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)
has_save = False
if self.scene_manager.save.has_save_game():
# Returns False if unable to load the save
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.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.initialized = True
self.model.process_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):
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.
Parameters
----------
dt : float
The change in time since the last call.
"""
# walking
speed = FLYING_SPEED if self.flying else RUNNING_SPEED if self.running else WALKING_SPEED
d = dt * speed # distance covered this tick.
dx, dy, dz = self.get_motion_vector()
# New position in space, before accounting for gravity.
dx, dy, dz = dx * d, dy * d, dz * d
# gravity
if not self.flying:
# Update your vertical speed: if you are falling, speed up until you
# hit terminal velocity; if you are jumping, slow down until you
# start falling.
self.dy -= dt * GRAVITY
self.dy = max(self.dy, -TERMINAL_VELOCITY)
dy += self.dy * dt
# 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
self.frustum_updated = True
def collide(self, position, height):
""" Checks to see if the player at the given `position` and `height`
is colliding with any blocks in the world.
Parameters
----------
position : tuple of len 3
The (x, y, z) position to check for collisions at.
height : int or float
The height of the player.
Returns
-------
position : tuple of len 3
The new position of the player taking into account collisions.
"""
# How much overlap with a dimension of a surrounding block you need to
# have to count as a collision. If 0, touching terrain at all counts as
# a collision. If .49, you sink into the ground, as if walking through
# tall DIRT_WITH_GRASS. If >= .5, you'll fall through the ground.
pad = 0.25
p = list(position)
np = normalize(position)
for face in FACES: # check all surrounding blocks
for i in range(3): # check each dimension independently
if not face[i]:
continue
# How much overlap you have with this dimension.
d = (p[i] - np[i]) * face[i]
if d < pad:
continue
for dy in range(height): # check each height
op = list(np)
op[1] -= dy
op[i] += face[i]
if tuple(op) not in self.model.world:
continue
p[i] -= (d - pad) * face[i]
if face == (0, -1, 0) or face == (0, 1, 0):
# You are colliding with the ground or ceiling, so stop
# falling / rising.
self.dy = 0
break
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 = 4
for dx in range(-pad, pad + 1):
for dy in [0]: # range(-pad, pad + 1):
for dz in range(-pad, pad + 1):
# Manathan distance
dist = abs(dx) + abs(dz)
if dist > 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.
Called when a mouse button is pressed. See pyglet docs for button
amd modifier mappings.
Parameters
----------
x, y : int
The coordinates of the mouse click. Always center of the screen if
the mouse is captured.
button : int
Number representing mouse button that was clicked. 1 = left button,
4 = right button.
modifiers : int
Number representing any modifying keys that were pressed when the
mouse button was clicked.
"""
if self.exclusive:
vector = self.get_sight_vector()
block, previous = self.model.hit_test(self.position, vector)
if button == mouse.RIGHT or (button == mouse.LEFT and modifiers & key.MOD_CTRL):
# ON OSX, control + left click = right click.
if previous:
self.model.add_block(previous, self.block)
elif button == pyglet.window.mouse.LEFT and block:
texture = self.model.world[block]
if texture != BEDSTONE:
self.model.remove_block(block)
self.audio.play(self.destroy_sfx)
else:
self.set_exclusive_mouse(True)
def on_mouse_motion(self, x, y, dx, dy):
"""Event handler for the Window.on_mouse_motion event.
Called when the player moves the mouse.
Parameters
----------
x, y : int
The coordinates of the mouse click. Always center of the screen if
the mouse is captured.
dx, dy : float
The movement of the mouse.
"""
if self.exclusive:
x, y = self.rotation
x, y = x + dx * LOOK_SPEED_X, y + dy * LOOK_SPEED_Y
y = max(-90, min(90, 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.
Called when the player presses a key. See pyglet docs for key
mappings.
Parameters
----------
symbol : int
Number representing the key that was pressed.
modifiers : int
Number representing any modifying keys that were pressed.
"""
if symbol == key.W:
self.strafe[0] -= 1
elif symbol == key.S:
self.strafe[0] += 1
elif symbol == key.A:
self.strafe[1] -= 1
elif symbol == key.D:
self.strafe[1] += 1
elif symbol == key.SPACE:
if self.flying:
# Reduces vertical flying speed
# 0.1 positive value that allows vertical flight up.
self.dy = 0.1 * JUMP_SPEED
elif self.dy == 0:
self.dy = JUMP_SPEED
self.audio.play(self.jump_sfx)
elif symbol == key.LCTRL:
self.running = True
elif symbol == key.LSHIFT:
if self.flying:
# Reduces vertical flying speed
# -0.1 negative value that allows vertical flight down.
self.dy = -0.1 * JUMP_SPEED
elif self.dy == 0:
self.dy = JUMP_SPEED
elif symbol == key.ESCAPE:
self.set_exclusive_mouse(False)
return pyglet.event.EVENT_HANDLED
elif symbol == key.TAB:
self.flying = not self.flying
elif symbol == key.F1:
self.scene_manager.change_scene("HelpScene")
elif symbol == key.F2:
self.toggleGui = not self.toggleGui
elif symbol == key.F3:
self.toggleLabel = not self.toggleLabel
elif symbol == key.F5:
self.scene_manager.save.save_world(self.model)
elif symbol == key.F12:
pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png')
elif symbol in self.num_keys:
index = (symbol - self.num_keys[0]) % len(self.inventory)
self.block = self.inventory[index]
elif symbol == key.ENTER:
self.scene_manager.change_scene('MenuScene')
def on_key_release(self, symbol, modifiers):
"""Event handler for the Window.on_key_release event.
Called when the player releases a key. See pyglet docs for key
mappings.
Parameters
----------
symbol : int
Number representing the key that was pressed.
modifiers : int
Number representing any modifying keys that were pressed.
"""
if symbol == key.W:
self.strafe[0] += 1
elif symbol == key.S:
self.strafe[0] -= 1
elif symbol == key.A:
self.strafe[1] += 1
elif symbol == key.D:
self.strafe[1] -= 1
elif symbol == key.SPACE:
self.dy = 0
elif symbol == key.LCTRL:
self.running = False
elif symbol == key.LSHIFT:
self.dy = 0
def on_resize(self, width, height):
"""Event handler for the Window.on_resize event.
Called when the window is resized to a new `width` and `height`.
"""
# Reset the info label and reticle positions.
self.info_label.y = height - 10
x, y = width // 2, height // 2
n = 10
self.reticle.vertices[:] = (x - n, y, x + n, y, x, y - n, x, y + n)
def on_draw(self):
"""Event handler for the Window.on_draw event.
Called by pyglet to draw the canvas.
"""
self.window.clear()
# Set the current position/rotation before drawing
self.block_group.position = self.position
self.block_group.rotation = self.rotation
# Draw everything in the batch
self.batch.draw()
# Optionally draw some things
if self.toggleGui:
self.draw_focused_block()
if self.toggleLabel:
self.draw_label()
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]
if block:
x, y, z = block
self.highlight.vertices[:] = cube_vertices(x, y, z, 0.51)
else:
# Make invisible by setting all vertices to 0
self.highlight.vertices[:] = [0] * 72
def draw_label(self):
""" Draw the label in the top left of the screen.
"""
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))
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
self.batch = pyglet.graphics.Batch()
self.labels = []
self.text_strings = [" GAME OPTIONS",
"* Left click mouse to destroy block",
"* Right click mouse to create block",
"* Press keys 1 through 0 to choose block type",
"* Press F2 key to hide block selection",
"* Press F3 key to hide debug stats"]
self.return_label = pyglet.text.Label("Press any key to return to game", font_size=25,
x=self.window.width // 2, y=20, anchor_x='center',
color=(0, 50, 50, 255), batch=self.batch)
self.spacing = 60
y_position = self.window.height - self.spacing
for string in self.text_strings:
self.labels.append(pyglet.text.Label(string, font_size=22, x=40, y=y_position,
color=(0, 50, 50, 255), batch=self.batch))
y_position -= self.spacing
self.on_resize(*self.window.get_size())
def on_resize(self, width, height):
y_position = height - self.spacing
for label in self.labels:
label.y = y_position
y_position -= self.spacing
def update(self, dt):
pass
def on_key_press(self, symbol, modifiers):
self.scene_manager.change_scene("GameScene")
return pyglet.event.EVENT_HANDLED
def on_draw(self):
self.window.clear()
self.batch.draw()