Пример #1
0
class Map:
    """Stores map tile data, entity positions, and draws the map.
    
    The map is made of 1.0x1.0 blocks.
    
    The map is rendered in square chunks of blocks, which are cached as 
    surfaces to reduce the number of blits needed to draw the map. A chunk
    only needs to be redrawn when a block it contains is changed.
    """
    def __init__(self, size):
        """Create map of size*size tiles."""
        self.TILE_SIZE = 20 # pixels
        self.CHUNK_SIZE = 8 # blocks square
        self.BLOCK_UPDATE_SIZE = (40, 40) # rect size around player in blocks
        self.BLOCK_UPDATE_FREQ = 1000 # number of blocks to update per second
        
        self._blocks = [] # block_ids of the map in row-major order
        self.size = size # (width, height) of the map in blocks
        self.entities = [] # list of MapEntities in the map
        
        self._blocks = map_generation.generate_map(size)
        
        self._particle_systems = [] # (ParticleSystem, pos) tuples
        
        self.cursor_pos = None # pixel coords or None
        
        self.light = Light(self)
        
        # prime the chunk cache by rendering every chunk in the map
        self._chunk_cache = {}
        print "priming chunk cache..."
        for chunk in self.get_chunks_in_rect(((0, 0) + self.size)):
            s = pygame.Surface((self.CHUNK_SIZE * self.TILE_SIZE, 
                                self.CHUNK_SIZE * self.TILE_SIZE))
            self.draw_chunk(s, chunk)
            self._chunk_cache[chunk] = s
        print "cached %i chunks" % len(self._chunk_cache)
    
    def get_block(self, x, y):
        """Return the block id at coordinates, or None if out of range."""
        if (x in xrange(0, self.size[0]) and y in xrange(0, self.size[1])):
            i = (y * self.size[1]) + x
            return self._blocks[i]
        else:
            return None
    
    def set_block(self, x, y, block_id):
        """Set the block at coordinates to block_id.
        
        Fails if block coords are out of range.
        """
        assert self.get_block(x, y) != None
        i = (y * self.size[1]) + x
        self._blocks[i] = block_id
        # invalidate the chunk cache
        cx = x / self.CHUNK_SIZE * self.CHUNK_SIZE
        cy = y / self.CHUNK_SIZE * self.CHUNK_SIZE
        if (cx, cy) in self._chunk_cache:
            del self._chunk_cache[(cx, cy)] # TODO: reuse surface?
        
        # update lighting and invalidate changed chunks
        # for every block with changed lighting, invalidate its chunk
        changed_blocks = self.light.update_light(x, y)
        for block in changed_blocks:
            cx = block[0] / self.CHUNK_SIZE * self.CHUNK_SIZE
            cy = block[1] / self.CHUNK_SIZE * self.CHUNK_SIZE
            if (cx, cy) in self._chunk_cache:
                del self._chunk_cache[(cx, cy)]

    def is_solid_block(self, x, y):
        """Return True if block at given coordinates is solid. 
        
        Assumes blocks outside map are not solid.
        """
        block_id = self.get_block(x, y)
        return (block_id != None and Block(block_id).is_solid)

    def draw_chunk(self, surf, pos):
        """Draw a self.CHUNK_SIZE square of the map tiles.
        
        surf: Surface to draw to.
        pos: The top-left position of the map chunk to draw.
        """
        surf.fill((100, 100, 255))
        # figure out range of tiles in this chunk
        tile_range = [(int(pos[i]), 
                       int(ceil(pos[i] + surf.get_size()[i] / self.TILE_SIZE)))
                       for i in [0, 1]]
        for x in xrange(*(tile_range[0])):
            for y in xrange(*(tile_range[1])):
                (px, py) = self.grid_to_px(pos, (x,y))
                bid = self.get_block(x, y)
                if bid != None:
                    block = Block(bid)
                    light_level = self.light.get_light(x, y)
                    block_surf = block.lit_surfs[light_level]
                    surf.blit(block_surf, (px, py))
                else:
                    pass #FIXME

    def get_chunks_in_rect(self, rect):
        """Generate the list of chunks inside a rect."""
        x_min = rect[0]
        x_max = rect[0] + rect[2]
        y_min = rect[1]
        y_max = rect[1] + rect[3]
        chunks_x = (int(x_min) / self.CHUNK_SIZE * self.CHUNK_SIZE, 
                    int(x_max) / self.CHUNK_SIZE * self.CHUNK_SIZE)
        chunks_y = (int(y_min) / self.CHUNK_SIZE * self.CHUNK_SIZE, 
                    int(y_max) / self.CHUNK_SIZE * self.CHUNK_SIZE)
        # loop over every chunk and yield it
        for x in [c for c in xrange(chunks_x[0], chunks_x[1]+1) 
                  if c % self.CHUNK_SIZE == 0]:
            for y in [c for c in xrange(chunks_y[0], chunks_y[1]+1) 
                      if c % self.CHUNK_SIZE == 0]:
                yield (x, y)

    def draw(self, surf, pos):
        """Draw the map tiles and entites.
        
        surf: Surface to draw to.
        pos: Top-left grid position to draw.
        surf_size: 
        
        Note: negative chunks will causing rounding in the wrong direction, 
        causing undrawn margins. Fix this by not drawing empty chunks.
        """
        # figure out which chunks are onscreen
        # get topleft and bottomright grid positions of viewport
        topleft = pos
        bottomright = self.px_to_grid(pos, surf.get_size())
        # get min and max grid positions inside viewport
        x_min = topleft[0]
        x_max = bottomright[0]
        y_min = topleft[1]
        y_max = bottomright[1]
        # get min and max chunk positions inside viewport
        chunks_x = (int(x_min) / self.CHUNK_SIZE * self.CHUNK_SIZE, 
                    int(x_max) / self.CHUNK_SIZE * self.CHUNK_SIZE)
        chunks_y = (int(y_min) / self.CHUNK_SIZE * self.CHUNK_SIZE, 
                    int(y_max) / self.CHUNK_SIZE * self.CHUNK_SIZE)
        # loop over every chunk to draw
        for x in [c for c in xrange(chunks_x[0], chunks_x[1]+1) 
                  if c % self.CHUNK_SIZE == 0]:
            for y in [c for c in xrange(chunks_y[0], chunks_y[1]+1) 
                      if c % self.CHUNK_SIZE == 0]:
                # redraw the chunk if it's not cached
                if (x, y) not in self._chunk_cache:
                    print "chunk cache miss on %i,%i" % (x, y)
                    now = pygame.time.get_ticks()
                    # TODO: add margin to allow tile overlapping between chunks
                    s = pygame.Surface((self.CHUNK_SIZE * self.TILE_SIZE, 
                                        self.CHUNK_SIZE * self.TILE_SIZE))
                    self.draw_chunk(s, (x, y))
                    self._chunk_cache[(x, y)] = s
                    elapsed = pygame.time.get_ticks() - now
                    print "cached new chunk in %i ms" % elapsed
                # rounding pos and blit_pos seems to fix edges between chunks
                blit_pos = self.grid_to_px((round(pos[0], 2), 
                                            round(pos[1], 2)), (x, y))
                blit_pos = (round(blit_pos[0]), round(blit_pos[1]))
                surf.blit(self._chunk_cache[(x, y)], 
                          blit_pos)
        
        # figure out which entities are onscreen and draw them
        for entity in self.entities:
            topleft = (entity.x, entity.y)
            p_tl = self.grid_to_px(pos, topleft)
            # TODO: clip entities offscreen
            entity.draw(p_tl[0], p_tl[1], entity.width * self.TILE_SIZE,
                        entity.height * self.TILE_SIZE, surf=surf)
    
        # draw particle systems
        for ps_pos in self._particle_systems:
            (ps, g_pos) = ps_pos
            p_pos = self.grid_to_px(pos, g_pos)
            # TODO: clip
            ps.draw(surf, p_pos)
        
        # draw selected block
        if self.cursor_pos != None:
            selected_block = self.px_to_grid(pos, self.cursor_pos)
            selected_block = (int(selected_block[0]), int(selected_block[1]))
            rect = (self.grid_to_px(pos, selected_block) +
                    (self.TILE_SIZE, self.TILE_SIZE))
            pygame.draw.rect(surf, (255, 0, 0), rect, 2)
        
    def grid_to_px(self, topleft, pos):
        """Return pixel coordinate from a grid coordinate.
        
        topleft: top-left of map being drawn.
        pos: grid position to convert.
        """
        return ((pos[0] - topleft[0]) * self.TILE_SIZE, 
                (pos[1] - topleft[1]) * self.TILE_SIZE)

    def px_to_grid(self, topleft, pos):
        """Return grid coordinate from a pixel coordinate.
        
        topleft: top-left of map being drawn.
        pos: pixel position to convert.
        """
        return ((float(pos[0]) / self.TILE_SIZE) + topleft[0], 
                (float(pos[1]) / self.TILE_SIZE) + topleft[1])

    def update(self, millis):
        """Update entities, particle systems, and blocks in the map.
        
        Blocks are updated in a rectangle around the player. Random blocks in
        this rectangle are chosen to be updated each call.
        """
        for entity in self.entities:
            entity.update(millis, self)
        
        for ps_pos in self._particle_systems:
            (ps, pos) = ps_pos
            ps.update(millis)
            if ps.is_expired():
                self._particle_systems.remove(ps_pos)
        
        # TODO: hack to get player pos
        update_center = (int(self.entities[0].x), int(self.entities[0].y))
        # loop so updates occur at specified frequency
        for i in xrange(int(ceil(millis /
                                 float(self.BLOCK_UPDATE_FREQ * 1000)))):
            x = randrange(update_center[0] - self.BLOCK_UPDATE_SIZE[0]/2, 
                          update_center[0] + self.BLOCK_UPDATE_SIZE[0]/2 + 1)
            y = randrange(update_center[1] - self.BLOCK_UPDATE_SIZE[1]/2, 
                          update_center[1] + self.BLOCK_UPDATE_SIZE[1]/2 + 1)
            # update block at (x, y)
            bid = self.get_block(x, y)
            if bid == Block(name="grass").id:
                
                # kill grass which is too dark
                if self.light.get_light(x, y) < self.light.MAX_LIGHT_LEVEL:
                    self.set_block(x, y, Block(name="dirt").id)
                
                # spread grass to adjacent blocks which are bright enough
                for pos in [(x-1,y),(x+1,y),(x,y-1),(x,y+1),(x-1,y-1),
                            (x+1,y+1),(x-1,y+1),(x+1,y-1)]:
                    if (self.light.get_light(*pos) == self.light.MAX_LIGHT_LEVEL 
                            and self.get_block(*pos) == Block(name="dirt").id):
                        self.set_block(pos[0], pos[1], Block(name="grass").id)

    def rect_colliding(self, rect, assume_solid=None):
        """Return true if the given rect will collide with the map.
        
        This works by finding all the map blocks inside the rect, and 
        returning true if any of them are solid blocks.
        
        rect should be a (x, y, w, h) tuple since pygame Rects don't use 
        floats.
        
        If assume_solid is an (x, y) tuple, that block will be assumed solid.
        """
        r_x = rect[0]
        r_y = rect[1]
        r_w = rect[2]
        r_h = rect[3]
        # get range of blocks inside the given rect
        x_range = (int(r_x), int(ceil(r_x + r_w)))
        y_range = (int(r_y), int(ceil(r_y + r_h)))
        for x in xrange(x_range[0], x_range[1]):
            for y in xrange(y_range[0], y_range[1]):
                if self.is_solid_block(x, y) or (x, y) == assume_solid:
                    return True
        return False