import logging import itertools import pytmx logger = logging.getLogger(__name__) ch = logging.StreamHandler() ch.setLevel(logging.INFO) logger.addHandler(ch) logger.setLevel(logging.INFO) try: from pygame.transform import flip, rotate import pygame except ImportError: logger.error('cannot import pygame (is it installed?)') raise __all__ = ['load_pygame', 'pygame_image_loader', 'simplify', 'build_rects'] def handle_transformation(tile, flags): if flags.flipped_diagonally: tile = flip(rotate(tile, 270), 1, 0) if flags.flipped_horizontally or flags.flipped_vertically: tile = flip(tile, flags.flipped_horizontally, flags.flipped_vertically) return tile def smart_convert(original, colorkey, pixelalpha): """ this method does several tests on a surface to determine the optimal flags and pixel format for each tile surface. this is done for the best rendering speeds and removes the need to convert() the images on your own """ tile_size = original.get_size() threshold = 127 # the default # count the number of pixels in the tile that are not transparent px = pygame.mask.from_surface(original, threshold).count() # there are no transparent pixels in the image if px == tile_size[0] * tile_size[1]: tile = original.convert() # there are transparent pixels, and tiled set a colorkey elif colorkey: tile = original.convert() tile.set_colorkey(colorkey, pygame.RLEACCEL) # there are transparent pixels, and set for perpixel alpha elif pixelalpha: tile = original.convert_alpha() # there are transparent pixels, and we won't handle them else: tile = original.convert() return tile def pygame_image_loader(filename, colorkey, **kwargs): """ pytmx image loader for pygame :param filename: :param colorkey: :param kwargs: :return: """ if colorkey: colorkey = pygame.Color('#{0}'.format(colorkey)) pixelalpha = kwargs.get('pixelalpha', True) image = pygame.image.load(filename) def load_image(rect=None, flags=None): if rect: try: tile = image.subsurface(rect) except ValueError: logger.error('Tile bounds outside bounds of tileset image') raise else: tile = image.copy() if flags: tile = handle_transformation(tile, flags) tile = smart_convert(tile, colorkey, pixelalpha) return tile return load_image def load_pygame(filename, *args, **kwargs): """ Load a TMX file, images, and return a TiledMap class PYGAME USERS: Use me. this utility has 'smart' tile loading. by default any tile without transparent pixels will be loaded for quick blitting. if the tile has transparent pixels, then it will be loaded with per-pixel alpha. this is a per-tile, per-image check. if a color key is specified as an argument, or in the tmx data, the per-pixel alpha will not be used at all. if the tileset's image has colorkey transparency set in Tiled, the util_pygam will return images that have their transparency already set. TL;DR: Don't attempt to convert() or convert_alpha() the individual tiles. It is already done for you. """ kwargs['image_loader'] = pygame_image_loader return pytmx.TiledMap(filename, *args, **kwargs) def build_rects(tmxmap, layer, tileset=None, real_gid=None): """generate a set of non-overlapping rects that represents the distribution of the specified gid. useful for generating rects for use in collision detection Use at your own risk: this is experimental...will change in future GID Note: You will need to add 1 to the GID reported by Tiled. :param tmxmap: TiledMap object :param layer: int or string name of layer :param tileset: int or string name of tileset :param real_gid: Tiled GID of the tile + 1 (see note) :return: List of pygame Rect objects """ if isinstance(tileset, int): try: tileset = tmxmap.tilesets[tileset] except IndexError: msg = "Tileset #{0} not found in map {1}." logger.debug(msg.format(tileset, tmxmap)) raise IndexError elif isinstance(tileset, str): try: tileset = [t for t in tmxmap.tilesets if t.name == tileset].pop() except IndexError: msg = "Tileset \"{0}\" not found in map {1}." logger.debug(msg.format(tileset, tmxmap)) raise ValueError elif tileset: msg = "Tileset must be either a int or string. got: {0}" logger.debug(msg.format(type(tileset))) raise TypeError gid = None if real_gid: try: gid, flags = tmxmap.map_gid(real_gid)[0] except IndexError: msg = "GID #{0} not found" logger.debug(msg.format(real_gid)) raise ValueError if isinstance(layer, int): layer_data = tmxmap.get_layer_data(layer) elif isinstance(layer, str): try: layer = [l for l in tmxmap.layers if l.name == layer].pop() layer_data = layer.data except IndexError: msg = "Layer \"{0}\" not found in map {1}." logger.debug(msg.format(layer, tmxmap)) raise ValueError p = itertools.product(range(tmxmap.width), range(tmxmap.height)) if gid: points = [(x, y) for (x, y) in p if layer_data[y][x] == gid] else: points = [(x, y) for (x, y) in p if layer_data[y][x]] rects = simplify(points, tmxmap.tilewidth, tmxmap.tileheight) return rects def simplify(all_points, tilewidth, tileheight): """Given a list of points, return list of rects that represent them kludge: "A kludge (or kluge) is a workaround, a quick-and-dirty solution, a clumsy or inelegant, yet effective, solution to a problem, typically using parts that are cobbled together." -- wikipedia turn a list of points into a rects adjacent rects will be combined. plain english: the input list must be a list of tuples that represent the areas to be combined into rects the rects will be blended together over solid groups so if data is something like: 0 1 1 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 you'll have the 4 rects that mask the area like this: ..######...... ..####........ ..........##.. ..........##.. .............. ....########## pretty cool, right? there may be cases where the number of rectangles is not as low as possible, but I haven't found that it is excessively bad. certainly much better than making a list of rects, one for each tile on the map! """ def pick_rect(points, rects): ox, oy = sorted([(sum(p), p) for p in points])[0][1] x = ox y = oy ex = None while 1: x += 1 if not (x, y) in points: if ex is None: ex = x - 1 if (ox, y + 1) in points: if x == ex + 1: y += 1 x = ox else: y -= 1 break else: if x <= ex: y -= 1 break c_rect = pygame.Rect(ox * tilewidth, oy * tileheight, (ex - ox + 1) * tilewidth, (y - oy + 1) * tileheight) rects.append(c_rect) rect = pygame.Rect(ox, oy, ex - ox + 1, y - oy + 1) kill = [p for p in points if rect.collidepoint(p)] [points.remove(i) for i in kill] if points: pick_rect(points, rects) rect_list = [] while all_points: pick_rect(all_points, rect_list) return rect_list