def pos(self): # Let character keep in the middle of screen pos = Game().role.pos - Vector2(RESOLUTION) / 2 # 使窗口在地图内 win = Rect(pos, RESOLUTION) win.clamp_ip(self.surf.get_rect()) # 变换为窗口坐标系 return -Vector2(win.topleft)
class Camera(object): def __init__(self, target, bounds, size): self.bounds = bounds self.rect = Rect((0, 0), size) def update(self, target): self.rect.center = target.center self.rect.clamp_ip(self.bounds) def draw_background(self, surf, bg): surf.blit(bg, (-self.rect.x, -self.rect.y)) def draw_sprite(self, surf, sprite): if self.rect.colliderect(sprite.rect): surf.blit(sprite.image, rel_rect(sprite.rect, self.rect))
class Camera(object): def __init__(self, target, bounds, size): self.bounds = bounds self.rect = Rect((0,0), size) def update(self, target): self.rect.center = target.center self.rect.clamp_ip(self.bounds) def draw_background(self, surf, bg): surf.blit(bg, (-self.rect.x, -self.rect.y)) def draw_sprite(self, surf, sprite): if self.rect.colliderect(sprite.rect): surf.blit(sprite.image, rel_rect(sprite.rect, self.rect))
class Viewport: def __init__(self): self.viewport = Rect((0, 0), SCREEN_SIZE) self.locked = False self.destination = None self.speed = 0 self.on_arrival = None def center(self, center, inside): if not self.locked: self.viewport.center = center self.viewport.clamp_ip(inside) def get(self): return self.viewport def lock(self): self.locked = True def unlock(self): self.locked = False def move_to(self, destination, speed=100, on_arrival=None): self.destination = destination self.speed = speed self.on_arrival = on_arrival def move(self, time_passed): if self.destination: location = Vector(*self.viewport.topleft) vec_to_destination = self.destination - location distance_to_destination = vec_to_destination.get_magnitude() vec_to_destination.normalize() travel_distance = min(distance_to_destination, time_passed * self.speed) vec_to_destination.x *= travel_distance vec_to_destination.y *= travel_distance move = location + vec_to_destination self.viewport.topleft = move.x, move.y if self.destination == move: self.destination = None self.on_arrival() self.on_arrival = None
def collision_check(self): ''' Method for checking if the entity has run into a tree or something and move it back a pixel if it has ''' if self.wall_collides: # Move the entity inside of the window (border collision) entity_rect = Rect(self.x, self.y, self.width,self.height) window_rect = Rect(0, 0, g.width * c.TILE_SIZE, g.height * c.TILE_SIZE) if not window_rect.contains(entity_rect): entity_rect.clamp_ip(window_rect) self.x = entity_rect.left self.y = entity_rect.top if self.collides: # Make sure collision rectangles are up to date self.update_collision_rects() # Get the tile the entity is standing on tile_pos = self.get_tile() checked_tiles = [] # Loop through a 3x3 tile square around the entity, to not check the entire map for i in range(tile_pos[0] - 1, tile_pos[0] + 2): for j in range(tile_pos[1] - 1, tile_pos[1] + 2): try: if c.IMAGES[g.map[i][j].type].collides: checked_tiles.append(g.map[i][j].rect()) except IndexError: # That index was apparently outside of the map pass except: raise # Check if each of the zones collides with any of the tiles if self.col_left.collidelist(checked_tiles) != -1: self.x += 1 if self.col_right.collidelist(checked_tiles) != -1: self.x -= 1 if self.col_bottom.collidelist(checked_tiles) != -1: self.y -= 1 if self.col_top.collidelist(checked_tiles) != -1: self.y += 1
def collision_check(self): """ Method for checking if the entity has run into a tree or something and move it back a pixel if it has """ if self.wall_collides: # Move the entity inside of the window (border collision) entity_rect = Rect(self.x, self.y, self.width,self.height) window_rect = Rect(0, 0, g.width * c.TILE_SIZE, g.height * c.TILE_SIZE) if not window_rect.contains(entity_rect): entity_rect.clamp_ip(window_rect) self.x = entity_rect.left self.y = entity_rect.top collided = False if self.collides: # Make sure collision rectangles are up to date self.update_collision_rects() # Get the tile the entity is standing on tile_pos = self.get_tile() checked_tiles = [] # Loop through a 3x3 tile square around the entity, to not check the entire map for i in range(tile_pos[0] - 1, tile_pos[0] + 2): for j in range(tile_pos[1] - 1, tile_pos[1] + 2): try: if c.IMAGES[g.map[i][j].type].collides: checked_tiles.append(g.map[i][j].rect()) except IndexError: # That index was apparently outside of the map pass # Check if each of the zones collides with any of the tiles # If so, move it in the appropriate direction, specified in update_collision_rects() as the keys for rect in self.rects: if self.rects[rect].collidelist(checked_tiles) != -1: self.x += rect[0] self.y += rect[1] collided = True self.collided = collided
class Camera(object): def __init__(self, target, bounds, size): self.bounds = bounds self.rect = Rect((0,0), size) def update(self, target): self.rect.center = target.center self.rect.clamp_ip(self.bounds) def draw_background(self, surf, bg): surf.blit(bg, (-self.rect.x, -self.rect.y)) def draw_background_alpha(self, surf, bg, alpha): new_bg = bg.copy() new_bg.set_alpha(alpha) surf.blit(new_bg, (-self.rect.x, -self.rect.y)) def draw_sprite(self, surf, sprite): if self.rect.colliderect(sprite.rect): surf.blit(sprite.image, rel_rect(sprite.rect, self.rect)) def draw_sprite_group(self, surf, group): for sprite in group: if self.rect.colliderect(sprite.rect): surf.blit(sprite.image, rel_rect(sprite.rect, self.rect)) def draw_sprite_group_alpha(self, surf, group, alpha): for sprite in group: if self.rect.colliderect(sprite.rect): new_sprite = sprite.image.copy() new_sprite.set_alpha(alpha) surf.blit(new_sprite, rel_rect(sprite.rect, self.rect)) def draw_sprite_alpha(self, surf, sprite, alpha): if self.rect.colliderect(sprite.rect): new_sprite = sprite.image.copy() new_sprite.set_alpha(alpha) surf.blit(new_sprite, rel_rect(sprite.rect, self.rect))
def test_clamp_ip( self ): r = Rect(10, 10, 10, 10) c = Rect(19, 12, 5, 5) c.clamp_ip(r) self.assertEqual(c.right, r.right) self.assertEqual(c.top, 12) c = Rect(1, 2, 3, 4) c.clamp_ip(r) self.assertEqual(c.topleft, r.topleft) c = Rect(5, 500, 22, 33) c.clamp_ip(r) self.assertEqual(c.center, r.center)
def test_clamp_ip(self): r = Rect(10, 10, 10, 10) c = Rect(19, 12, 5, 5) c.clamp_ip(r) self.assertEqual(c.right, r.right) self.assertEqual(c.top, 12) c = Rect(1, 2, 3, 4) c.clamp_ip(r) self.assertEqual(c.topleft, r.topleft) c = Rect(5, 500, 22, 33) c.clamp_ip(r) self.assertEqual(c.center, r.center)
class Menu(object): """popup_menu.Menu Menu(pos, name, items) : return menu pos -> (x,y); topleft coordinates of the menu. name -> str; the name of the menu. items -> list; a list containing strings for menu items labels. This class is not intended to be used directly. Use PopupMenu or NonBlockingPopupMenu instead, unless designing your own subclass. """ def __init__(self, pos, name, items, gameController): self.Game = gameController screen = pygame.display.get_surface() screen_rect = screen.get_rect() self.name = name self.items = [] self.menu_item = None # Make the frame rect x, y = pos self.rect = Rect(x, y, 0, 0) self.rect.width += margin * 2 self.rect.height += margin * 2 # Make the title image and rect, and grow the frame rect TitleFont = pygame.font.SysFont(None, 20) TitleFont.bold = True self.title_image = TitleFont.render(name, True, text_color) self.title_rect = self.title_image.get_rect(topleft=(x + margin, y + margin)) self.rect.width = margin * 2 + self.title_rect.width self.rect.height = margin + self.title_rect.height # Make the item highlight rect self.hi_rect = Rect(0, 0, 0, 0) # Make menu items n = 0 for item in items: menu_item = MenuItem(item, n) self.items.append(menu_item) self.rect.width = max(self.rect.width, menu_item.rect.width + margin * 2) self.rect.height += menu_item.rect.height + margin n += 1 self.rect.height += margin # Position menu fully within view if not screen_rect.contains(self.rect): savex, savey = self.rect.topleft self.rect.clamp_ip(screen_rect) self.title_rect.top = self.rect.top + margin self.title_rect.left = self.rect.left + margin # Position menu items within menu frame y = self.title_rect.bottom + margin for item in self.items: item.rect.x = self.rect.x + margin item.rect.y = y y = item.rect.bottom + margin item.rect.width = self.rect.width - margin * 2 # Calculate highlight rect's left-alignment and size self.hi_rect.left = menu_item.rect.left self.hi_rect.width = self.rect.width - margin * 2 self.hi_rect.height = menu_item.rect.height # Create the menu frame and highlight frame images self.bg_image = pygame.transform.scale(HUDConf.PLAYER_ICON_SLOT, self.rect.size) self.hi_image = pygame.surface.Surface(self.hi_rect.size) # self.bg_image.fill(bg_color) self.hi_image.fill(hi_color) # Draw menu border rect = self.bg_image.get_rect() # pygame.draw.rect(self.bg_image, glint_color, rect, 1) t, l, b, r = rect.top, rect.left, rect.bottom, rect.right # pygame.draw.line(self.bg_image, shadow_color, (l, b - 1), (r, b - 1), 1) # pygame.draw.line(self.bg_image, shadow_color, (r - 1, t), (r - 1, b), 1) # Draw title divider in menu frame left = margin right = self.rect.width - margin * 2 y = self.title_rect.height + 1 # pygame.draw.line(self.bg_image, shadow_color, (left, y), (right, y)) def draw(self): # Draw the menu on the main display. self.Game.screen.blit(self.bg_image, self.rect) self.Game.screen.blit(self.title_image, self.title_rect) for item in self.items: if item is self.menu_item: self.hi_rect.top = item.rect.top self.Game.screen.blit(self.hi_image, self.hi_rect) self.Game.screen.blit(item.image, item.rect) def check_collision(self, mouse_pos): # Set self.menu_item if the mouse is hovering over one. self.menu_item = None if self.rect.collidepoint(mouse_pos): for item in self.items: if item.rect.collidepoint(mouse_pos): self.menu_item = item break
class BufferedRenderer(object): """ Renderer that support scrolling, zooming, layers, and animated tiles The buffered renderer must be used with a data class to get tile, shape, and animation information. See the data class api in pyscroll.data, or use the built-in pytmx support for loading maps created with Tiled. """ def __init__(self, data, size, clamp_camera=True, colorkey=None, alpha=False, time_source=time.time, scaling_function=pygame.transform.scale): # default options self.map_rect = None # pygame rect of entire map self.data = data # reference to data source self.clamp_camera = clamp_camera # if clamped, cannot scroll past map edge self.time_source = time_source # determines how tile animations are processed self.scaling_function = scaling_function # what function to use when zooming self.default_shape_texture_gid = 1 # [experimental] texture to draw shapes with self.default_shape_color = 0, 255, 0 # [experimental] color to fill polygons with # internal private defaults self._alpha = False if colorkey and alpha: print('cannot select both colorkey and alpha. choose one.') raise ValueError elif colorkey: self._clear_color = colorkey else: self._clear_color = None # private attributes self._size = None # size that the camera/viewport is on screen, kinda self._redraw_cutoff = None # size of dirty tile edge that will trigger full redraw self._x_offset = None # offsets are used to scroll map in sub-tile increments self._y_offset = None self._buffer = None # complete rendering of tilemap self._tile_view = None # this rect represents each tile on the buffer self._half_width = None # 'half x' attributes are used to reduce division ops. self._half_height = None self._tile_queue = None # tiles queued to be draw onto buffer self._animation_queue = None # heap queue of animation token. schedules tile changes self._animation_map = None # map of GID to other GIDs in an animation self._last_time = None # used for scheduling animations self._layer_quadtree = None # used to draw tiles that overlap optional surfaces self._zoom_buffer = None # used to speed up zoom operations self._zoom_level = 1.0 # negative numbers make map smaller, positive: bigger # this represents the viewable pixels, aka 'camera' self.view_rect = Rect(0, 0, 0, 0) self.reload_animations() self.set_size(size) def _update_time(self): self._last_time = time.time() * 1000 def reload_animations(self): """ Reload animation information """ self._update_time() self._animation_map = dict() self._animation_queue = list() for gid, frame_data in self.data.get_animations(): frames = list() for frame_gid, frame_duration in frame_data: image = self.data.get_tile_image_by_gid(frame_gid) frames.append(AnimationFrame(image, frame_duration)) ani = AnimationToken(gid, frames) ani.next += self._last_time self._animation_map[ani.gid] = ani.frames[ani.index].image heappush(self._animation_queue, ani) def _process_animation_queue(self): self._update_time() requires_redraw = False # test if the next scheduled tile change is ready while self._animation_queue[0].next <= self._last_time: requires_redraw = True token = heappop(self._animation_queue) # advance the animation index, looping by default if token.index == len(token.frames) - 1: token.index = 0 else: token.index += 1 next_frame = token.frames[token.index] token.next = next_frame.duration + self._last_time self._animation_map[token.gid] = next_frame.image heappush(self._animation_queue, token) if requires_redraw: # TODO: record the tiles that changed and update only affected tiles self.redraw_tiles() pass def _calculate_zoom_buffer_size(self, value): if value <= 0: print('zoom level cannot be zero or less') raise ValueError value = 1.0 / value return [int(round(i * value)) for i in self._size] @property def zoom(self): """ Zoom the map in or out. Increase this number to make map appear to come closer to camera. Decrease this number to make map appear to move away from camera. Default value is 1.0 This value cannot be negative or 0.0 :return: float """ return self._zoom_level @zoom.setter def zoom(self, value): self._zoom_level = value buffer_size = self._calculate_zoom_buffer_size(value) self._initialize_buffers(buffer_size) def set_size(self, size): """ Set the size of the map in pixels This is an expensive operation, do only when absolutely needed. :param size: (width, height) pixel size of camera/view of the group """ self._size = size buffer_size = self._calculate_zoom_buffer_size(self._zoom_level) self._initialize_buffers(buffer_size) def _create_buffers(self, view_size, buffer_size): """ Create the buffers, taking in account pixel alpha or colorkey :param view_size: pixel size of the view :param buffer_size: pixel size of the buffer """ requires_zoom_buffer = not view_size == buffer_size self._zoom_buffer = None if self._clear_color: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=pygame.RLEACCEL) self._zoom_buffer.set_colorkey(self._clear_color) self._buffer = Surface(buffer_size, flags=pygame.RLEACCEL) self._buffer.set_colorkey(self._clear_color) self._buffer.fill(self._clear_color) elif self._alpha: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=pygame.SRCALPHA) self._buffer = Surface(buffer_size, flags=pygame.SRCALPHA) else: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size) self._buffer = Surface(buffer_size) def _initialize_buffers(self, size): """ Create the buffers to cache tile drawing :param size: (int, int): size of the draw area :return: None """ tw, th = self.data.tile_size mw, mh = self.data.map_size buffer_tile_width = int(math.ceil(size[0] / tw) + 2) buffer_tile_height = int(math.ceil(size[1] / th) + 2) buffer_pixel_size = buffer_tile_width * tw, buffer_tile_height * th self.map_rect = Rect(0, 0, mw * tw, mh * th) self.view_rect.size = size self._tile_view = Rect(0, 0, buffer_tile_width, buffer_tile_height) self._redraw_cutoff = min(buffer_tile_width, buffer_tile_height) self._create_buffers(size, buffer_pixel_size) self._half_width = size[0] // 2 self._half_height = size[1] // 2 self._x_offset = 0 self._y_offset = 0 def make_rect(x, y): return Rect((x * tw, y * th), (tw, th)) rects = [make_rect(*i) for i in product(range(buffer_tile_width), range(buffer_tile_height))] # TODO: figure out what depth -actually- does self._layer_quadtree = quadtree.FastQuadTree(rects, 4) self.redraw_tiles() def scroll(self, vector): """ scroll the background in pixels :param vector: (int, int) """ self.center((vector[0] + self.view_rect.centerx, vector[1] + self.view_rect.centery)) def center(self, coords): """ center the map on a pixel float numbers will be rounded. :param coords: (number, number) """ x, y = [round(i, 0) for i in coords] self.view_rect.center = x, y if self.clamp_camera: self.view_rect.clamp_ip(self.map_rect) x, y = self.view_rect.center # calc the new position in tiles and offset tw, th = self.data.tile_size left, self._x_offset = divmod(x - self._half_width, tw) top, self._y_offset = divmod(y - self._half_height, th) # adjust the view if the view has changed without a redraw dx = int(left - self._tile_view.left) dy = int(top - self._tile_view.top) view_change = max(abs(dx), abs(dy)) if view_change <= self._redraw_cutoff: self._buffer.scroll(-dx * tw, -dy * th) self._tile_view.move_ip((dx, dy)) self._queue_edge_tiles(dx, dy) self._flush_tile_queue() elif view_change > self._redraw_cutoff: logger.info('scrolling too quickly. redraw forced') self._tile_view.move_ip((dx, dy)) self.redraw_tiles() def _queue_edge_tiles(self, dx, dy): """ Queue edge tiles and clear edge areas on buffer if needed :param dx: Edge along X axis to enqueue :param dy: Edge along Y axis to enqueue :return: None """ v = self._tile_view fill = partial(self._buffer.fill, self._clear_color) tw, th = self.data.tile_size self._tile_queue = iter([]) def append(rect): self._tile_queue = chain(self._tile_queue, self.data.get_tile_images_by_rect(rect)) if self._clear_color: fill(((rect[0] - self._tile_view.left) * tw, (rect[1] - self._tile_view.top) * th, rect[2] * tw, rect[3] * th)) if dx > 0: # right side append((v.right - dx, v.top, dx, v.height)) elif dx < 0: # left side append((v.left, v.top, -dx, v.height)) if dy > 0: # bottom side append((v.left, v.bottom - dy, v.width, dy)) elif dy < 0: # top side append((v.left, v.top, v.width, -dy)) def draw(self, surface, rect, surfaces=None): """ Draw the map onto a surface pass a rect that defines the draw area for: drawing to an area smaller that the whole window/screen surfaces may optionally be passed that will be blitted onto the surface. this must be a list of tuples containing a layer number, image, and rect in screen coordinates. surfaces will be drawn in order passed, and will be correctly drawn with tiles from a higher layer overlapping the surface. surfaces list should be in the following format: [ (layer, surface, rect), ... ] :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace into tiles """ if self._zoom_level == 1.0: self._render_map(surface, rect, surfaces) else: self._render_map(self._zoom_buffer, self._zoom_buffer.get_rect(), surfaces) self.scaling_function(self._zoom_buffer, rect.size, surface) def _render_map(self, surface, rect, surfaces): """ Render the map and optional surfaces to destination surface :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace into tiles """ if self._animation_queue: self._process_animation_queue() if rect.width > self.map_rect.width: x = (rect.width - self.map_rect.width) // 4 print(x) self._x_offset += x if rect.height > self.map_rect.height: pass # need to set clipping otherwise the map will draw outside its area with surface_clipping_context(surface, rect): # draw the entire map to the surface taking in account the scrolling offset surface.blit(self._buffer, (-self._x_offset - rect.left, -self._y_offset - rect.top)) if surfaces: self._draw_surfaces(surface, rect, surfaces) def _draw_surfaces(self, surface, rect, surfaces): """ Draw surfaces onto map, then redraw tiles that cover them :param surface: destination :param rect: clipping area :param surfaces: sequence of surfaces to blit """ surface_blit = surface.blit left, top = self._tile_view.topleft ox = self._x_offset - rect.left oy = self._y_offset - rect.top hit = self._layer_quadtree.hit get_tile = self.data.get_tile_image tile_layers = tuple(self.data.visible_tile_layers) dirty = [(surface_blit(i[0], i[1]), i[2]) for i in surfaces] for dirty_rect, layer in dirty: for r in hit(dirty_rect.move(ox, oy)): x, y, tw, th = r for l in [i for i in tile_layers if gt(i, layer)]: tile = get_tile((x // tw + left, y // th + top, l)) if tile: surface_blit(tile, (x - ox, y - oy)) def _draw_objects(self): """ Totally unoptimized drawing of objects to the map [probably broken] """ tw, th = self.data.tile_size buff = self._buffer blit = buff.blit map_gid = self.data.tmx.map_gid default_color = self.default_shape_color get_image_by_gid = self.data.get_tile_image_by_gid _draw_textured_poly = pygame.gfxdraw.textured_polygon _draw_poly = pygame.draw.polygon _draw_lines = pygame.draw.lines ox = self._tile_view.left * tw oy = self._tile_view.top * th def draw_textured_poly(texture, points): try: _draw_textured_poly(buff, points, texture, tw, th) except pygame.error: pass def draw_poly(color, points, width=0): _draw_poly(buff, color, points, width) def draw_lines(color, points, width=2): _draw_lines(buff, color, False, points, width) def to_buffer(pt): return pt[0] - ox, pt[1] - oy for layer in self.data.visible_object_layers: for o in (o for o in layer if o.visible): texture_gid = getattr(o, "texture", None) color = getattr(o, "color", default_color) # BUG: this is not going to be completely accurate, because it # does not take into account times where texture is flipped. if texture_gid: texture_gid = map_gid(texture_gid)[0][0] texture = get_image_by_gid(int(texture_gid)) if hasattr(o, 'points'): points = [to_buffer(i) for i in o.points] if o.closed: if texture_gid: draw_textured_poly(texture, points) else: draw_poly(color, points) else: draw_lines(color, points) elif o.gid: tile = get_image_by_gid(o.gid) if tile: pt = to_buffer((o.x, o.y)) blit(tile, pt) else: x, y = to_buffer((o.x, o.y)) points = ((x, y), (x + o.width, y), (x + o.width, y + o.height), (x, y + o.height)) if texture_gid: draw_textured_poly(texture, points) else: draw_poly(color, points) def _flush_tile_queue(self): """ Blit the queued tiles and block until the tile queue is empty """ tw, th = self.data.tile_size ltw = self._tile_view.left * tw tth = self._tile_view.top * th blit = self._buffer.blit map_get = self._animation_map.get for x, y, l, tile, gid in self._tile_queue: blit(map_get(gid, tile), (x * tw - ltw, y * th - tth)) def redraw_tiles(self): """ redraw the visible portion of the buffer -- it is slow. """ if self._clear_color: self._buffer.fill(self._clear_color) elif self._alpha: self._buffer.fill(0) self._tile_queue = self.data.get_tile_images_by_rect(self._tile_view) self._flush_tile_queue() def get_center_offset(self): """ Return x, y pair that will change world coords to screen coords :return: int, int """ return (-self.view_rect.centerx + self._half_width, -self.view_rect.centery + self._half_height)
class Menu(object): """popup_menu.Menu Menu(pos, name, items) : return menu pos -> (x,y); topleft coordinates of the menu. name -> str; the name of the menu. items -> list; a list containing strings for menu items labels. This class is not intended to be used directly. Use PopupMenu or NonBlockingPopupMenu instead, unless designing your own subclass. """ def __init__(self, pos, name, items): screen = pygame.display.get_surface() screen_rect = screen.get_rect() self.name = name self.items = [] self.menu_item = None # Make the frame rect x,y = pos self.rect = Rect(x,y,0,0) self.rect.width += margin * 2 self.rect.height += margin * 2 # Make the title image and rect, and grow the frame rect self.title_image = font.render(name, True, text_color) self.title_rect = self.title_image.get_rect(topleft=(x+margin,y+margin)) self.rect.width = margin*2 + self.title_rect.width self.rect.height = margin + self.title_rect.height # Make the item highlight rect self.hi_rect = Rect(0,0,0,0) # Make menu items n = 0 for item in items: menu_item = MenuItem(item, n) self.items.append(menu_item) self.rect.width = max(self.rect.width, menu_item.rect.width+margin*2) self.rect.height += menu_item.rect.height + margin n += 1 self.rect.height += margin # Position menu fully within view if not screen_rect.contains(self.rect): savex,savey = self.rect.topleft self.rect.clamp_ip(screen_rect) self.title_rect.top = self.rect.top + margin self.title_rect.left = self.rect.left + margin # Position menu items within menu frame y = self.title_rect.bottom + margin for item in self.items: item.rect.x = self.rect.x + margin item.rect.y = y y = item.rect.bottom + margin item.rect.width = self.rect.width - margin*2 # Calculate highlight rect's left-alignment and size self.hi_rect.left = menu_item.rect.left self.hi_rect.width = self.rect.width - margin*2 self.hi_rect.height = menu_item.rect.height # Create the menu frame and highlight frame images self.bg_image = pygame.surface.Surface(self.rect.size) self.hi_image = pygame.surface.Surface(self.hi_rect.size) self.bg_image.fill(bg_color) self.hi_image.fill(hi_color) # Draw menu border rect = self.bg_image.get_rect() pygame.draw.rect(self.bg_image, glint_color, rect, 1) t,l,b,r = rect.top,rect.left,rect.bottom,rect.right pygame.draw.line(self.bg_image, shadow_color, (l,b-1), (r,b-1), 1) pygame.draw.line(self.bg_image, shadow_color, (r-1,t), (r-1,b), 1) # Draw title divider in menu frame left = margin right = self.rect.width - margin*2 y = self.title_rect.height + 1 pygame.draw.line(self.bg_image, shadow_color, (left,y), (right,y)) def draw(self): # Draw the menu on the main display. screen = pygame.display.get_surface() screen.blit(self.bg_image, self.rect) screen.blit(self.title_image, self.title_rect) for item in self.items: if item is self.menu_item: self.hi_rect.top = item.rect.top screen.blit(self.hi_image, self.hi_rect) screen.blit(item.image, item.rect) def check_collision(self, mouse_pos): # Set self.menu_item if the mouse is hovering over one. self.menu_item = None if self.rect.collidepoint(mouse_pos): for item in self.items: if item.rect.collidepoint(mouse_pos): self.menu_item = item break
scale /= 1.1 else: redraw = False if redraw: if scale < screen_size[0]/env_size[0]: scale = screen_size[0]/env_size[0] if scale > 16000/env_size[0]: scale = 16000/env_size[0] size = (int(env_size[0]*scale), int(env_size[1]*scale)) background = Surface(size) pygame.draw.rect(background, (255,255,255), Rect((0,0), size)) tree.draw(background, pygame.font.SysFont("Calibri", int(11*scale)), size, scale) mouse_pose = pygame.mouse.get_pos() background_mouse_pose = (mouse_pose[0]-background_dimensions.left, mouse_pose[1]-background_dimensions.top) current_scale = scale/prev_scale background_dimensions.move_ip(int(-background_mouse_pose[0]*(current_scale-1)), int(-background_mouse_pose[1]*(current_scale-1))) background_dimensions.size = size x = background_dimensions.width-2000 y = background_dimensions.height-750 background_dimensions.clamp_ip(Rect(-x, -y, 2000+2*x, 750+2*y)) screen.blit(background, background_dimensions) pygame.display.update() prev_scale = scale pygame.quit()
class BufferedRenderer: """ Renderer that support scrolling, zooming, layers, and animated tiles The buffered renderer must be used with a data class to get tile, shape, and animation information. See the data class api in pyscroll.data, or use the built-in pytmx support for loading maps created with Tiled. NOTE: colorkey and alpha transparency is quite slow """ _rgba_clear_color = 0, 0, 0, 0 _rgb_clear_color = 0, 0, 0 def __init__(self, data, size, clamp_camera=True, colorkey=None, alpha=False, time_source=time.time, scaling_function=transform.scale, tall_sprites=0, **kwargs): bg_fill = kwargs.get("background_color") if bg_fill: self._rgb_clear_color = bg_fill self._rgba_clear_color = bg_fill # default options self.data = data # reference to data source self.clamp_camera = clamp_camera # if true, cannot scroll past map edge self.time_source = time_source # determines how tile animations are processed self.scaling_function = scaling_function # what function to use when scaling the zoom buffer self.tall_sprites = tall_sprites # correctly render tall sprites self.map_rect = None # pygame rect of entire map # Tall Spritesthat's # this value, if greater than 0, is the number of pixels from the bottom of # tall sprites which is compared against the bottom of a tile on the same # layer of the sprite. In other words, if set, it prevents tiles from being # drawn over sprites which are taller than the tile height. The value that # is how far apart the sprites have to be before drawing the tile over. # Reasonable values are about 10% of the tile height # This feature only works for the first layer over the tall sprite, all # other layers will be drawn over the tall sprite. # internal private defaults if colorkey and alpha: LOG.error("cannot select both colorkey and alpha. choose one.") raise ValueError elif colorkey: self._clear_color = colorkey elif alpha: self._clear_color = self._rgba_clear_color else: self._clear_color = None # private attributes self._anchored_view = True # if true, map is fixed to upper left corner self._previous_blit = None # rect of the previous map blit when map edges are visible self._size = None # actual pixel size of the view, as it occupies the screen self._redraw_cutoff = None # size of dirty tile edge that will trigger full redraw self._x_offset = None # offsets are used to scroll map in sub-tile increments self._y_offset = None self._buffer = None # complete rendering of tilemap self._tile_view = None # this rect represents each tile on the buffer self._half_width = None # 'half x' attributes are used to reduce division ops. self._half_height = None self._tile_queue = None # tiles queued to be draw onto buffer self._animation_queue = None # heap queue of animation token; schedules tile changes self._layer_quadtree = None # used to draw tiles that overlap optional surfaces self._zoom_buffer = None # used to speed up zoom operations self._zoom_level = 1.0 # negative numbers make map smaller, positive: bigger self._real_ratio_x = 1.0 # zooming slightly changes aspect ratio; this compensates self._real_ratio_y = 1.0 # zooming slightly changes aspect ratio; this compensates self.view_rect = Rect(0, 0, 0, 0) # this represents the viewable map pixels if hasattr(Surface, "blits"): self._flush_tile_queue = self._flush_tile_queue_blits self.set_size(size) def scroll(self, vector): """scroll the background in pixels :param vector: (int, int) """ self.center((vector[0] + self.view_rect.centerx, vector[1] + self.view_rect.centery)) def center(self, coords): """center the map on a pixel float numbers will be rounded. :param coords: (number, number) """ x, y = round(coords[0]), round(coords[1]) self.view_rect.center = x, y mw, mh = self.data.map_size tw, th = self.data.tile_size vw, vh = self._tile_view.size # prevent camera from exposing edges of the map if self.clamp_camera: self._anchored_view = True self.view_rect.clamp_ip(self.map_rect) x, y = self.view_rect.center # calc the new position in tiles and pixel offset left, self._x_offset = divmod(x - self._half_width, tw) top, self._y_offset = divmod(y - self._half_height, th) right = left + vw bottom = top + vh if not self.clamp_camera: # not anchored, so the rendered map is being offset by values larger # than the tile size. this occurs when the edges of the map are inside # the screen. a situation like is shows a background under the map. self._anchored_view = True dx = int(left - self._tile_view.left) dy = int(top - self._tile_view.top) if mw < vw or left < 0: left = 0 self._x_offset = x - self._half_width self._anchored_view = False elif right > mw: left = mw - vw self._x_offset += dx * tw self._anchored_view = False if mh < vh or top < 0: top = 0 self._y_offset = y - self._half_height self._anchored_view = False elif bottom > mh: top = mh - vh self._y_offset += dy * th self._anchored_view = False # adjust the view if the view has changed without a redraw dx = int(left - self._tile_view.left) dy = int(top - self._tile_view.top) view_change = max(abs(dx), abs(dy)) if view_change and (view_change <= self._redraw_cutoff): self._buffer.scroll(-dx * tw, -dy * th) self._tile_view.move_ip(dx, dy) self._queue_edge_tiles(dx, dy) self._flush_tile_queue(self._buffer) elif view_change > self._redraw_cutoff: LOG.info("scrolling too quickly. redraw forced") self._tile_view.move_ip(dx, dy) self.redraw_tiles(self._buffer) def draw(self, surface, rect, surfaces=None): """Draw the map onto a surface pass a rect that defines the draw area for: drawing to an area smaller that the whole window/screen surfaces may optionally be passed that will be blitted onto the surface. this must be a sequence of tuples containing a layer number, image, and rect in screen coordinates. surfaces will be drawn in order passed, and will be correctly drawn with tiles from a higher layer overlapping the surface. surfaces list should be in the following format: [ (layer, surface, rect), ... ] or this: [ (layer, surface, rect, blendmode_flags), ... ] :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace between tiles :return rect: area that was drawn over """ if self._zoom_level == 1.0: self._render_map(surface, rect, surfaces) else: self._render_map(self._zoom_buffer, self._zoom_buffer.get_rect(), surfaces) self.scaling_function(self._zoom_buffer, rect.size, surface) return self._previous_blit.copy() @property def zoom(self): """Zoom the map in or out. Increase this number to make map appear to come closer to camera. Decrease this number to make map appear to move away from camera. Default value is 1.0 This value cannot be negative or 0.0 :return: float """ return self._zoom_level @zoom.setter def zoom(self, value): zoom_buffer_size = self._calculate_zoom_buffer_size(self._size, value) self._zoom_level = value self._initialize_buffers(zoom_buffer_size) zoom_buffer_size = self._zoom_buffer.get_size() self._real_ratio_x = float(self._size[0]) / zoom_buffer_size[0] self._real_ratio_y = float(self._size[1]) / zoom_buffer_size[1] def set_size(self, size): """Set the size of the map in pixels This is an expensive operation, do only when absolutely needed. :param size: (width, height) pixel size of camera/view of the group """ buffer_size = self._calculate_zoom_buffer_size(size, self._zoom_level) self._size = size self._initialize_buffers(buffer_size) def redraw_tiles(self, surface): """Redraw the visible portion of the buffer -- this is slow.""" # TODO/BUG: Redraw animated tiles correctly. They are getting reset here LOG.debug("pyscroll buffer redraw") self._clear_surface(self._buffer) self._tile_queue = self.data.get_tile_images_by_rect(self._tile_view) self._flush_tile_queue(surface) def get_center_offset(self): """Return x, y pair that will change world coords to screen coords :return: int, int """ return (-self.view_rect.centerx + self._half_width, -self.view_rect.centery + self._half_height) def translate_point(self, point): """ Translate world coordinates and return screen coordinates. Respects zoom level. Will be returned as tuple. :rtype: tuple """ mx, my = self.get_center_offset() if self._zoom_level == 1.0: return point[0] + mx, point[1] + my else: return int(round((point[0] + mx)) * self._real_ratio_x), int( round((point[1] + my) * self._real_ratio_y)) def translate_rect(self, rect): """Translate rect position and size to screen coordinates. Respects zoom level. :rtype: Rect """ mx, my = self.get_center_offset() rx = self._real_ratio_x ry = self._real_ratio_y x, y, w, h = rect if self._zoom_level == 1.0: return Rect(x + mx, y + my, w, h) else: return Rect(round((x + mx) * rx), round((y + my) * ry), round(w * rx), round(h * ry)) def translate_points(self, points): """Translate coordinates and return screen coordinates Will be returned in order passed as tuples. :return: list """ retval = [] append = retval.append sx, sy = self.get_center_offset() if self._zoom_level == 1.0: for c in points: append((c[0] + sx, c[1] + sy)) else: rx = self._real_ratio_x ry = self._real_ratio_y for c in points: append((int(round( (c[0] + sx) * rx)), int(round((c[1] + sy) * ry)))) return retval def translate_rects(self, rects): """Translate rect position and size to screen coordinates. Respects zoom level. Will be returned in order passed as Rects. :return: list """ retval = [] append = retval.append sx, sy = self.get_center_offset() if self._zoom_level == 1.0: for r in rects: x, y, w, h = r append(Rect(x + sx, y + sy, w, h)) else: rx = self._real_ratio_x ry = self._real_ratio_y for r in rects: x, y, w, h = r append( Rect(round((x + sx) * rx), round((y + sy) * ry), round(w * rx), round(h * ry))) return retval def _render_map(self, surface, rect, surfaces): """Render the map and optional surfaces to destination surface :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace between tiles """ self._tile_queue = self.data.process_animation_queue(self._tile_view) self._tile_queue and self._flush_tile_queue(self._buffer) # TODO: could maybe optimize to remove just the edges, ideally by drawing lines # if not self.anchored_view: # surface.fill(self._clear_color, self._previous_blit) if not self._anchored_view: self._clear_surface(surface, self._previous_blit) offset = -self._x_offset + rect.left, -self._y_offset + rect.top with surface_clipping_context(surface, rect): self._previous_blit = surface.blit(self._buffer, offset) if surfaces: surfaces_offset = -offset[0], -offset[1] self._draw_surfaces(surface, surfaces_offset, surfaces) def _clear_surface(self, surface, rect=None): """Clear the buffer, taking in account colorkey or alpha :return: """ clear_color = self._rgb_clear_color if self._clear_color is None else self._clear_color surface.fill(clear_color, rect) def _draw_surfaces(self, surface, offset, surfaces): """Draw surfaces onto buffer, then redraw tiles that cover them :param surface: destination :param offset: offset to compensate for buffer alignment :param surfaces: sequence of surfaces to blit """ surface_blit = surface.blit ox, oy = offset left, top = self._tile_view.topleft hit = self._layer_quadtree.hit get_tile = self.data.get_tile_image tile_layers = tuple(self.data.visible_tile_layers) dirty = [] dirty_append = dirty.append # TODO: check to avoid sorting overhead # sort layers, then the y value def sprite_sort(i): return i[2], i[1][1] + i[0].get_height() surfaces.sort(key=sprite_sort) layer_getter = itemgetter(2) for layer, group in groupby(surfaces, layer_getter): del dirty[:] for i in group: try: flags = i[3] except IndexError: dirty_append(surface_blit(i[0], i[1])) else: dirty_append(surface_blit(i[0], i[1], None, flags)) # TODO: make set of covered tiles, in the case where a cluster # of sprite surfaces causes excessive over tile overdrawing for dirty_rect in dirty: for r in hit(dirty_rect.move(ox, oy)): x, y, tw, th = r for l in [i for i in tile_layers if gt(i, layer)]: if self.tall_sprites and l == layer + 1: if y - oy + th <= dirty_rect.bottom - self.tall_sprites: continue tile = get_tile(x // tw + left, y // th + top, l) tile and surface_blit(tile, (x - ox, y - oy)) def _queue_edge_tiles(self, dx, dy): """Queue edge tiles and clear edge areas on buffer if needed :param dx: Edge along X axis to enqueue :param dy: Edge along Y axis to enqueue :return: None """ v = self._tile_view tw, th = self.data.tile_size self._tile_queue = iter([]) def append(rect): self._tile_queue = chain(self._tile_queue, self.data.get_tile_images_by_rect(rect)) # TODO: optimize so fill is only used when map is smaller than buffer self._clear_surface( self._buffer, ((rect[0] - v.left) * tw, (rect[1] - v.top) * th, rect[2] * tw, rect[3] * th)) if dx > 0: # right side append((v.right - 1, v.top, dx, v.height)) elif dx < 0: # left side append((v.left, v.top, -dx, v.height)) if dy > 0: # bottom side append((v.left, v.bottom - 1, v.width, dy)) elif dy < 0: # top side append((v.left, v.top, v.width, -dy)) @staticmethod def _calculate_zoom_buffer_size(size, value): if value <= 0: LOG.error("zoom level cannot be zero or less") raise ValueError value = 1.0 / value return int(size[0] * value), int(size[1] * value) def _create_buffers(self, view_size, buffer_size): """Create the buffers, taking in account pixel alpha or colorkey :param view_size: pixel size of the view :param buffer_size: pixel size of the buffer """ requires_zoom_buffer = not view_size == buffer_size self._zoom_buffer = None if self._clear_color is None: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size) self._buffer = Surface(buffer_size) elif self._clear_color == self._rgba_clear_color: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=SRCALPHA) self._buffer = Surface(buffer_size, flags=SRCALPHA) self.data.convert_surfaces(self._buffer, True) elif self._clear_color is not self._rgb_clear_color: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=RLEACCEL) self._zoom_buffer.set_colorkey(self._clear_color) self._buffer = Surface(buffer_size, flags=RLEACCEL) self._buffer.set_colorkey(self._clear_color) self._buffer.fill(self._clear_color) def _initialize_buffers(self, view_size): """Create the buffers to cache tile drawing :param view_size: (int, int): size of the draw area :return: None """ def make_rect(x, y): return Rect((x * tw, y * th), (tw, th)) tw, th = self.data.tile_size mw, mh = self.data.map_size buffer_tile_width = int(math.ceil(view_size[0] / tw) + 1) buffer_tile_height = int(math.ceil(view_size[1] / th) + 1) buffer_pixel_size = buffer_tile_width * tw, buffer_tile_height * th self.map_rect = Rect(0, 0, mw * tw, mh * th) self.view_rect.size = view_size self._previous_blit = Rect(self.view_rect) self._tile_view = Rect(0, 0, buffer_tile_width, buffer_tile_height) self._redraw_cutoff = 1 # TODO: optimize this value self._create_buffers(view_size, buffer_pixel_size) self._half_width = view_size[0] // 2 self._half_height = view_size[1] // 2 self._x_offset = 0 self._y_offset = 0 rects = [ make_rect(*i) for i in product(range(buffer_tile_width), range(buffer_tile_height)) ] # TODO: figure out what depth -actually- does # values <= 8 tend to reduce performance self._layer_quadtree = FastQuadTree(rects, 4) self.redraw_tiles(self._buffer) def _flush_tile_queue(self, surface): """Blit the queued tiles and block until the tile queue is empty""" tw, th = self.data.tile_size ltw = self._tile_view.left * tw tth = self._tile_view.top * th surface_blit = surface.blit self.data.prepare_tiles(self._tile_view) for x, y, l, image in self._tile_queue: surface_blit(image, (x * tw - ltw, y * th - tth)) def _flush_tile_queue_blits(self, surface): """Blit the queued tiles and block until the tile queue is empty for pygame 1.9.4 + """ tw, th = self.data.tile_size ltw = self._tile_view.left * tw tth = self._tile_view.top * th self.data.prepare_tiles(self._tile_view) blit_list = [(image, (x * tw - ltw, y * th - tth)) for x, y, l, image in self._tile_queue] surface.blits(blit_list)
class Player(pygame.sprite.DirtySprite): """Class representing individual players' avatar, their attributes, and movement :param direction_facing: the direction the player is facing (0 for left and 1 for right) :param sword_height: the height at which the player is holding the sword (0 means no sword, 3 is high, 2 is med, 1 is low sword) :param is_ghost: True if the player has respawned as a ghost and False if the player is not currently a ghost :param is_locked_on: True if camera is following this player, False otherwise :param sprite: the image name for the player's current motion :param image_dict: a dictionary of image file names for each motion """ def __init__(self, x_pos, y_pos, direction_facing, sword_height, is_ghost, is_locked_on, image_dict, win_direction): pygame.sprite.DirtySprite.__init__(self) self._player_state = { "running": False, "jumping": False, "ducking": False, "thrusting": False, "on_right_wall": False, "on_left_wall": False, "on_ground": False, "locked_on": False, "ghost": False, "ghost_counter": -1, "sword_height": 2, "direction_facing": direction_facing, "air_time": 0, "x_velocity": 0, "y_velocity": 0, "count_until_turn_around": 0, "sword": True, "sword_moving": True, "run_counter": 0, "ignore_gravity": False, "player_won": False, "win_direction": win_direction #-1 left, 1 right } self._image_dict = image_dict self.image = pygame.image.load(self.getSprite()).convert_alpha() self.rect = Rect(x_pos + image_shift_amount_x, y_pos - image_shift_amount_y, 73, 140) def getXPos(self): return self.rect.x def getYPos(self): return self.rect.y def moveLeft(self): if (self.getPlayerState("ghost_counter") > 150 or self.getPlayerState("ghost_counter") == -1): self.setPlayerState("x_velocity", self.getPlayerState("x_velocity") - 1.25) if abs(self.getPlayerState("x_velocity")) > 5: self._player_state["count_until_turn_around"] += 1 else: self._player_state["count_until_turn_around"] -= 4 if self._player_state["count_until_turn_around"] > 15: self.setDirection("left") self._player_state["count_until_turn_around"] = 20 self.setPlayerState("running", True) elif self._player_state["count_until_turn_around"] < 0: self._player_state["count_until_turn_around"] = 0 if self.getPlayerState("x_velocity") < -12: self.setPlayerState("x_velocity", -12) if self.getPlayerState( "ducking") and self.getPlayerState("x_velocity") < -3: self.setPlayerState("x_velocity", -3) else: self.setPlayerState("x_velocity", 0) def moveRight(self): if (self.getPlayerState("ghost_counter") > 150 or self.getPlayerState("ghost_counter") == -1): self.setPlayerState("x_velocity", self.getPlayerState("x_velocity") + 1.25) if abs(self.getPlayerState("x_velocity")) > 5: self._player_state["count_until_turn_around"] += 1 else: self._player_state["count_until_turn_around"] -= 4 if self._player_state["count_until_turn_around"] > 15: self.setDirection("right") self._player_state["count_until_turn_around"] = 20 self.setPlayerState("running", True) elif self._player_state["count_until_turn_around"] < 0: self._player_state["count_until_turn_around"] = 0 if self.getPlayerState("x_velocity") > 12: self.setPlayerState("x_velocity", 12) if self.getPlayerState( "ducking") and self.getPlayerState("x_velocity") > 3: self.setPlayerState("x_velocity", 3) else: self.setPlayerState("x_velocity", 0) def standingStill(self): if self.getPlayerState("x_velocity") > 0: self.setPlayerState("x_velocity", self.getPlayerState("x_velocity") / 5) elif self.getPlayerState("x_velocity") < 0: self.setPlayerState("x_velocity", self.getPlayerState("x_velocity") / 5) if abs(self.getPlayerState("x_velocity")) < .1: self.setPlayerState("x_velocity", 0) if abs(self.getPlayerState("x_velocity")) < 1: self._player_state["count_until_turn_around"] -= 4 if self._player_state["count_until_turn_around"] > 15: self.setDirection("right") elif self._player_state["count_until_turn_around"] < 0: self._player_state["count_until_turn_around"] = 0 def calculateGravity(self, time): if self.getPlayerState("air_time") == 0: self.setPlayerState("air_time", time) self.setPlayerState( "y_velocity", self.getPlayerState("y_velocity") + 5 * (time - self.getPlayerState("air_time"))) if self.getPlayerState("y_velocity") > 50: self.setPlayerState("y_velocity", 50) if self.getPlayerState("on_ground") or self.getPlayerState( "ignore_gravity"): self.setPlayerState("y_velocity", 0) #if self.getPlayerState("ignore_gravity"): # self.setPlayerState("y_velocity", 0) def jump(self, time): self.setPlayerState("y_velocity", -14) self.setPlayerState("air_time", time) self.setPlayerState("on_ground", False) def update(self): #print(self.getSprite()) self.image = pygame.image.load(self.getSprite()) self.dirty = 1 # force redraw from image, since we moved the sprite rect def getPlayerState(self, type): return self._player_state[type] def setPlayerState(self, type, value): self._player_state[type] = value def setDirection(self, direction): if (self.getPlayerState("ghost_counter") > 150 or self.getPlayerState("ghost_counter") == -1): if direction == "left": self._player_state["direction_facing"] = 1 else: self._player_state["direction_facing"] = 0 def getDirection(self): if self._player_state["direction_facing"] == 1: return "left" else: return "right" def adjustSwordHeight(self, adjustment): self._player_state["sword_height"] += adjustment if self._player_state["sword_height"] < 1: self._player_state["sword_height"] = 1 elif self._player_state["sword_height"] > 3: self._player_state["sword_height"] = 3 def respawn(self, startx, starty): if self._player_state["win_direction"] == -1: self.rect.x = startx + 314 self.rect.y = starty - 150 else: self.rect.x = startx - 386 self.rect.y = starty - 150 def getSprite(self): #print(self._player_state["ghost_counter"]) if self._player_state["ghost"]: front = "ghost_" else: front = "" if self._player_state["direction_facing"] == 1: append = "_l" else: append = "_r" if not self._player_state["sword"]: append = append + "_nosword" if self._player_state["ghost_counter"] >= 0 and self._player_state[ "ghost_counter"] < 50: return self._image_dict[front + "dead" + append + "_1"] elif self._player_state["ghost_counter"] >= 50 and self._player_state[ "ghost_counter"] < 100: return self._image_dict[front + "dead" + append + "_2"] elif self._player_state["ghost_counter"] >= 100 and self._player_state[ "ghost_counter"] < 150: return self._image_dict[front + "dead" + append + "_3"] else: if self._player_state["ghost_counter"] == 151: self._player_state["ghost"] = True if self._player_state["running"]: self._player_state[ "run_counter"] = self._player_state["run_counter"] + 1 if self._player_state["run_counter"] == 22: self._player_state["run_counter"] = 1 if self._player_state["run_counter"] > 14: run = "_1" elif self._player_state["run_counter"] > 7: run = "_2" else: run = "_3" return self._image_dict[front + "run" + append + run] if self._player_state["jumping"]: return self._image_dict[front + "jump" + append] if self._player_state["ducking"]: return self._image_dict[front + "duck" + append] if self._player_state["sword_height"] == 1: append = "_low" + append elif self._player_state["sword_height"] == 2: append = "_med" + append else: append = "_high" + append if self._player_state["thrusting"]: return self._image_dict[front + "thrust" + append] else: return self._image_dict[front + "sword" + append] def getImageDict(self): return self._image_dict def setImageDict(self, image_dict): self._image_dict = image_dict def getCollisionRect(self): return Rect(self.rect.x + image_shift_amount_x, self.rect.y - image_shift_amount_y, 73, 140) def debug(self): print("x_vel: ", end="") print(self.getPlayerState("x_velocity"), end=" ") print("y_vel: ", end="") print(self.getPlayerState("y_velocity"), end=" ") print("x_cord: ", end="") print(self.rect.x, end=" ") print("y_cord: ", end="") print(self.rect.y) print("count: ", end="") print(self.getPlayerState("count_until_turn_around")) def move(self, entities, camera): #TODO Check for being stabbed in this method self.setPlayerState("on_ground", False) self.setPlayerState("on_right_wall", False) self.setPlayerState("on_left_wall", False) if self.getPlayerState("ducking"): self.setPlayerState("running", False) ghost_multiplier = 1 if self.getPlayerState("ghost"): ghost_multiplier = 1.5 collision_list = self.test_collision_X( entities, camera) # Test all entities on the map for collision with player self.rect.x += (self.getPlayerState("x_velocity")) * ghost_multiplier for objects in collision_list: if self.getPlayerState("x_velocity") == 0 and ( self._player_state["ghost_counter"] < 0 or self._player_state["ghost_counter"] > 150): self._player_state["ghost_counter"] = 0 if self.getPlayerState("x_velocity") < 0: # Moving left self.rect.left = objects.right - image_shift_amount_x self.setPlayerState("on_left_wall", True) elif self.getPlayerState("x_velocity") > 0: # Moving right self.rect.right = objects.left - image_shift_amount_x self.setPlayerState("on_right_wall", True) # Lock player to look at other player when standing still or moving short time. # Flip player if they have been moving a certain amount of time. collision_list = self.test_collision_Y(entities, camera) self.rect.y += (self.getPlayerState("y_velocity")) for objects in collision_list: if self.getPlayerState("y_velocity") < 0: # Moving up self.rect.top = objects.bottom - image_shift_amount_y elif self.getPlayerState("y_velocity") > 0: # Moving down self.rect.bottom = objects.top + image_shift_amount_y self.setPlayerState("on_ground", True) #screen border keeps player on screens if self._player_state["ghost_counter"] < 0 or self._player_state[ "ghost_counter"] >= 150: # if player is not dying screen_rect = camera.getScreenRect() offset = camera.getOffset() screen_rect = pygame.Rect(screen_rect.left + offset[0], 0, 1000, 600) if not screen_rect.contains(self.rect): if (self._player_state["on_right_wall"] or self._player_state["on_left_wall"]): self._player_state["ghost_counter"] = 0 else: self.rect.clamp_ip( screen_rect) # makes player stay on screen self.update() # updates players position def test_collision_Y(self, entities, camera): collision_list = [] self.rect.y += (self.getPlayerState("y_velocity")) length = len(entities) for objects in entities: if self.getCollisionRect().colliderect(objects.getRect()): if objects.getEffects()["Kill"] and ( self.getPlayerState("ghost_counter") == -1 or self.getPlayerState("ghost_counter") > 151): if self.getPlayerState( "win_direction") == camera.getTarget( ).getPlayerState("win_direction"): camera.setActive(False) self.setPlayerState("ghost_counter", 0) self.setPlayerState("sword", True) elif objects.getEffects()["P2Flag"]: if self.getPlayerState( "win_direction" ) == -1 and not self.getPlayerState("ghost"): self.setPlayerState("player_won", True) elif objects.getEffects()["P1Flag"]: if self.getPlayerState( "win_direction" ) == 1 and not self.getPlayerState("ghost"): self.setPlayerState("player_won", True) if length <= 2: pass else: collision_list.append(objects.getRect()) length = length - 1 self.rect.y -= (self.getPlayerState("y_velocity")) return collision_list def test_collision_X(self, entities, camera): collision_list = [] self.rect.x += (self.getPlayerState("x_velocity")) length = len(entities) for objects in entities: if self.getCollisionRect().colliderect(objects.getRect()): if objects.getEffects()["Kill"]: if self.getPlayerState( "win_direction") == camera.getTarget( ).getPlayerState("win_direction"): camera.setActive(False) self.setPlayerState("ghost_counter", 0) self.setPlayerState("sword", True) elif objects.getEffects()["P2Flag"]: if self.getPlayerState( "win_direction" ) == -1 and not self.getPlayerState("ghost"): self.setPlayerState("player_won", True) elif objects.getEffects()["P1Flag"]: if self.getPlayerState( "win_direction" ) == 1 and not self.getPlayerState("ghost"): self.setPlayerState("player_won", True) else: collision_list.append(objects.getRect()) length = length - 1 self.rect.x -= (self.getPlayerState("x_velocity")) return collision_list def duck(self): global hit_box_height hit_box_height /= 2 def standUp(self): global hit_box_height hit_box_height *= 2 sprite = property(getSprite) image_dict = property(getImageDict, setImageDict) duck = property(duck) stand_up = property(standUp)
class BufferedRenderer(object): """ Renderer that support scrolling, zooming, layers, and animated tiles The buffered renderer must be used with a data class to get tile, shape, and animation information. See the data class api in pyscroll.data, or use the built-in pytmx support for loading maps created with Tiled. """ _alpha_clear_color = 0, 0, 0, 0 def __init__(self, data, size, clamp_camera=True, colorkey=None, alpha=False, time_source=time.time, scaling_function=pygame.transform.scale): # default options self.data = data # reference to data source self.clamp_camera = clamp_camera # if true, cannot scroll past map edge self.anchored_view = True # if true, map will be fixed to upper left corner self.map_rect = None # pygame rect of entire map self.time_source = time_source # determines how tile animations are processed self.scaling_function = scaling_function # what function to use when scaling the zoom buffer # internal private defaults if colorkey and alpha: print('cannot select both colorkey and alpha. choose one.') raise ValueError elif colorkey: self._clear_color = colorkey elif alpha: self._clear_color = self._alpha_clear_color else: self._clear_color = None # private attributes self._size = None # size that the camera/viewport is on screen, kinda self._redraw_cutoff = None # size of dirty tile edge that will trigger full redraw self._x_offset = None # offsets are used to scroll map in sub-tile increments self._y_offset = None self._buffer = None # complete rendering of tilemap self._tile_view = None # this rect represents each tile on the buffer self._half_width = None # 'half x' attributes are used to reduce division ops. self._half_height = None self._tile_queue = None # tiles queued to be draw onto buffer self._animation_queue = None # heap queue of animation token; schedules tile changes self._last_time = None # used for scheduling animations self._layer_quadtree = None # used to draw tiles that overlap optional surfaces self._zoom_buffer = None # used to speed up zoom operations self._zoom_level = 1.0 # negative numbers make map smaller, positive: bigger # used to speed up animated tile redraws by keeping track of animated tiles # so they can be updated individually self._animation_tiles = defaultdict(set) # this represents the viewable pixels, aka 'camera' self.view_rect = Rect(0, 0, 0, 0) self.reload_animations() self.set_size(size) def scroll(self, vector): """ scroll the background in pixels :param vector: (int, int) """ self.center((vector[0] + self.view_rect.centerx, vector[1] + self.view_rect.centery)) def center(self, coords): """ center the map on a pixel float numbers will be rounded. :param coords: (number, number) """ x, y = [round(i, 0) for i in coords] self.view_rect.center = x, y mw, mh = self.data.map_size tw, th = self.data.tile_size self.anchored_view = ((self._tile_view.width < mw) or (self._tile_view.height < mh)) if self.anchored_view and self.clamp_camera: self.view_rect.clamp_ip(self.map_rect) x, y = self.view_rect.center if not self.anchored_view: # calculate offset and do not scroll the map layer # this is used to handle maps smaller than screen self._x_offset = x - self._half_width self._y_offset = y - self._half_height else: # calc the new position in tiles and offset left, self._x_offset = divmod(x - self._half_width, tw) top, self._y_offset = divmod(y - self._half_height, th) # adjust the view if the view has changed without a redraw dx = int(left - self._tile_view.left) dy = int(top - self._tile_view.top) view_change = max(abs(dx), abs(dy)) if view_change and (view_change <= self._redraw_cutoff): self._buffer.scroll(-dx * tw, -dy * th) self._tile_view.move_ip(dx, dy) self._queue_edge_tiles(dx, dy) self._flush_tile_queue(self._buffer) elif view_change > self._redraw_cutoff: logger.info('scrolling too quickly. redraw forced') self._tile_view.move_ip(dx, dy) self.redraw_tiles(self._buffer) def draw(self, surface, rect, surfaces=None): """ Draw the map onto a surface pass a rect that defines the draw area for: drawing to an area smaller that the whole window/screen surfaces may optionally be passed that will be blitted onto the surface. this must be a sequence of tuples containing a layer number, image, and rect in screen coordinates. surfaces will be drawn in order passed, and will be correctly drawn with tiles from a higher layer overlapping the surface. surfaces list should be in the following format: [ (layer, surface, rect), ... ] or this: [ (layer, surface, rect, blendmode_flags), ... ] :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace between tiles """ if self._zoom_level == 1.0: self._render_map(surface, rect, surfaces) else: self._render_map(self._zoom_buffer, self._zoom_buffer.get_rect(), surfaces) self.scaling_function(self._zoom_buffer, rect.size, surface) @property def zoom(self): """ Zoom the map in or out. Increase this number to make map appear to come closer to camera. Decrease this number to make map appear to move away from camera. Default value is 1.0 This value cannot be negative or 0.0 :return: float """ return self._zoom_level @zoom.setter def zoom(self, value): buffer_size = self._calculate_zoom_buffer_size(self._size, value) self._zoom_level = value self._initialize_buffers(buffer_size) def set_size(self, size): """ Set the size of the map in pixels This is an expensive operation, do only when absolutely needed. :param size: (width, height) pixel size of camera/view of the group """ buffer_size = self._calculate_zoom_buffer_size(size, self._zoom_level) self._size = size self._initialize_buffers(buffer_size) def redraw_tiles(self, surface): """ redraw the visible portion of the buffer -- it is slow. """ logger.warn('pyscroll buffer redraw') if self._clear_color: surface.fill(self._clear_color) self._tile_queue = self.data.get_tile_images_by_rect(self._tile_view) self._flush_tile_queue(surface) def get_center_offset(self): """ Return x, y pair that will change world coords to screen coords :return: int, int """ return (-self.view_rect.centerx + self._half_width, -self.view_rect.centery + self._half_height) def _render_map(self, surface, rect, surfaces): """ Render the map and optional surfaces to destination surface :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace between tiles """ if self._animation_queue: self._process_animation_queue() if not self.anchored_view: surface.fill(0) offset = -self._x_offset + rect.left, -self._y_offset + rect.top with surface_clipping_context(surface, rect): surface.blit(self._buffer, offset) if surfaces: surfaces_offset = -offset[0], -offset[1] self._draw_surfaces(surface, surfaces_offset, surfaces) def _draw_surfaces(self, surface, offset, surfaces): """ Draw surfaces onto buffer, then redraw tiles that cover them :param surface: destination :param offset: offset to compensate for buffer alignment :param surfaces: sequence of surfaces to blit """ surface_blit = surface.blit ox, oy = offset left, top = self._tile_view.topleft hit = self._layer_quadtree.hit get_tile = self.data.get_tile_image tile_layers = tuple(self.data.visible_tile_layers) dirty = list() dirty_append = dirty.append # TODO: check to avoid sorting overhead layer_getter = itemgetter(2) surfaces.sort(key=layer_getter) for layer, group in groupby(surfaces, layer_getter): del dirty[:] for i in group: try: flags = i[3] except IndexError: dirty_append(surface_blit(i[0], i[1])) else: dirty_append(surface_blit(i[0], i[1], None, flags)) # TODO: make set of covered tiles, in the case where a cluster # of sprite surfaces causes excessive over tile overdrawing for dirty_rect in dirty: for r in hit(dirty_rect.move(ox, oy)): x, y, tw, th = r for l in [i for i in tile_layers if gt(i, layer)]: tile = get_tile((x // tw + left, y // th + top, l)) if tile: surface_blit(tile, (x - ox, y - oy)) def _queue_edge_tiles(self, dx, dy): """ Queue edge tiles and clear edge areas on buffer if needed :param dx: Edge along X axis to enqueue :param dy: Edge along Y axis to enqueue :return: None """ v = self._tile_view fill = partial(self._buffer.fill, self._clear_color) tw, th = self.data.tile_size self._tile_queue = iter([]) def append(rect): self._tile_queue = chain(self._tile_queue, self.data.get_tile_images_by_rect(rect)) if self._clear_color: fill(((rect[0] - v.left) * tw, (rect[1] - v.top) * th, rect[2] * tw, rect[3] * th)) if dx > 0: # right side append((v.right - 1, v.top, dx, v.height)) elif dx < 0: # left side append((v.left, v.top, -dx, v.height)) if dy > 0: # bottom side append((v.left, v.bottom - 1, v.width, dy)) elif dy < 0: # top side append((v.left, v.top, v.width, -dy)) def _update_time(self): self._last_time = time.time() * 1000 def reload_animations(self): """ Reload animation information """ self._update_time() self._animation_queue = list() for gid, frame_data in self.data.get_animations(): frames = list() for frame_gid, frame_duration in frame_data: image = self.data.get_tile_image_by_gid(frame_gid) frames.append(AnimationFrame(image, frame_duration)) ani = AnimationToken(gid, frames) ani.next += self._last_time heappush(self._animation_queue, ani) def _process_animation_queue(self): self._update_time() self._tile_queue = list() tile_layers = tuple(self.data.visible_tile_layers) # test if the next scheduled tile change is ready while self._animation_queue[0].next <= self._last_time: token = heappop(self._animation_queue) # advance the animation frame index, looping by default if token.index == len(token.frames) - 1: token.index = 0 else: token.index += 1 next_frame = token.frames[token.index] token.next = next_frame.duration + self._last_time heappush(self._animation_queue, token) # go through the animated tile map: # * queue tiles that need to be changed # * remove map entries that do not collide with screen needs_clear = False for x, y, l in self._animation_tiles[token.gid]: # if this tile is on the buffer (checked by using the tile view) if self._tile_view.collidepoint(x, y): # redraw the entire column of tiles for layer in tile_layers: if layer == l: self._tile_queue.append( (x, y, layer, next_frame.image, token.gid)) else: image = self.data.get_tile_image((x, y, layer)) if image: self._tile_queue.append( (x, y, layer, image, None)) else: needs_clear = True # this will delete the set of tile locations that are checked for # animated tiles. when the tile queue is flushed, any tiles in the # queue will be added again. i choose to remove the set, rather # than removing the item in the set to reclaim memory over time... # though i could implement it by removing entries. idk -lt if needs_clear: del self._animation_tiles[token.gid] if self._tile_queue: self._flush_tile_queue(self._buffer) @staticmethod def _calculate_zoom_buffer_size(size, value): if value <= 0: print('zoom level cannot be zero or less') raise ValueError value = 1.0 / value return [int(round(i * value)) for i in size] def _create_buffers(self, view_size, buffer_size): """ Create the buffers, taking in account pixel alpha or colorkey :param view_size: pixel size of the view :param buffer_size: pixel size of the buffer """ requires_zoom_buffer = not view_size == buffer_size self._zoom_buffer = None if self._clear_color == self._alpha_clear_color: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=pygame.SRCALPHA) self._buffer = Surface(buffer_size, flags=pygame.SRCALPHA) self.data.convert_surfaces(self._buffer, True) elif self._clear_color: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=pygame.RLEACCEL) self._zoom_buffer.set_colorkey(self._clear_color) self._buffer = Surface(buffer_size, flags=pygame.RLEACCEL) self._buffer.set_colorkey(self._clear_color) self._buffer.fill(self._clear_color) else: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size) self._buffer = Surface(buffer_size) def _initialize_buffers(self, view_size): """ Create the buffers to cache tile drawing :param view_size: (int, int): size of the draw area :return: None """ tw, th = self.data.tile_size mw, mh = self.data.map_size buffer_tile_width = int(math.ceil(view_size[0] / tw) + 1) buffer_tile_height = int(math.ceil(view_size[1] / th) + 1) buffer_pixel_size = buffer_tile_width * tw, buffer_tile_height * th self.map_rect = Rect(0, 0, mw * tw, mh * th) self.view_rect.size = view_size self._tile_view = Rect(0, 0, buffer_tile_width, buffer_tile_height) self._redraw_cutoff = 1 # TODO: optimize this value self._create_buffers(view_size, buffer_pixel_size) self._half_width = view_size[0] // 2 self._half_height = view_size[1] // 2 self._x_offset = 0 self._y_offset = 0 def make_rect(x, y): return Rect((x * tw, y * th), (tw, th)) rects = [ make_rect(*i) for i in product(range(buffer_tile_width), range(buffer_tile_height)) ] # TODO: figure out what depth -actually- does # values <= 8 tend to reduce performance self._layer_quadtree = quadtree.FastQuadTree(rects, 4) self.redraw_tiles(self._buffer) def _flush_tile_queue(self, surface): """ Blit the queued tiles and block until the tile queue is empty """ tw, th = self.data.tile_size ltw = self._tile_view.left * tw tth = self._tile_view.top * th surface_blit = surface.blit for x, y, l, tile, gid in self._tile_queue: self._animation_tiles[gid].add((x, y, l)) surface_blit(tile, (x * tw - ltw, y * th - tth))
class MapBase(Window): def __init__(self, width, height, default_tile_name='floor'): Window.__init__(self,None,10) self.save_data = SaveObject() self.tile_manager = TileManager() default_tile = self.tile_manager.get_tile(default_tile_name) self.tiles = [] for x in range(width): col = [] self.tiles.append(col) for y in range(height): location = MapLocation(self, (x,y), default_tile) col.append(location) self.width = width self.height = height tiles_x = core.screen.get_width() / 32 tiles_y = core.screen.get_height() / 32 self.dimentions = Rect(0,0,width,height) self.character = None self.entities = RenderEntity() self.non_passable_entities = RenderEntity() self.viewport = Rect(0,0,tiles_x,tiles_y) self.offset = Rect(0,0,0,0) self.map_tile_coverage = Rect(0,0,tiles_x+5,tiles_y+5) if self.map_tile_coverage.width > width: self.map_tile_coverage.width = width if self.map_tile_coverage.height > height: self.map_tile_coverage.height = height self.map_non_scroll_region = \ self.viewport.inflate(SCROLL_EDGE*-2,SCROLL_EDGE*-2) self.action_listeners = {} self.entry_listeners = {} self.movement_listeners = [] self.scrolling = False self.frame = 0 self.map_frames_dirty = [True,True,True,True] self.map_frames = [] self.heal_points = 0 self.regen_rate = 2000000000 self.sound = core.mixer.Sound('%s/sounds/beep.wav' % DATA_DIR) for f in range(4): #TODO Add non hardcoded values for buffer #TODO Make sure we don't make a larger surface than we need #TODO Ex: 5x5 map self.map_frames.append(Surface(((1+width) * TILE_SIZE, \ (1+height) * TILE_SIZE))) def __getstate__(self): dict = {} dict['width'] = self.width dict['height'] = self.height dict['offset.width'] = self.offset.width dict['offset.height'] = self.offset.height dict['save_data'] = self.save_data return dict def __setstate__(self, dict): if self.__class__.__name__ == 'MapBase': self.__init__(dict['width'],dict['height']) else: self.__init__() self.save_data = dict['save_data'] self.offset.width = dict['offset.width'] self.offset.height = dict['offset.height'] self.blur_events() def dispose(self): self.destroy() del self.tiles self.tile_manager.clear() del self.action_listeners del self.entry_listeners del self.movement_listeners self.entities.empty() self.non_passable_entities.empty() self.character.map = None def set_regen_rate(self, rate): self.regen_rate = rate def get(self, x, y): if x<0 or y<0: return None try: return self.tiles[x][y] except: return None def calculate_tile_coverage(self, viewable): if self.character is None: return coverage = self.map_tile_coverage coverage.center = self.character.pos view_scroll = viewable.inflate(8,8) coverage.clamp_ip(view_scroll) coverage.clamp_ip(self.dimentions) self.offset.left = (viewable.left - coverage.left) * TILE_SIZE self.offset.top = (viewable.top - coverage.top ) * TILE_SIZE if not self.map_non_scroll_region.collidepoint(self.character.pos): self.map_non_scroll_region = \ self.viewport.inflate(SCROLL_EDGE*-2,SCROLL_EDGE*-2) def set_location(self, loc, tile_name, walkable=True, tile_pos=None): x, y = loc location = self.get(x, y) tile = self.tile_manager.get_tile(tile_name, None, tile_pos) location.set_tile(tile) location.set_walkable(walkable) def place_character(self, character, pos, passable=False, direction=NORTH): self.character = character character.map = self character.can_trigger_actions = 1 if not self.viewport.collidepoint(pos): self.viewport.center = pos self.viewport.clamp_ip(self.dimentions) self.place_entity(character, pos, passable, direction) self.calculate_tile_coverage(self.viewport) def place_entity(self, entity, entity_pos, passable=False, direction=NORTH): entity.face(direction) entity.map = self entity.move_to(entity_pos) self.entities.add(entity) if not passable: self.non_passable_entities.add(entity) def add_entry_listener(self, x, y, listener): self.entry_listeners[ (x,y) ] = listener def add_movement_listener(self, listener): self.movement_listeners.append(listener) def update(self): """Invoked once per cycle of the event loop, to allow animation to update""" if self.character.entered_tile: self.character.entered_tile = False self.check_heal() if self.entry_listeners.has_key( self.character.pos ): self.entry_listeners[self.character.pos]() for listener in self.movement_listeners: listener() if self.scrolling: axis = self.scroll_axis diff = [0,0] diff[axis] = self.scroll_anchor - self.character.rect[axis] self.entities.scroll(diff) diff[axis] = diff[axis] * -1 self.offset[axis] = self.offset[axis] + diff[axis] if not self.character.moving: self.scrolling = False if self.is_left(): self.move_character(WEST) if self.is_right(): self.move_character(EAST) if self.is_up(): self.move_character(NORTH) if self.is_down(): self.move_character(SOUTH) if self.map_frames_dirty[self.frame]: self.build_current_frame() self.map_frames_dirty[self.frame] = False self.entities.update() def draw(self, blit): blit(self.map_frames[self.frame], (0,0), self.offset) self.entities.draw(blit) def build_current_frame(self): #TODO Decide if map_tile_coverage is the right name for this blit = self.map_frames[self.frame].blit rect = (self.frame * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE) x = 0 y = 0 for col in range(self.map_tile_coverage.left, \ self.map_tile_coverage.right): column = self.tiles[col] for row in range(self.map_tile_coverage.top, \ self.map_tile_coverage.bottom): blit(column[row].tile, (x,y), rect) y = y + TILE_SIZE x = x + TILE_SIZE y = 0 def init(self): self.offset.width = core.screen.get_rect().width self.offset.height = core.screen.get_rect().height self.entities.run_command('enter_map') def handle_event(self,event): if event.type == PUSH_ACTION_EVENT: self.character_activate() if event.type == PUSH_ACTION2_EVENT: menu.run_main_menu() if event.type == QUIT_EVENT: core.wm.running = False def check_heal(self): self.heal_points = self.heal_points + 1 if self.heal_points >= self.regen_rate: core.game.save_data.hero.regenerate() self.heal_points = 0 def character_activate(self): if not self.character.moving: target = add(self.character.pos,MOVE_VECTORS[self.character.facing]) entities = self.non_passable_entities.entity_collisions(target) for e in entities: e.activate() def move_character(self, dir): self.character.move(dir) if not self.scrolling: if self.character.moving: nsr = self.map_non_scroll_region x,y = self.character.pos if dir % 2 == 0: # North or south if y < nsr.bottom and \ y >= nsr.top: return if y < SCROLL_EDGE or \ y >= self.height - SCROLL_EDGE: return self.scroll_axis = 1 else: # East or west if x < nsr.right and \ x >= nsr.left: return if x < SCROLL_EDGE or \ x >= self.width - SCROLL_EDGE: return self.scroll_axis = 0 self.scrolling = True vector = MOVE_VECTORS[dir] self.map_non_scroll_region.move_ip(vector) old_viewport = self.viewport self.viewport = old_viewport.move(vector) self.scroll_anchor = self.character.rect[self.scroll_axis] if not self.map_tile_coverage.contains(self.viewport): self.calculate_tile_coverage(old_viewport) self.dirty() def dirty(self): for f in range(4): self.map_frames_dirty[f] = True def move_ok(self, target_pos, character): x, y = target_pos target = self.get(x,y) if target is not None and target.is_walkable(): entities = self.non_passable_entities.entity_collisions(target_pos) if len(entities) > 0: if character.can_trigger_actions: for e in entities: e.touch() else: for e in entities: if e.can_trigger_actions: character.touch() return 0 else: self.sound.play() return 1 else: return 0 def get_tiles_from_ascii(self,ascii,tile_map): for y in range(self.height): line = ascii[y] for x in range(self.width): c = line[x] args = tile_map[c] pos = None if len(args) > 1: pos = args[1] self.set_location( (x,y), args[0], tile_map['walkable'].find(c)!=-1, pos )
class BufferedRenderer(object): """ Renderer that support scrolling, zooming, layers, and animated tiles The buffered renderer must be used with a data class to get tile, shape, and animation information. See the data class api in pyscroll.data, or use the built-in pytmx support for loading maps created with Tiled. """ def __init__(self, data, size, clamp_camera=True, colorkey=None, alpha=False, time_source=time.time, scaling_function=pygame.transform.scale): # default options self.data = data # reference to data source self.clamp_camera = clamp_camera # if true, cannot scroll past map edge self.anchored_view = True # if true, map will be fixed to upper left corner self.map_rect = None # pygame rect of entire map self.time_source = time_source # determines how tile animations are processed self.scaling_function = scaling_function # what function to use when zooming self.default_shape_texture_gid = 1 # [experimental] texture to draw shapes with self.default_shape_color = 0, 255, 0 # [experimental] color to fill polygons with # internal private defaults if colorkey and alpha: print('cannot select both colorkey and alpha. choose one.') raise ValueError elif colorkey: self._clear_color = colorkey elif alpha: self._clear_color = (0, 0, 0, 0) else: self._clear_color = None # private attributes self._size = None # size that the camera/viewport is on screen, kinda self._redraw_cutoff = None # size of dirty tile edge that will trigger full redraw self._x_offset = None # offsets are used to scroll map in sub-tile increments self._y_offset = None self._buffer = None # complete rendering of tilemap self._tile_view = None # this rect represents each tile on the buffer self._half_width = None # 'half x' attributes are used to reduce division ops. self._half_height = None self._tile_queue = None # tiles queued to be draw onto buffer #self._animation_queue = None # heap queue of animation token. schedules tile changes #self._animation_map = None # map of GID to other GIDs in an animation self._last_time = None # used for scheduling animations self._layer_quadtree = None # used to draw tiles that overlap optional surfaces self._zoom_buffer = None # used to speed up zoom operations self._zoom_level = 1.0 # negative numbers make map smaller, positive: bigger # this represents the viewable pixels, aka 'camera' self.view_rect = Rect(0, 0, 0, 0) self.reload_animations() self.set_size(size) def scroll(self, vector): """ scroll the background in pixels :param vector: (int, int) """ self.center((vector[0] + self.view_rect.centerx, vector[1] + self.view_rect.centery)) def center(self, coords): """ center the map on a pixel float numbers will be rounded. :param coords: (number, number) """ x, y = [round(i, 0) for i in coords] self.view_rect.center = x, y mw, mh = self.data.map_size tw, th = self.data.tile_size self.anchored_view = ((self._tile_view.width < mw) or (self._tile_view.height < mh)) if self.anchored_view and self.clamp_camera: self.view_rect.clamp_ip(self.map_rect) x, y = self.view_rect.center if not self.anchored_view: # calculate offset and do not scroll the map layer # this is used to handle maps smaller than screen self._x_offset = x - self._half_width self._y_offset = y - self._half_height else: # calc the new position in tiles and offset left, self._x_offset = divmod(x - self._half_width, tw) top, self._y_offset = divmod(y - self._half_height, th) # adjust the view if the view has changed without a redraw dx = int(left - self._tile_view.left) dy = int(top - self._tile_view.top) view_change = max(abs(dx), abs(dy)) if view_change and (view_change <= self._redraw_cutoff): self._buffer.scroll(-dx * tw, -dy * th) self._tile_view.move_ip(dx, dy) self._queue_edge_tiles(dx, dy) self._flush_tile_queue() elif view_change > self._redraw_cutoff: logger.info('scrolling too quickly. redraw forced') self._tile_view.move_ip(dx, dy) self.redraw_tiles() def draw(self, surface, rect, surfaces=None): """ Draw the map onto a surface pass a rect that defines the draw area for: drawing to an area smaller that the whole window/screen surfaces may optionally be passed that will be blitted onto the surface. this must be a sequence of tuples containing a layer number, image, and rect in screen coordinates. surfaces will be drawn in order passed, and will be correctly drawn with tiles from a higher layer overlapping the surface. surfaces list should be in the following format: [ (layer, surface, rect), ... ] or this: [ (layer, surface, rect, blendmode_flags), ... ] :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace between tiles """ if self._zoom_level == 1.0: self._render_map(surface, rect, surfaces) else: self._render_map(self._zoom_buffer, self._zoom_buffer.get_rect(), surfaces) self.scaling_function(self._zoom_buffer, rect.size, surface) @property def zoom(self): """ Zoom the map in or out. Increase this number to make map appear to come closer to camera. Decrease this number to make map appear to move away from camera. Default value is 1.0 This value cannot be negative or 0.0 :return: float """ return self._zoom_level @zoom.setter def zoom(self, value): self._zoom_level = value buffer_size = self._calculate_zoom_buffer_size(value) self._initialize_buffers(buffer_size) def set_size(self, size): """ Set the size of the map in pixels This is an expensive operation, do only when absolutely needed. :param size: (width, height) pixel size of camera/view of the group """ self._size = size buffer_size = self._calculate_zoom_buffer_size(self._zoom_level) self._initialize_buffers(buffer_size) def redraw_tiles(self): """ redraw the visible portion of the buffer -- it is slow. """ if self._clear_color: self._buffer.fill(self._clear_color) self._tile_queue = self.data.get_tile_images_by_rect(self._tile_view) self._flush_tile_queue() def get_center_offset(self): """ Return x, y pair that will change world coords to screen coords :return: int, int """ return (-self.view_rect.centerx + self._half_width, -self.view_rect.centery + self._half_height) def _render_map(self, surface, rect, surfaces): """ Render the map and optional surfaces to destination surface :param surface: pygame surface to draw to :param rect: area to draw to :param surfaces: optional sequence of surfaces to interlace between tiles """ if self._animation_queue: self._process_animation_queue() if not self.anchored_view: surface.fill(0) offset = -self._x_offset + rect.left, -self._y_offset + rect.top with surface_clipping_context(surface, rect): surface.blit(self._buffer, offset) if surfaces: surfaces_offset = -offset[0], -offset[1] self._draw_surfaces(surface, surfaces_offset, surfaces) def _draw_surfaces(self, surface, offset, surfaces): """ Draw surfaces onto buffer, then redraw tiles that cover them :param surface: destination :param offset: offset to compensate for buffer alignment :param surfaces: sequence of surfaces to blit """ surface_blit = surface.blit ox, oy = offset left, top = self._tile_view.topleft hit = self._layer_quadtree.hit get_tile = self.data.get_tile_image tile_layers = tuple(self.data.visible_tile_layers) dirty = list() dirty_append = dirty.append for i in surfaces: try: flags = i[3] except IndexError: dirty_append((surface_blit(i[0], i[1]), i[2])) else: dirty_append((surface_blit(i[0], i[1], None, flags), i[2])) for dirty_rect, layer in dirty: for r in hit(dirty_rect.move(ox, oy)): x, y, tw, th = r for l in [i for i in tile_layers if gt(i, layer)]: tile = get_tile((x // tw + left, y // th + top, l)) if tile: surface_blit(tile, (x - ox, y - oy)) def _draw_objects(self): """ Totally unoptimized drawing of objects to the map [probably broken] """ tw, th = self.data.tile_size buff = self._buffer blit = buff.blit map_gid = self.data.tmx.map_gid default_color = self.default_shape_color get_image_by_gid = self.data.get_tile_image_by_gid _draw_textured_poly = pygame.gfxdraw.textured_polygon _draw_poly = pygame.draw.polygon _draw_lines = pygame.draw.lines ox = self._tile_view.left * tw oy = self._tile_view.top * th def draw_textured_poly(texture, points): try: _draw_textured_poly(buff, points, texture, tw, th) except pygame.error: pass def draw_poly(color, points, width=0): _draw_poly(buff, color, points, width) def draw_lines(color, points, width=2): _draw_lines(buff, color, False, points, width) def to_buffer(pt): return pt[0] - ox, pt[1] - oy for layer in self.data.visible_object_layers: for o in (o for o in layer if o.visible): texture_gid = getattr(o, "texture", None) color = getattr(o, "color", default_color) # BUG: this is not going to be completely accurate, because it # does not take into account times where texture is flipped. if texture_gid: texture_gid = map_gid(texture_gid)[0][0] texture = get_image_by_gid(int(texture_gid)) if hasattr(o, 'points'): points = [to_buffer(i) for i in o.points] if o.closed: if texture_gid: draw_textured_poly(texture, points) else: draw_poly(color, points) else: draw_lines(color, points) elif o.gid: tile = get_image_by_gid(o.gid) if tile: pt = to_buffer((o.x, o.y)) blit(tile, pt) else: x, y = to_buffer((o.x, o.y)) points = ((x, y), (x + o.width, y), (x + o.width, y + o.height), (x, y + o.height)) if texture_gid: draw_textured_poly(texture, points) else: draw_poly(color, points) def _queue_edge_tiles(self, dx, dy): """ Queue edge tiles and clear edge areas on buffer if needed :param dx: Edge along X axis to enqueue :param dy: Edge along Y axis to enqueue :return: None """ v = self._tile_view fill = partial(self._buffer.fill, self._clear_color) tw, th = self.data.tile_size self._tile_queue = iter([]) def append(rect): self._tile_queue = chain(self._tile_queue, self.data.get_tile_images_by_rect(rect)) if self._clear_color: fill(((rect[0] - v.left) * tw, (rect[1] - v.top) * th, rect[2] * tw, rect[3] * th)) if dx > 0: # right side append((v.right - 1, v.top, dx, v.height)) elif dx < 0: # left side append((v.left, v.top, -dx, v.height)) if dy > 0: # bottom side append((v.left, v.bottom - 1, v.width, dy)) elif dy < 0: # top side append((v.left, v.top, v.width, -dy)) def _update_time(self): self._last_time = time.time() * 1000 def reload_animations(self): """ Reload animation information """ self._update_time() self._animation_map = dict() self._animation_queue = list() for gid, frame_data in self.data.get_animations(): frames = list() for frame_gid, frame_duration in frame_data: image = self.data.get_tile_image_by_gid(frame_gid) frames.append(AnimationFrame(image, frame_duration)) ani = AnimationToken(gid, frames) ani.next += self._last_time self._animation_map[ani.gid] = ani.frames[ani.index].image print(self._animation_map[ani.gid]) heappush(self._animation_queue, ani) def _process_animation_queue(self): self._update_time() requires_redraw = False # test if the next scheduled tile change is ready while self._animation_queue[0].next <= self._last_time: requires_redraw = True token = heappop(self._animation_queue) # advance the animation index, looping by default if token.index == len(token.frames) - 1: token.index = 0 else: token.index += 1 next_frame = token.frames[token.index] token.next = next_frame.duration + self._last_time self._animation_map[token.gid] = next_frame.image heappush(self._animation_queue, token) if requires_redraw: # TODO: record the tiles that changed and update only affected tiles self.redraw_tiles() def _calculate_zoom_buffer_size(self, value): if value <= 0: print('zoom level cannot be zero or less') raise ValueError value = 1.0 / value return [int(round(i * value)) for i in self._size] def _create_buffers(self, view_size, buffer_size): """ Create the buffers, taking in account pixel alpha or colorkey :param view_size: pixel size of the view :param buffer_size: pixel size of the buffer """ requires_zoom_buffer = not view_size == buffer_size self._zoom_buffer = None if self._clear_color == (0, 0, 0, 0): if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=pygame.SRCALPHA) self._buffer = Surface(buffer_size, flags=pygame.SRCALPHA) elif self._clear_color: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size, flags=pygame.RLEACCEL) self._zoom_buffer.set_colorkey(self._clear_color) self._buffer = Surface(buffer_size, flags=pygame.RLEACCEL) self._buffer.set_colorkey(self._clear_color) self._buffer.fill(self._clear_color) else: if requires_zoom_buffer: self._zoom_buffer = Surface(view_size) self._buffer = Surface(buffer_size) def _initialize_buffers(self, view_size): """ Create the buffers to cache tile drawing :param view_size: (int, int): size of the draw area :return: None """ tw, th = self.data.tile_size mw, mh = self.data.map_size buffer_tile_width = int(math.ceil(view_size[0] / tw) + 2) buffer_tile_height = int(math.ceil(view_size[1] / th) + 2) buffer_pixel_size = buffer_tile_width * tw, buffer_tile_height * th self.map_rect = Rect(0, 0, mw * tw, mh * th) self.view_rect.size = view_size self._tile_view = Rect(0, 0, buffer_tile_width, buffer_tile_height) self._redraw_cutoff = 0.5 # TODO: optimize this value self._create_buffers(view_size, buffer_pixel_size) self._half_width = view_size[0] // 2 self._half_height = view_size[1] // 2 self._x_offset = 0 self._y_offset = 0 def make_rect(x, y): return Rect((x * tw, y * th), (tw, th)) rects = [ make_rect(*i) for i in product(range(buffer_tile_width), range(buffer_tile_height)) ] # TODO: figure out what depth -actually- does self._layer_quadtree = quadtree.FastQuadTree(rects, 4) self.redraw_tiles() def _flush_tile_queue(self): """ Blit the queued tiles and block until the tile queue is empty """ tw, th = self.data.tile_size ltw = self._tile_view.left * tw tth = self._tile_view.top * th blit = self._buffer.blit map_get = self._animation_map.get for x, y, l, tile, gid in self._tile_queue: blit(map_get(gid, tile), (x * tw - ltw, y * th - tth)) #