diff options
Diffstat (limited to 'external/pytmx/util_pygame.py')
-rw-r--r-- | external/pytmx/util_pygame.py | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/external/pytmx/util_pygame.py b/external/pytmx/util_pygame.py new file mode 100644 index 0000000..7744e80 --- /dev/null +++ b/external/pytmx/util_pygame.py @@ -0,0 +1,269 @@ +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 |