def __init__(self, name, spawn_pos): self.name = name self._spawn_pos = WVec(spawn_pos) self.pos = WVec() self._req_pos = WVec() self._vel = WVec() self._req_vel = WVec() self.spawn() self._is_on_ground = False self._w_size = WVec(0.6, 1.8) self._bounds_w_shift = WBounds( min=WVec(-self._w_size.x / 2, 0.0), max=WVec(self._w_size.x / 2, self._w_size.y), ) self._anim_surf_walking = AnimatedSurface( os.path.join(RESOURCES_PATH, self._MAIN_PLAYER_DIR, "walking"), w_height=self._w_size.y, neutrals=(0, 8), ) self._anim_surf_sprinting = AnimatedSurface( os.path.join(RESOURCES_PATH, self._MAIN_PLAYER_DIR, "sprinting"), w_height=self._w_size.y, neutrals=(0, 6), ) self._anim_surf = self._anim_surf_walking self._walking_speed = PLAYER_ABILITY_FACTOR * 4.5 / CAM_FPS self._sprinting_speed = PLAYER_ABILITY_FACTOR * 7.5 / CAM_FPS self._jumping_speed = PLAYER_ABILITY_FACTOR * 7.75 / CAM_FPS
def _update_colliders(self): self.colliders = Colliders() for block_w_pos in self.blocks_map: if not (block_w_pos + WVec(-1, 0)) in self.blocks_map: self.colliders.left.append(block_w_pos) if not (block_w_pos + WVec(+1, 0)) in self.blocks_map: self.colliders.right.append(block_w_pos) if not (block_w_pos + WVec(0, -1)) in self.blocks_map: self.colliders.down.append(block_w_pos) if not (block_w_pos + WVec(0, +1)) in self.blocks_map: self.colliders.up.append(block_w_pos)
def __init__(self): self._seed = random.randint(0, 2**20) + 0.15681 self.chunks_existing_map = {} self._chunks_visible_map = {} self._c_view = CBounds(CVec(0, 0), CVec( 0, 0)) # Needs to keep the arguments, in order to remain of type int. self._max_view = WBounds(WVec(0, 0), WVec(0, 0)) # This too. self._max_surf = pg.Surface((0, 0)) self._force_draw = True self._action_cooldown_remaining = 0
def __init__(self, dir_path, w_height, neutrals=(), frame_rate=30): self.neutrals = neutrals self._images = [] self._images_flipped = [] images_path = os.path.join(dir_path, "images") masks_path = os.path.join(dir_path, "masks") for image_file, mask_file in zip( sorted(os.scandir(images_path), key=lambda x: x.name), sorted(os.scandir(masks_path), key=lambda x: x.name)): image = pg.image.load(image_file.path).convert() mask = pg.image.load(mask_file.path).convert() self._images.append((image, mask)) self._images_flipped.append((pg.transform.flip(image, True, False), pg.transform.flip(mask, True, False))) self.pix_size = PixVec(*self._images[0][0].get_size()) pix_to_w_factor = w_height / self.pix_size.y self.w_size = WVec(x=self.pix_size.x * pix_to_w_factor, y=w_height) self.action = AnimAction.pause self._frame = 0 self._frame_rate = frame_rate self.is_flipped = False self.surf = pg.Surface(self.pix_size) self._light_surf = pg.Surface(self.pix_size)
def gen_chunk_blocks(self, chunk_w_pos: WVec): blocks_map = {} for w_shift_x in range(CHUNK_W_SIZE.x): for w_shift_y in range(CHUNK_W_SIZE.y): block_w_pos = WVec(chunk_w_pos.x + w_shift_x, chunk_w_pos.y + w_shift_y) block_type = self._choose_block_type_at_pos(block_w_pos) if block_type is None: continue blocks_map[block_w_pos] = Block(block_type) return blocks_map
def pix_to_w_shift(pix_shift: PixVec, source_surf_pix_size: PixVec, dest_surf_pix_size: PixVec, source_pivot: PixVec = PixVec(), dest_pivot: PixVec = PixVec(), *, scale=BLOCK_PIX_SIZE.x): return WVec( (pix_shift.x + source_pivot.x - dest_pivot.x) / scale, (-pix_shift.y + dest_surf_pix_size.y - source_surf_pix_size.y + source_pivot.y - dest_pivot.y) / scale, )
def __init__(self): self._pos = WVec() self._req_pos = WVec(self._pos) self._vel = WVec() self._req_vel = WVec(self._vel) self._zoom_vel = 1.0 self._req_zoom_vel = 1.0 self._scale = CAM_DEFAULT_SCALE if FULLSCREEN: self._screen = pg.display.set_mode((0, 0), pg.FULLSCREEN) else: self._screen = pg.display.set_mode((1280, 720)) self._pix_size = PixVec(self._screen.get_size()) self.selected_block_w_pos = WVec(self._pos) self.selected_space_w_pos = WVec(self._pos) self._block_selector_surf = pg.image.load( os.path.join(GUI_PATH, "block_selector.png")).convert() self._block_selector_space_only_surf = pg.image.load( os.path.join(GUI_PATH, "block_selector_space_only.png")).convert() # Surfs to reuse self._world_max_surf_scaled = pg.Surface((0, 0)) self._player_surf_scaled = pg.Surface((0, 0)) self._player_surf_scaled.set_colorkey(C_KEY) self._block_selector_surf_scaled = pg.Surface((0, 0)) self._block_selector_surf_scaled.set_colorkey(C_KEY) self._clock = pg.time.Clock() self._font = pg.font.SysFont(pg.font.get_default_font(), 24)
def set_transforms(self, pos, vel=WVec()): """Set the transforms directly without going through a request. """ self.pos = WVec(pos) self._req_pos = WVec(self.pos) self._vel = WVec(vel) self._req_vel = WVec(self._vel)
def _get_chunk_maps_around(self, w_pos: WVec, c_radius): chunks_map = {} for pos_x in range(floor(w_pos.x - c_radius * CHUNK_W_SIZE.x), floor(w_pos.x + (c_radius + 1) * CHUNK_W_SIZE.x), CHUNK_W_SIZE.x): for pos_y in range( floor(w_pos.y - c_radius * CHUNK_W_SIZE.y), floor(w_pos.y + (c_radius + 1) * CHUNK_W_SIZE.y), CHUNK_W_SIZE.y): chunk_map = self._get_chunk_map_at_w_pos(WVec(pos_x, pos_y)) if chunk_map is None: continue chunks_map.update((chunk_map, )) return chunks_map
def move(self, world, substeps=1): """Apply requested and physics-induced movements. """ self._vel.x += (self._req_vel.x - self._vel.x) * PLAYER_POS_DAMPING_FACTOR self._vel.y += self._req_vel.y self._vel += self._ACC self._is_on_ground = False # Assumption, to be corrected inside self._collide. collision_steps = (floor(self._vel.norm()) + 1) * substeps for _ in range(collision_steps): self._req_pos = self.pos + self._vel / collision_steps self._collide(world) self.pos = WVec(self._req_pos)
def _update_chunks_visible(self): self._max_view = WBounds( self._c_view.min * CHUNK_W_SIZE, (self._c_view.max + 1) * CHUNK_W_SIZE, ) self._chunks_visible_map = {} for chunk_w_pos_x in range(self._max_view.min.x, self._max_view.max.x, CHUNK_W_SIZE.x): for chunk_w_pos_y in range(self._max_view.min.y, self._max_view.max.y, CHUNK_W_SIZE.y): chunk_w_pos = WVec(chunk_w_pos_x, chunk_w_pos_y) if chunk_w_pos in self.chunks_existing_map: chunk_visible = self.chunks_existing_map[chunk_w_pos] else: chunk_visible = self._create_chunk(chunk_w_pos) self._chunks_visible_map[chunk_w_pos] = chunk_visible
def get_block_pos_and_space_pos(self, start_w_pos: WVec, end_w_pos: WVec, max_distance, *, substeps=5, max_rays=3) -> BlockSelection: w_vel = end_w_pos - start_w_pos w_speed = w_vel.norm() w_dir = w_vel.dir_() w_dirs = w_vel.dirs_() c_radius = min(w_speed, max_distance) // max(CHUNK_W_SIZE) + 1 block_w_pos: WVec block_w_pos = floor(end_w_pos) blocks_map = self._get_blocks_map_around(end_w_pos, c_radius) got_block = True if block_w_pos not in blocks_map: got_block = False for dir_ in w_dirs: if block_w_pos + dir_ in blocks_map: break else: # Return early if there's no block at or next to block_w_pos: return BlockSelection(None, None) ray_origin_shift = WVec() w_dir_horiz, w_dir_vert = w_dirs if (w_dir_horiz == Dir.right) ^ (not got_block): ray_origin_shift.x = BLOCK_BOUND_SHIFTS.min.x else: ray_origin_shift.x = BLOCK_BOUND_SHIFTS.max.x if (w_dir_vert == Dir.up) ^ (not got_block): ray_origin_shift.y = BLOCK_BOUND_SHIFTS.min.y else: ray_origin_shift.y = BLOCK_BOUND_SHIFTS.max.y poss_to_check = set() for ray_index in range(max_rays): poss_to_check.add(block_w_pos + WVec(ray_origin_shift.x, ray_index / (max_rays - 1))) poss_to_check.add(block_w_pos + WVec(ray_index / (max_rays - 1), ray_origin_shift.y)) hits = 0 found_path = False for pos_to_check in poss_to_check: w_vel_iter = pos_to_check - start_w_pos w_speed_iter = w_vel_iter.norm() w_vel_step = w_vel_iter / (w_speed_iter * substeps) max_mult = floor(w_speed_iter * substeps) for mult in range(max_mult + 1): w_pos = floor(start_w_pos + w_vel_step * mult) if w_pos in blocks_map and not w_pos == block_w_pos: break else: hits += 1 if hits < 2: # This is to avoid selecting blocks for which only 1 corner is visible. continue found_path = True break if not found_path: return BlockSelection(None, None) if got_block: block_w_pos_shifts = [-w_dir_horiz, -w_dir_vert] else: block_w_pos_shifts = [w_dir_vert, w_dir_horiz] block_center_rel_w_pos = end_w_pos - block_w_pos - WVec(0.5, 0.5) if -w_dir.y * block_center_rel_w_pos.y > -w_dir.x * block_center_rel_w_pos.x: block_w_pos_shifts.reverse() block_w_pos_shift = block_w_pos_shifts[0] if got_block: if block_w_pos + block_w_pos_shift in blocks_map: block_w_pos_shift = block_w_pos_shifts[1] return BlockSelection(block_w_pos, block_w_pos_shift, space_only=False) else: # got space: if block_w_pos + block_w_pos_shift not in blocks_map: block_w_pos_shift = block_w_pos_shifts[1] return BlockSelection(block_w_pos + block_w_pos_shift, -block_w_pos_shift, space_only=True)
def cell_index_to_block_w_pos(self, ij): i, j = ij x = j - 1 y = CHUNK_W_SIZE.y - i w_shift = WVec(x, y) return self._w_shift_to_block_w_pos(w_shift)
def set_transforms(self, pos: WVec, vel: WVec = WVec()): self._pos = WVec(pos) self._req_pos = WVec(self._pos) self._vel = WVec(vel) self._req_vel = WVec(self._vel)
def action_w_pos(self): """Getter for the position from which the player acts upon its environment. """ action_w_pos = WVec(self.pos) action_w_pos.y += self._w_size.y * self._ACTION_POS_RATIO return action_w_pos
import os import pygame as pg from core.classes import PixVec, WVec, WBounds, Dir # ==== TECHNICAL DIMENSIONS ==== PLAYER_S_POS = PixVec(0.5, 0.333) HOTBAR_S_POS = PixVec(0.5, 0.1) HOTBAR_ORIG_PIX_SIZE = PixVec(184, 24) HOTBAR_PIX_SIZE = HOTBAR_ORIG_PIX_SIZE * 4 BLOCK_PIX_SIZE = PixVec( 16, 16) # Should stay equal to block texture size resolution. That is, 16. CHUNK_W_SIZE = WVec(8, 8) CHUNK_PIX_SIZE = BLOCK_PIX_SIZE * CHUNK_W_SIZE WORLD_HEIGHT_BOUNDS = WVec(0, 2**8) BLOCK_BOUND_SHIFTS = WBounds(WVec(0, 0), WVec(1, 1)) # ==== COLORS ==== C_KEY = pg.Color(255, 0, 0) C_BLACK = pg.Color(0, 0, 0) C_WHITE = pg.Color(255, 255, 255) C_SKY = pg.Color(120, 190, 225) # ==== CAM ==== CAM_FPS = 60 CAM_DEFAULT_SCALE = 64.0 CAM_SCALE_BOUNDS = (16.0, 128.0) # ==== GAME DYNAMICS ====
class Player: _ACC = WVec(GRAVITY) _ACTION_POS_RATIO = 0.75 _MAIN_PLAYER_DIR = "steve" def __init__(self, name, spawn_pos): self.name = name self._spawn_pos = WVec(spawn_pos) self.pos = WVec() self._req_pos = WVec() self._vel = WVec() self._req_vel = WVec() self.spawn() self._is_on_ground = False self._w_size = WVec(0.6, 1.8) self._bounds_w_shift = WBounds( min=WVec(-self._w_size.x / 2, 0.0), max=WVec(self._w_size.x / 2, self._w_size.y), ) self._anim_surf_walking = AnimatedSurface( os.path.join(RESOURCES_PATH, self._MAIN_PLAYER_DIR, "walking"), w_height=self._w_size.y, neutrals=(0, 8), ) self._anim_surf_sprinting = AnimatedSurface( os.path.join(RESOURCES_PATH, self._MAIN_PLAYER_DIR, "sprinting"), w_height=self._w_size.y, neutrals=(0, 6), ) self._anim_surf = self._anim_surf_walking self._walking_speed = PLAYER_ABILITY_FACTOR * 4.5 / CAM_FPS self._sprinting_speed = PLAYER_ABILITY_FACTOR * 7.5 / CAM_FPS self._jumping_speed = PLAYER_ABILITY_FACTOR * 7.75 / CAM_FPS # ==== GET DATA ==== def get_bounds(self, w_pos=None) -> WBounds: """Return the boundaries of the Player at its current position, or at w_pos if the argument has been passed. """ if w_pos is None: w_pos = self.pos return get_bounds(w_pos, self._bounds_w_shift) @property def is_dead(self): return self.pos.y < PLAYER_POS_MIN_HEIGHT @property def action_w_pos(self): """Getter for the position from which the player acts upon its environment. """ action_w_pos = WVec(self.pos) action_w_pos.y += self._w_size.y * self._ACTION_POS_RATIO return action_w_pos # ==== DRAW ==== def draw(self, camera, world): sky_light = world.get_sky_light_at_w_pos(self.pos) camera.draw_player(self._anim_surf, self.pos, sky_light) # ==== REQUEST MOVEMENTS ==== def req_move_right(self): self._anim_surf_walking.sync(self._anim_surf) self._anim_surf = self._anim_surf_walking self._anim_surf.action = AnimAction.play self._anim_surf.is_flipped = False self._req_vel.x = self._walking_speed def req_move_left(self): self._anim_surf_walking.sync(self._anim_surf) self._anim_surf = self._anim_surf_walking self._anim_surf.action = AnimAction.play self._anim_surf.is_flipped = True self._req_vel.x = -self._walking_speed def req_sprint_right(self): self._anim_surf_sprinting.sync(self._anim_surf) self._anim_surf = self._anim_surf_sprinting self._anim_surf.action = AnimAction.play self._anim_surf.is_flipped = False self._req_vel.x = self._sprinting_speed def req_sprint_left(self): self._anim_surf_sprinting.sync(self._anim_surf) self._anim_surf = self._anim_surf_sprinting self._anim_surf.action = AnimAction.play self._anim_surf.is_flipped = True self._req_vel.x = -self._sprinting_speed def req_h_move_stop(self): self._anim_surf.action = AnimAction.end self._req_vel.x = 0 def req_v_move_stop(self): self._req_vel.y = 0 def req_jump(self): if self._is_on_ground: self._req_vel.y = self._jumping_speed else: self.req_jump_stop() def req_jump_stop(self): self._req_vel.y = 0 # ==== APPLY MOVEMENTS ==== def set_transforms(self, pos, vel=WVec()): """Set the transforms directly without going through a request. """ self.pos = WVec(pos) self._req_pos = WVec(self.pos) self._vel = WVec(vel) self._req_vel = WVec(self._vel) def spawn(self): """Set or reset the player to its spawning state. """ self.set_transforms(self._spawn_pos, (0.0, -100.0 / CAM_FPS)) def _collide(self, world, threshold=0.001): """Check for collisions with the world and update the transforms accordingly. """ world_colliders = world.get_colliders_around(self.pos, c_radius=1) tested_horiz_pos = (self._req_pos.x, self.pos.y) tested_horiz_pos_bounds = self.get_bounds(tested_horiz_pos) tested_vert_pos = (self.pos.x, self._req_pos.y) tested_vert_pos_bounds = self.get_bounds(tested_vert_pos) for pos_x in range(tested_vert_pos_bounds.min.x, tested_vert_pos_bounds.max.x + 1): if self._vel.y < 0: pos_y = tested_vert_pos_bounds.min.y if (pos_x, pos_y) in world_colliders.up: self._req_pos.y = pos_y + BLOCK_BOUND_SHIFTS.max.y - self._bounds_w_shift.min.y + threshold self._vel.y = 0 self._is_on_ground = True break else: pos_y = tested_vert_pos_bounds.max.y if (pos_x, pos_y) in world_colliders.down: self._req_pos.y = pos_y + BLOCK_BOUND_SHIFTS.min.y - self._bounds_w_shift.max.y - threshold self._vel.y = 0 break for pos_y in range(tested_horiz_pos_bounds.min.y, tested_horiz_pos_bounds.max.y + 1): if self._vel.x <= 0: pos_x = tested_horiz_pos_bounds.min.x if (pos_x, pos_y) in world_colliders.right: self._req_pos.x = pos_x + BLOCK_BOUND_SHIFTS.max.x - self._bounds_w_shift.min.x + threshold self._vel.x = 0 break else: pos_x = tested_horiz_pos_bounds.max.x if (pos_x, pos_y) in world_colliders.left: self._req_pos.x = pos_x + BLOCK_BOUND_SHIFTS.min.x - self._bounds_w_shift.max.x - threshold self._vel.x = 0 break def move(self, world, substeps=1): """Apply requested and physics-induced movements. """ self._vel.x += (self._req_vel.x - self._vel.x) * PLAYER_POS_DAMPING_FACTOR self._vel.y += self._req_vel.y self._vel += self._ACC self._is_on_ground = False # Assumption, to be corrected inside self._collide. collision_steps = (floor(self._vel.norm()) + 1) * substeps for _ in range(collision_steps): self._req_pos = self.pos + self._vel / collision_steps self._collide(world) self.pos = WVec(self._req_pos) # ==== SAVE AND LOAD ==== def load_from_disk(self, dir_path): try: with open(os.path.join(dir_path, f"{self.name}.json")) as file: data = json.load(file) except FileNotFoundError: return LoadResult.no_file self.set_transforms(data["pos"], data["vel"]) self._is_on_ground = data["is_on_ground"] self._anim_surf.is_flipped = data["is_reversed"] return LoadResult.success def save_to_disk(self, dir_path): data = { "pos": tuple(self.pos), "vel": tuple(self._vel), "is_on_ground": self._is_on_ground, "is_reversed": self._anim_surf.is_flipped, } with open(os.path.join(dir_path, f"{self.name}.json"), "w") as file: json.dump(data, file, indent=4)