# HG changeset patch # User Radomir Dopieralski # Date 1341004008 -7200 # Node ID ca34c14ea3a85d507276607601e2d15db8191a1f # Parent 2a0f43c87b80aeee5e1dc80bde4fde1ef4d3ebc0 Files, resources, objects diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/__init__.py diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/__main__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/__main__.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from game import Game + + +game = Game() +game.load_level('level.tmx') +game.run() diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/control.png Binary file 004/000/control.png has changed diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/effects.png Binary file 004/000/effects.png has changed diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/game.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/game.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +import pygame +import pygame.locals as pg + +import level +import sprites + + +class Game(object): + def __init__(self): + pygame.display.init() + pygame.font.init() + self.screen = pygame.display.set_mode((800, 600)) + self.screen.fill((0x66, 0x66, 0x66)) + self.keys = { + pg.K_UP: (self.move_hero, (0, -1)), + pg.K_DOWN: (self.move_hero, (0, 1)), + pg.K_LEFT: (self.move_hero, (-1, 0)), + pg.K_RIGHT: (self.move_hero, (1, 0)), + pg.K_ESCAPE: (self.quit,), + } + self.shadow_sprites = pygame.sprite.RenderUpdates() + self.sprites = pygame.sprite.RenderUpdates() + self.key = None + + def load_level(self, filename): + self.level = level.Level(filename) + self.hero_sprite = sprites.HeroSprite(self.level.hero_position) + self.sprites.add(self.hero_sprite) + self.shadow_sprites.add(sprites.ShadowSprite(self.hero_sprite)) + self.background = self.screen.copy() + self.level.render(self.background) + self.screen.blit(self.background, (0, 0)) + self.sprites.draw(self.screen) + pygame.display.update() + + def quit(self): + self.game_over = True + + def move_hero(self, direction): + if self.level.move_hero(direction): + self.hero_sprite.move(direction) + + def control(self): + for event in pygame.event.get(): + if event.type == pg.QUIT: + self.quit() + elif event.type == pg.KEYDOWN: + self.key = event.key + if not self.hero_sprite.animations: + keys = pygame.key.get_pressed() + for key, action in self.keys.iteritems(): + if keys[key] or self.key == key: + action[0](*action[1:]) + break + self.key = None + + def display(self): + self.sprites.clear(self.screen, self.background) + self.shadow_sprites.clear(self.screen, self.background) + self.sprites.update() + self.shadow_sprites.update() + dirty = [] + dirty.extend(self.shadow_sprites.draw(self.screen)) + dirty.extend(self.sprites.draw(self.screen)) + pygame.display.update(dirty) + + def run(self): + self.clock = pygame.time.Clock() + self.game_over = False + while not self.game_over: + self.display() + self.control() + self.clock.tick(30) + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/level.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/level.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from pytmx import tmxloader + + +class Level(object): + def __init__(self, filename): + self.tiled_map = tmxloader.load_pygame(filename) + self.hero_position = 1, 1 + + def render(self, screen): + w = self.tiled_map.tilewidth + h = self.tiled_map.tileheight + get_image = self.tiled_map.getTileImage + for l, layer in enumerate(self.tiled_map.tilelayers): + for y in xrange(self.tiled_map.height): + for x in xrange(self.tiled_map.width): + image = get_image(x, y, l) + if image: + screen.blit(image, (x * w, y * h)) + x, y = self.hero_position + + def get_properties(self, (x, y)): + properties = {} + for l, layer in enumerate(self.tiled_map.tilelayers): + properties.update(self.tiled_map.getTileProperties((x, y, l)) or {}) + return properties + + def is_blocked(self, (x, y)): + return self.get_properties((x, y)).get('block') + + def move_hero(self, (dx, dy)): + x, y = self.hero_position + if not self.is_blocked((x + dx, y + dy)): + self.hero_position = x + dx, y + dy + return True + return False + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/level.tmx --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/level.tmx Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + eJxjZGBgYKQDpiUY7nYwUYBH7Ri1Y9SOoWEHNcFws4PWGABJRwF/ + + + + + eJyTZ2BgkKcDVqIhHu52UDPcR6Id+MJ61A762zFc0hW17aBHWTJU7aA1BgDmfzlJ + + + + + eJy1VdtqwzAMFWQbFHaBwNq1DyFhD92lD7Zr//+v1WY65ERTRyFMIOzKro6OJCtJRJL8SNA1qi3KLNG5x8L/xd1E+7uqH/+g94Q7OnF1tM/mrNBanDt91XPVQeZcjHT/tep2he4Ia9A1EI9i7EJ7rk3642yk/UB+rN1KvNEGX4+Or2YveoYcIta2f6u6d/RAPsDnVh5BMXA+mVizxvRu4mFfmeJFPc4OP+RjonPwFcXge168ydiBLbJ8g5P63hAOYzw4voABns3ey9xXQZb8OVdFOWXFsPMAGB3xQJ3whry++qr67ehJzzE/PB4Wm+0v9H87n7jO3BuoLXLqYXTyu+acMwj7tdjwhXeAvDOPo6ybg5+y7FGuB3qlSSa+ntgZCWk+evrN7+NJ1s3Ba/qsWODC3xbuRc47vjtB5tlh5yMk0noBeX8qEg== + + + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/pytmx/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/pytmx/__init__.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,5 @@ +from tmxloader import load_tmx, load_pygame +from pytmx import TiledElement, TiledMap, TiledTileset, TiledLayer, \ + TiledObjectGroup, TiledObject +from utils import buildDistributionRects + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/pytmx/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/pytmx/constants.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,11 @@ +# internal flags +TRANS_FLIPX = 1 +TRANS_FLIPY = 2 +TRANS_ROT = 4 + + +# Tiled gid flags +GID_TRANS_FLIPX = 1<<31 +GID_TRANS_FLIPY = 1<<30 +GID_TRANS_ROT = 1<<29 + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/pytmx/pytmx.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/pytmx/pytmx.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,572 @@ +from itertools import chain, product +from xml.etree import ElementTree +from collections import defaultdict +from utils import decode_gid, types, parse_properties, read_points +from constants import * + + + +""" +Class declarations and other important stuff +""" + + + +class TiledElement(object): + def set_properties(self, node): + """ + read the xml attributes and tiled "properties" from a xml node and fill + in the values into the object's dictionary. Names will be checked to + make sure that they do not conflict with reserved names. + """ + + # set the attributes reserved for tiled + [ setattr(self, k, types[str(k)](v)) for (k,v) in node.items() ] + + # set the attributes that are derived from tiled 'properties' + for k,v in parse_properties(node).items(): + if k in self.reserved: + msg = "{} \"{}\" has a property called \"{}\"" + print msg.format(self.__class__.__name__, self.name, k, self.__class__.__name__) + msg = "This name is reserved for {} objects and can cannot be used." + print msg.format(self.__class__.__name__) + print "Please change the name in Tiled and try again." + print v + raise ValueError + setattr(self, k, types[str(k)](v)) + + +class TiledMap(TiledElement): + """ + Contains the tile layers, tile images, object groups, and objects from a + Tiled TMX map. + """ + + reserved = "version orientation width height tilewidth tileheight properties tileset layer objectgroup".split() + + + def __init__(self, filename): + from collections import defaultdict + + TiledElement.__init__(self) + self.tilesets = [] # list of TiledTileset objects + self.tilelayers = [] # list of TiledLayer objects + self.objectgroups = [] # list of TiledObjectGroup objects + self.tile_properties = {} # dict of tiles that have metadata + self.filename = filename + + # only used tiles are actually loaded, so there will be a difference + # between the GID's in the Tile map data (tmx) and the data in this + # class and the layers. This dictionary keeps track of that difference. + self.gidmap = defaultdict(list) + + # should be filled in by a loader function + self.images = [] + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 + self.height = 0 + self.tilewidth = 0 + self.tileheight = 0 + + self.imagemap = {} # mapping of gid and trans flags to real gids + self.maxgid = 1 + + self.load() + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.filename) + + + def getTileImage(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is not valid." + raise Exception, msg.format(x, y, layer) + except TypeError: + msg = "Tiles must be specified in integers." + raise TypeError, msg + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid" + raise Exception, msg.format(x, y, layer) + + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + useful if you don't want to repeatedly call getTileImage + """ + + raise NotImplementedError + + + def getObjects(self): + """ + Return iterator of all the objects associated with this map + """ + + return chain(*(i for i in self.objectgroups)) + + + def getTileProperties(self, (x, y, layer)): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid." + raise Exception, msg.format(x, y, layer) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + except KeyError: + return None + + + def getLayerData(self, layer): + """ + Return the data for a layer. + + Data is an array of arrays. + + >>> pos = data[y][x] + """ + + try: + return self.tilelayers[layer].data + except IndexError: + msg = "Layer {} does not exist." + raise ValueError, msg.format(layer) + + + def getTileLocation(self, gid): + # experimental way to find locations of a tile by the GID + + p = product(xrange(self.width), + xrange(self.height), + xrange(len(self.tilelayers))) + + return [ (x,y,l) for (x,y,l) in p + if self.tilelayers[l].data[y][x] == gid ] + + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + + + def setTileProperties(self, gid, d): + """ + set the properties of a tile by GID. + must use a standard python dict as d + """ + + try: + self.tile_properties[gid] = d + except KeyError: + msg = "GID #{} does not exist." + raise ValueError, msg.format(gid) + + + def getTilePropertiesByLayer(self, layer): + """ + Return a list of tile properties (dict) in use in this tile layer. + """ + + try: + layer = int(layer) + except: + msg = "Layer must be an integer. Got {} instead." + raise ValueError, msg.format(type(layer)) + + p = product(range(self.width),range(self.height)) + layergids = set( self.tilelayers[layer].data[y][x] for x, y in p ) + + props = [] + for gid in layergids: + try: + props.append((gid, self.tile_properties[gid])) + except: + continue + + return props + + + def registerGID(self, real_gid, flags=0): + """ + used to manage the mapping of GID between the tmx data and the internal + data. + + number returned is gid used internally + """ + + if real_gid: + try: + return self.imagemap[(real_gid, flags)][0] + except KeyError: + # this tile has not been encountered before, or it has been + # transformed in some way. make a new GID for it. + gid = self.maxgid + self.maxgid += 1 + self.imagemap[(real_gid, flags)] = (gid, flags) + self.gidmap[real_gid].append((gid, flags)) + return gid + + else: + return 0 + + + + def mapGID(self, real_gid): + """ + used to lookup a GID read from a TMX file's data + """ + + try: + return self.gidmap[int(real_gid)] + except KeyError: + return None + except TypeError: + msg = "GID's must be an integer" + raise TypeError, msg + + + def loadTileImages(self, filename): + raise NotImplementedError + + + def load(self): + """ + parse a map node from a tiled tmx file + """ + etree = ElementTree.parse(self.filename).getroot() + self.set_properties(etree) + + # initialize the gid mapping + self.imagemap[(0,0)] = 0 + + for node in etree.findall('layer'): + self.tilelayers.append(TiledLayer(self, node)) + + for node in etree.findall('objectgroup'): + self.objectgroups.append(TiledObjectGroup(self, node)) + + for node in etree.findall('tileset'): + self.tilesets.append(TiledTileset(self, node)) + + # "tile objects", objects with a GID, have need to have their + # attributes set after the tileset is loaded + for o in self.getObjects(): + p = self.getTilePropertiesByGID(o.gid) + if p: + o.name = "TileObject" + o.__dict__.update(p) + + + @property + def visibleTileLayers(self): + return [layer for layer in self.tilelayers if layer.visible] + + +class TiledTileset(TiledElement): + reserved = "firstgid source name tilewidth tileheight spacing margin image tile properties".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.firstgid = 0 + self.source = None + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + self.tiles = {} + + self.parse(node) + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a tileset element and return a tileset object and properties for + tiles as a dict + + a bit of mangling is doew here so that tilesets that have external + TSX files appear the same as those that don't + """ + import os + + + # if true, then node references an external tileset + source = node.get('source', False) + if source: + if source[-4:].lower() == ".tsx": + + # external tilesets don't save this, store it for later + self.firstgid = int(node.get('firstgid')) + + # we need to mangle the path - tiled stores relative paths + dirname = os.path.dirname(self.parent.filename) + path = os.path.abspath(os.path.join(dirname, source)) + try: + node = ElementTree.parse(path).getroot() + except IOError: + msg = "Cannot load external tileset: {}" + raise Exception, msg.format(path) + + else: + msg = "Found external tileset, but cannot handle type: {}" + raise Exception, msg.format(self.source) + + self.set_properties(node) + + # since tile objects [probably] don't have a lot of metadata, + # we store it seperately in the parent (a TiledMap instance) + for child in node.iter('tile'): + real_gid = int(child.get("id")) + p = parse_properties(child) + p['width'] = self.tilewidth + p['height'] = self.tileheight + for gid, flags in self.parent.mapGID(real_gid + self.firstgid): + self.parent.setTileProperties(gid, p) + + + image_node = node.find('image') + self.source = image_node.get('source') + self.trans = image_node.get("trans", None) + + +class TiledLayer(TiledElement): + reserved = "name x y width height opacity properties data".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.data = [] + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = True + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a layer element + """ + from utils import group + from itertools import product, imap + from struct import unpack + import array + + self.set_properties(node) + + data = None + next_gid = None + + data_node = node.find('data') + + encoding = data_node.get("encoding", None) + if encoding == "base64": + from base64 import decodestring + data = decodestring(data_node.text.strip()) + + elif encoding == "csv": + next_gid = imap(int, "".join( + line.strip() for line in data_node.text.strip() + ).split(",")) + + elif encoding: + msg = "TMX encoding type: {} is not supported." + raise Exception, msg.format(encoding) + + compression = data_node.get("compression", None) + if compression == "gzip": + from StringIO import StringIO + import gzip + with gzip.GzipFile(fileobj=StringIO(data)) as fh: + data = fh.read() + + elif compression == "zlib": + import zlib + data = zlib.decompress(data) + + elif compression: + msg = "TMX compression type: {} is not supported." + raise Exception, msg.format(str(attr["compression"])) + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + # TODO: this will probably raise an exception if there are no tiles + if encoding == next_gid == None: + def get_children(parent): + for child in parent.findall('tile'): + yield int(child.get('gid')) + + next_gid = get_children(data_node) + + elif data: + # data is a list of gid's. cast as 32-bit ints to format properly + # create iterator to effeciently parse data + next_gid=imap(lambda i:unpack("".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a objectgroup element and return a object group + """ + + self.set_properties(node) + + for child in node.findall('object'): + o = TiledObject(self.parent, child) + self.append(o) + + +class TiledObject(TiledElement): + reserved = "name type x y width height gid properties polygon polyline image".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + self.set_properties(node) + + # correctly handle "tile objects" (object with gid set) + if self.gid: + self.gid = self.parent.registerGID(self.gid) + + polygon = node.find('polygon') + if polygon is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polygon.get('points')) + self.closed = 1 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) + + polyline = node.find('polyline') + if polyline is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polyline.get('points')) + self.closed = 0 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/pytmx/tmxloader.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/pytmx/tmxloader.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,304 @@ +""" +Map loader for TMX Files +bitcraft (leif dot theden at gmail.com) +v.15 - for python 2.7 + +If you have any problems or suggestions, please contact me via email. +Tested with Tiled 0.8.1 for Mac. + +released under the LGPL v3 + +=============================================================================== + +This map loader can be used to load maps created in the Tiled map editor. It +provides a simple way to get tiles and associated metadata so that you can draw +a map onto the screen. It does not force you to draw your map in any +particular way. + +This is *not* a rendering engine. It will load the data that is necessary to +render a map onto the screen. All tiles will be loaded into in memory and +available to blit onto the screen. + + +Design Goals: + Simple api + Memory efficient and fast + +Features: + Loads data and "properties" metadata from Tile's TMX format + "Properties" for: maps, tilesets, layers, objectgroups, objects, and tiles + Point data for polygon and polyline objects + Automatic flipping and rotation of tiles + Supports base64, csv, gzip, zlib and uncompressed XML + Image loading with pygame + +New in .15: + loader: fixed issue where objects with tile gid did not load properties + loader: polygon and polyline objects + loader: new lookup methods use iterators + loader: loading function moved into classes + loader: data/images can be reloaded on the fly + loader: uses etree for faster xml parsing + +New in .14: + loader: Fixed gid lookup for "buildDistributionRects" + loader: Added useful output to a few classes "__repr__" + loader: Fixed a gid mapping issue that broke rotated tiles + pygame: fixed colorkey handling + pygame: correctly handles margins and spacing between tiles in tilesets + pygame: b/c of changes, now correctly renders tiled's example maps + added scrolling demo + +New in .13: + loader: Renamed "get_tile_image" to "getTileImage" + loader: Removed duplicates returned from getTilePropertiesByLayer + loader: Modified confusing messages for GID errors + loader: Fixed bug where transformed tile properties are not available + loader: No longer loads metadata for tiles that are not used + loader: Reduced tile cache to 256 unique tiles + loader: Removed 'visible' from list of reserved words + loader: Added 'buildDistributionRects' and maputils module + loader: Added some misc. functions for retrieving properties + pygame: Smarter tile management made tile loading cache useless; removed it + pygame: pygame.RLEACCEL flag added when appropriate + +New in .12: + loader: Fixed bug where tile properties could contain reserved words + loader: Reduced size of image index by only allocating space for used tiles + +New in .11: + loader: Added support for tileset properties + loader: Now checks for property names that are reserved for internal use + loader: Added support for rotated tiles + pygame: Only the tiles that are used in the map will be loaded into memory + pygame: Added support for rotated tiles + pygame: Added option to force a bitsize (depth) for surfaces + pygame: Added option to convert alpha transparency to colorkey transparency + pygame: Tilesets no longer load with per-pixel alphas by default + pygame: Colorkey transparency should be correctly handled now + + +NOTES: + +* The Tiled "properties" have reserved names. + +If you use "properties" for any of the following object types, you cannot use +any of theese words as a name for your property. A ValueError will be raised +if there are any conflicts. + +As of 0.8.1, these values are: + +map: version, orientation, width, height, tilewidth, tileheight + properties, tileset, layer, objectgroup + +tileset: firstgid, source, name, tilewidth, tileheight, spacing, margin, + image, tile, properties + +tile: id, image, properties + +layer: name, x, y, width, height, opacity, properties, data + +objectgroup: name, color, x, y, width, height, opacity, object, properties + +object: name, type, x, y, width, height, gid, properties, polygon, + polyline, image + + + +I have been intentionally not including a module rendering utility since +rendering a map will have different requirements for every situation. However, +I can appreciate that some poeple won't understand how it works unless they see +it, so I am including a sample map and viewer. It includes a scrolling/zooming +renderer. They are for demonstation purposes, and may not be suitable for all +projects. Use at your own risk. + +I've also included a copy of this loader that may work with python 3.x. I +personally do not think that python 3.x should be used with pygame, yet (and I +am not the only person). You can try it if you insist on using pygame with +python 3.x, but I don't update that often. + +=============================================================================== + +Basic usage sample: + + >>> from pytmx import tmxloader + >>> tmxdata = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "getTileImage": + + >>> image = tmxdata.getTileImage(x, y, layer) + >>> screen.blit(position, image) + + +Maps, tilesets, layers, objectgroups, and objects all have a simple way to +access metadata that was set inside tiled: they all become object attributes. + + >>> layer = tmxdata.layers[0] + >>> print layer.tilewidth + 32 + >>> print layer.weather + 'sunny' + + +Tiles properties are the exception here, and must be accessed through +"getTileProperties". The data is a regular Python dictionary: + + >>> tile = tmxdata.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + + + +Please see the TiledMap class for some api information. +""" +from utils import types +from constants import * + + + +def load_tmx(filename, *args, **kwargs): + # for .14 compatibility + from pytmx import TiledMap + + tiledmap = TiledMap(filename) + return tiledmap + + +def load_images_pygame(tmxdata, mapping, *args, **kwargs): + """ + due to the way the tiles are loaded, they will be in the same pixel format + as the display when it is loaded. take this into consideration if you + intend to support different screen pixel formats. + + by default, the images will not have per-pixel alphas. this can be + changed by including "pixelalpha=True" in the keywords. this will result + in much slower blitting speeds. + + if the tileset's image has colorkey transparency set in Tiled, the loader + will return images that have their transparency already set. using a + tileset with colorkey transparency will greatly increase the speed of + rendering the map. + + optionally, you can force the loader to strip the alpha channel of the + tileset image and to fill in the missing areas with a color, then use that + new color as a colorkey. the resulting tiles will render much faster, but + will not preserve the transparency of the tile if it uses partial + transparency (which you shouldn't be doing anyway, this is SDL). + """ + from itertools import product + from pygame import Surface, mask + import pygame, os + + + def handle_transformation(tile, flags): + if flags: + fx = flags & TRANS_FLIPX == TRANS_FLIPX + fy = flags & TRANS_FLIPY == TRANS_FLIPY + r = flags & TRANS_ROT == TRANS_ROT + + if r: + # not sure why the flip is required...but it is. + newtile = pygame.transform.rotate(tile, 270) + newtile = pygame.transform.flip(newtile, 1, 0) + + if fx or fy: + newtile = pygame.transform.flip(newtile, fx, fy) + + elif fx or fy: + newtile = pygame.transform.flip(tile, fx, fy) + + # preserve any flags that may have been lost after the transformation + return newtile.convert(tile) + + else: + return tile + + + pixelalpha = kwargs.get("pixelalpha", False) + force_colorkey = kwargs.get("force_colorkey", False) + force_bitdepth = kwargs.get("depth", False) + + if force_colorkey: + try: + force_colorkey = pygame.Color(*force_colorkey) + except: + msg = "Cannot understand color: {}" + raise Exception, msg.format(force_colorkey) + + tmxdata.images = [0] * tmxdata.maxgid + + for firstgid, t in sorted((t.firstgid, t) for t in tmxdata.tilesets): + path = os.path.join(os.path.dirname(tmxdata.filename), t.source) + + image = pygame.image.load(path) + + w, h = image.get_size() + tile_size = (t.tilewidth, t.tileheight) + real_gid = t.firstgid - 1 + + if t.trans: + tileset_colorkey = pygame.Color("#{}".format(t.trans)) + + # i dont agree with margins and spacing, but i'll support it anyway + # such is life. okay.jpg + tilewidth = t.tilewidth + t.spacing + tileheight = t.tileheight + t.spacing + + # some tileset images may be slightly larger than the tile area + # ie: may include a banner, copyright, ect. this compensates for that + width = ((int((w-t.margin*2) + t.spacing) / tilewidth) * tilewidth) - t.spacing + height = ((int((h-t.margin*2) + t.spacing) / tileheight) * tileheight) - t.spacing + + # using product avoids the overhead of nested loops + p = product(xrange(t.margin, height+t.margin, tileheight), + xrange(t.margin, width+t.margin, tilewidth)) + + for (y, x) in p: + real_gid += 1 + gids = tmxdata.mapGID(real_gid) + if gids == []: continue + + # we do some tests to correctly handle the tile and set the right + # blitting flags. just grab a section of it. + temp = image.subsurface(((x,y), tile_size)) + + # count the number of pixels in the tile that are not transparent + px = mask.from_surface(temp).count() + + # there are no transparent pixels in the image + if px == tile_size[0] * tile_size[1]: + tile = temp.convert() + + # there are transparent pixels, and set to force a colorkey + elif force_colorkey: + tile = Surface(tile_size) + tile.fill(force_colorkey) + tile.blit(temp, (0,0)) + tile.set_colorkey(force_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and tiled set a colorkey + elif t.trans: + tile = temp.convert() + tile.set_colorkey(tileset_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and set for perpixel alpha + elif pixelalpha: + tile = temp.convert_alpha() + + # there are transparent pixels, and we won't handle them + else: + tile = temp.convert() + + for gid, flags in gids: + tmxdata.images[gid] = handle_transformation(tile, flags) + + +def load_pygame(filename, *args, **kwargs): + tmxdata = load_tmx(filename, *args, **kwargs) + load_images_pygame(tmxdata, None, *args, **kwargs) + + return tmxdata + + + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/000/pytmx/tmxloader3.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/000/pytmx/tmxloader3.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,612 @@ +""" +Map loader for TMX Files +bitcraft (leif.theden at gmail.com) +v.7 -- for python 3.x + +If you have any problems, please contact me via email. +Tested with Tiled 0.7.1 for Mac. + +released under the LGPL v3 + +====================================================================== +Design Goals: + Simple api + Memory efficient and fast + Quick access to tiles, attributes, and properties + +Non-Goals: + Rendering + +Works: + Pygame image loading + Map loading with all required types + Properties for all types: maps, layers, objects, tiles + Automatic flipping of tiles + +Todo: + Pygame: test colorkey transparency + + Optimized for maps that do not make heavy use of tile + properties. If i find that it is used a lot then i can rework + it for better performance. + +====================================================================== + +Basic usage sample: + + >>> import tmxloader + >>> tiledmap = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "get_tile_image": + + >>> image = tiledmap.get_tile_image(x, y, layer) + >>> screen.blit(position, image) + + +Layers, objectgroups, tilesets, and maps all have a simple way to access +metadata that was set inside tiled: they all become class attributes. + + >>> print(layer.tilewidth) + 32 + >>> print(layer.weather) + 'sunny' + + +Tiles are the exception here, and must be accessed through "getTileProperties" +and are regular Python dictionaries: + + >>> tile = tiledmap.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + +""" + +from itertools import chain + + +# internal flags +FLIP_X = 1 +FLIP_Y = 2 + + +# Tiled gid flags +GID_FLIP_X = 1<<31 +GID_FLIP_Y = 1<<30 + + +class TiledElement(object): + pass + +class TiledMap(TiledElement): + """ + not really useful unless "loaded" ie: don't instance directly. + see the pygame loader for inspiration + """ + + def __init__(self): + TiledElement.__init__(self) + self.layers = [] # list of all layer types (tile layers + object layers) + self.tilesets = [] # list of TiledTileset objects + self.tilelayers = [] # list of TiledLayer objects + self.objectgroups = [] # list of TiledObjectGroup objects + self.tile_properties = {} # dict of tiles that have additional metadata (properties) + self.filename = None + + # this is a work around to tiled's strange way of storing gid's + self.images = [0] + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 + self.height = 0 + self.tilewidth = 0 + self.tileheight = 0 + + def get_tile_image(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + usefull if you don't want to repeatedly call get_tile_image + probably not the most effecient way of doing this, but oh well. + """ + + raise NotImplementedError + + def getObjects(self): + """ + Return iterator all of the objects associated with this map + """ + + return chain(*[ i.objects for i in self.objectgroups ]) + + def getTileProperties(self, x, y, layer): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + +# the following classes get their attributes filled in with the loader + +class TiledTileset(TiledElement): + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.firstgid = 0 + self.lastgid = 0 + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + +class TiledLayer(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.data = None + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = 1 + +class TiledObjectGroup(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.objects = [] + + # defaults from the specification + self.name = None + +class TiledObject(TiledElement): + __slots__ = ['name', 'type', 'x', 'y', 'width', 'height', 'gid'] + + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + +def load_tmx(filename): + """ + Utility function to parse a Tiled TMX and return a usable object. + Images will not be loaded, so probably not useful to call this directly + + See the load_pygame func for an idea of what to do + """ + + from xml.dom.minidom import parse + from itertools import tee, islice, chain + from collections import defaultdict + from struct import unpack + import array, os + + # used to change the unicode string returned from minidom to + # proper python variable types. + types = { + "version": float, + "orientation": str, + "width": int, + "height": int, + "tilewidth": int, + "tileheight": int, + "firstgid": int, + "source": str, + "name": str, + "spacing": int, + "margin": int, + "source": str, + "trans": str, + "id": int, + "opacity": float, + "visible": bool, + "encoding": str, + "compression": str, + "gid": int, + "type": str, + "x": int, + "y": int, + "value": str, + } + + def pairwise(iterable): + # return a list as a sequence of pairs + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + def group(l, n): + # return a list as a sequence of n tuples + return zip(*[islice(l, i, None, n) for i in range(n)]) + + def parse_properties(node): + """ + parse a node and return a dict that represents a tiled "property" + """ + + d = {} + + for child in node.childNodes: + if child.nodeName == "properties": + for subnode in child.getElementsByTagName("property"): + # the "properties" from tiled's tmx have an annoying + # quality that "name" and "value" is included as part of it. + # so we mangle it to get that stuff out. + d.update(dict(pairwise([ str(i.value) for i in list(subnode.attributes.values()) ]))) + + return d + + def get_properties(node): + """ + parses a node and returns a dict that contains the data from the node's + attributes and any data from "property" elements as well. + """ + + d = {} + + # get tag attributes + d.update(get_attributes(node)) + + # get vlues of the properties element, if any + d.update(parse_properties(node)) + + return d + + def set_properties(obj, node): + """ + read the xml attributes and tiled "properties" from a xml node and fill in + the values into an object's dictionary + """ + + [ setattr(obj, k, v) for k,v in list(get_properties(node).items()) ] + + def get_attributes(node): + """ + get the attributes from a node and fix them to the correct type + """ + + d = defaultdict(lambda:None) + + for k, v in list(node.attributes.items()): + k = str(k) + d[k] = types[k](v) + + return d + + def decode_gid(raw_gid): + # gid's are encoded with extra information + # as of 0.7.0 it determines if the tile should be flipped when rendered + + flags = 0 + if raw_gid & GID_FLIP_X == GID_FLIP_X: flags += FLIP_X + if raw_gid & GID_FLIP_Y == GID_FLIP_Y: flags += FLIP_Y + gid = raw_gid & ~(GID_FLIP_X | GID_FLIP_Y) + + return gid, flags + + + def parse_map(node): + """ + parse a map node from a tiled tmx file + return a tiledmap + """ + + tiledmap = TiledMap() + tiledmap.filename = filename + set_properties(tiledmap, map_node) + + for node in map_node.getElementsByTagName("tileset"): + t, tiles = parse_tileset(node) + tiledmap.tilesets.append(t) + tiledmap.tile_properties.update(tiles) + + for node in dom.getElementsByTagName("layer"): + l = parse_layer(tiledmap.tilesets, node) + tiledmap.tilelayers.append(l) + tiledmap.layers.append(l) + + for node in dom.getElementsByTagName("objectgroup"): + o = parse_objectgroup(node) + tiledmap.objectgroups.append(o) + tiledmap.layers.append(o) + + return tiledmap + + + def parse_tileset(node, firstgid=None): + """ + parse a tileset element and return a tileset object and properties for tiles as a dict + """ + + tileset = TiledTileset() + set_properties(tileset, node) + tiles = {} + + if firstgid != None: + tileset.firstgid = firstgid + + # since tile objects probably don't have a lot of metadata, + # we store it seperately from the class itself + for child in node.childNodes: + if child.nodeName == "tile": + p = get_properties(child) + gid = p["id"] + tileset.firstgid + del p["id"] + tiles[gid] = p + + # check for tiled "external tilesets" + if hasattr(tileset, "source"): + if tileset.source[-4:].lower() == ".tsx": + try: + # we need to mangle the path some because tiled stores relative paths + path = os.path.join(os.path.dirname(filename), tileset.source) + tsx = parse(path) + except IOError: + raise IOError("Cannot load external tileset: " + path) + + tileset_node = tsx.getElementsByTagName("tileset")[0] + tileset, tiles = parse_tileset(tileset_node, tileset.firstgid) + else: + raise Exception("Found external tileset, but cannot handle type: " + tileset.source) + + # if we have an "image" tag, process it here + try: + image_node = node.getElementsByTagName("image")[0] + except IndexError: + pass + else: + attr = get_attributes(image_node) + tileset.source = attr["source"] + tileset.trans = attr["trans"] + + # calculate the number of tiles in this tileset + x, r = divmod(attr["width"], tileset.tilewidth) + y, r = divmod(attr["height"], tileset.tileheight) + + tileset.lastgid = tileset.firstgid + x + y + + return tileset, tiles + + + def parse_layer(tilesets, node): + """ + parse a layer element and return a layer object + + tilesets is required since we need to mangle gid's here + """ + + layer = TiledLayer() + layer.data = [] + layer.flipped_tiles = [] + set_properties(layer, node) + + data = None + next_gid = None + + data_node = node.getElementsByTagName("data")[0] + attr = get_attributes(data_node) + + if attr["encoding"] == "base64": + from base64 import b64decode + data = b64decode(bytes(data_node.lastChild.nodeValue, 'ascii')) + + elif attr["encoding"] == "csv": + next_gid = map(int, "".join([line.strip() for line in data_node.lastChild.nodeValue]).split(",")) + + elif not attr["encoding"] == None: + raise Exception("TMX encoding type: " + str(attr["encoding"]) + " is not supported.") + + if attr["compression"] == "gzip": + from io import BytesIO + import gzip + with gzip.GzipFile(fileobj=BytesIO(data)) as fh: + data = fh.read() + + elif not attr["compression"] == None: + raise Exception("TMX compression type: " + str(attr["compression"]) + " is not supported.") + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + if attr["encoding"] == next_gid == None: + def get_children(parent): + for child in parent.getElementsByTagName("tile"): + yield int(child.getAttribute("gid")) + + next_gid = get_children(data_node) + + elif not data == None: + # cast the data as a list of 32-bit integers + def u(i): return unpack("".format(self.__class__.__name__, self.filename) + + + def getTileImage(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is not valid." + raise Exception, msg.format(x, y, layer) + except TypeError: + msg = "Tiles must be specified in integers." + raise TypeError, msg + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid" + raise Exception, msg.format(x, y, layer) + + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + useful if you don't want to repeatedly call getTileImage + """ + + raise NotImplementedError + + + def getObjects(self): + """ + Return iterator of all the objects associated with this map + """ + + return chain(*(i for i in self.objectgroups)) + + + def getTileProperties(self, (x, y, layer)): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid." + raise Exception, msg.format(x, y, layer) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + except KeyError: + return None + + + def getLayerData(self, layer): + """ + Return the data for a layer. + + Data is an array of arrays. + + >>> pos = data[y][x] + """ + + try: + return self.tilelayers[layer].data + except IndexError: + msg = "Layer {} does not exist." + raise ValueError, msg.format(layer) + + + def getTileLocation(self, gid): + # experimental way to find locations of a tile by the GID + + p = product(xrange(self.width), + xrange(self.height), + xrange(len(self.tilelayers))) + + return [ (x,y,l) for (x,y,l) in p + if self.tilelayers[l].data[y][x] == gid ] + + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + + + def setTileProperties(self, gid, d): + """ + set the properties of a tile by GID. + must use a standard python dict as d + """ + + try: + self.tile_properties[gid] = d + except KeyError: + msg = "GID #{} does not exist." + raise ValueError, msg.format(gid) + + + def getTilePropertiesByLayer(self, layer): + """ + Return a list of tile properties (dict) in use in this tile layer. + """ + + try: + layer = int(layer) + except: + msg = "Layer must be an integer. Got {} instead." + raise ValueError, msg.format(type(layer)) + + p = product(range(self.width),range(self.height)) + layergids = set( self.tilelayers[layer].data[y][x] for x, y in p ) + + props = [] + for gid in layergids: + try: + props.append((gid, self.tile_properties[gid])) + except: + continue + + return props + + + def registerGID(self, real_gid, flags=0): + """ + used to manage the mapping of GID between the tmx data and the internal + data. + + number returned is gid used internally + """ + + if real_gid: + try: + return self.imagemap[(real_gid, flags)][0] + except KeyError: + # this tile has not been encountered before, or it has been + # transformed in some way. make a new GID for it. + gid = self.maxgid + self.maxgid += 1 + self.imagemap[(real_gid, flags)] = (gid, flags) + self.gidmap[real_gid].append((gid, flags)) + return gid + + else: + return 0 + + + + def mapGID(self, real_gid): + """ + used to lookup a GID read from a TMX file's data + """ + + try: + return self.gidmap[int(real_gid)] + except KeyError: + return None + except TypeError: + msg = "GID's must be an integer" + raise TypeError, msg + + + def loadTileImages(self, filename): + raise NotImplementedError + + + def load(self): + """ + parse a map node from a tiled tmx file + """ + etree = ElementTree.parse(self.filename).getroot() + self.set_properties(etree) + + # initialize the gid mapping + self.imagemap[(0,0)] = 0 + + for node in etree.findall('layer'): + self.tilelayers.append(TiledLayer(self, node)) + + for node in etree.findall('objectgroup'): + self.objectgroups.append(TiledObjectGroup(self, node)) + + for node in etree.findall('tileset'): + self.tilesets.append(TiledTileset(self, node)) + + # "tile objects", objects with a GID, have need to have their + # attributes set after the tileset is loaded + for o in self.getObjects(): + p = self.getTilePropertiesByGID(o.gid) + if p: + o.name = "TileObject" + o.__dict__.update(p) + + + @property + def visibleTileLayers(self): + return [layer for layer in self.tilelayers if layer.visible] + + +class TiledTileset(TiledElement): + reserved = "firstgid source name tilewidth tileheight spacing margin image tile properties".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.firstgid = 0 + self.source = None + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + self.tiles = {} + + self.parse(node) + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a tileset element and return a tileset object and properties for + tiles as a dict + + a bit of mangling is doew here so that tilesets that have external + TSX files appear the same as those that don't + """ + import os + + + # if true, then node references an external tileset + source = node.get('source', False) + if source: + if source[-4:].lower() == ".tsx": + + # external tilesets don't save this, store it for later + self.firstgid = int(node.get('firstgid')) + + # we need to mangle the path - tiled stores relative paths + dirname = os.path.dirname(self.parent.filename) + path = os.path.abspath(os.path.join(dirname, source)) + try: + node = ElementTree.parse(path).getroot() + except IOError: + msg = "Cannot load external tileset: {}" + raise Exception, msg.format(path) + + else: + msg = "Found external tileset, but cannot handle type: {}" + raise Exception, msg.format(self.source) + + self.set_properties(node) + + # since tile objects [probably] don't have a lot of metadata, + # we store it seperately in the parent (a TiledMap instance) + for child in node.iter('tile'): + real_gid = int(child.get("id")) + p = parse_properties(child) + p['width'] = self.tilewidth + p['height'] = self.tileheight + for gid, flags in self.parent.mapGID(real_gid + self.firstgid): + self.parent.setTileProperties(gid, p) + + + image_node = node.find('image') + self.source = image_node.get('source') + self.trans = image_node.get("trans", None) + + +class TiledLayer(TiledElement): + reserved = "name x y width height opacity properties data".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.data = [] + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = True + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a layer element + """ + from utils import group + from itertools import product, imap + from struct import unpack + import array + + self.set_properties(node) + + data = None + next_gid = None + + data_node = node.find('data') + + encoding = data_node.get("encoding", None) + if encoding == "base64": + from base64 import decodestring + data = decodestring(data_node.text.strip()) + + elif encoding == "csv": + next_gid = imap(int, "".join( + line.strip() for line in data_node.text.strip() + ).split(",")) + + elif encoding: + msg = "TMX encoding type: {} is not supported." + raise Exception, msg.format(encoding) + + compression = data_node.get("compression", None) + if compression == "gzip": + from StringIO import StringIO + import gzip + with gzip.GzipFile(fileobj=StringIO(data)) as fh: + data = fh.read() + + elif compression == "zlib": + import zlib + data = zlib.decompress(data) + + elif compression: + msg = "TMX compression type: {} is not supported." + raise Exception, msg.format(str(attr["compression"])) + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + # TODO: this will probably raise an exception if there are no tiles + if encoding == next_gid == None: + def get_children(parent): + for child in parent.findall('tile'): + yield int(child.get('gid')) + + next_gid = get_children(data_node) + + elif data: + # data is a list of gid's. cast as 32-bit ints to format properly + # create iterator to effeciently parse data + next_gid=imap(lambda i:unpack("".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a objectgroup element and return a object group + """ + + self.set_properties(node) + + for child in node.findall('object'): + o = TiledObject(self.parent, child) + self.append(o) + + +class TiledObject(TiledElement): + reserved = "name type x y width height gid properties polygon polyline image".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + self.set_properties(node) + + # correctly handle "tile objects" (object with gid set) + if self.gid: + self.gid = self.parent.registerGID(self.gid) + + polygon = node.find('polygon') + if polygon is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polygon.get('points')) + self.closed = 1 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) + + polyline = node.find('polyline') + if polyline is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polyline.get('points')) + self.closed = 0 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/001/pytmx/tmxloader.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/001/pytmx/tmxloader.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,304 @@ +""" +Map loader for TMX Files +bitcraft (leif dot theden at gmail.com) +v.15 - for python 2.7 + +If you have any problems or suggestions, please contact me via email. +Tested with Tiled 0.8.1 for Mac. + +released under the LGPL v3 + +=============================================================================== + +This map loader can be used to load maps created in the Tiled map editor. It +provides a simple way to get tiles and associated metadata so that you can draw +a map onto the screen. It does not force you to draw your map in any +particular way. + +This is *not* a rendering engine. It will load the data that is necessary to +render a map onto the screen. All tiles will be loaded into in memory and +available to blit onto the screen. + + +Design Goals: + Simple api + Memory efficient and fast + +Features: + Loads data and "properties" metadata from Tile's TMX format + "Properties" for: maps, tilesets, layers, objectgroups, objects, and tiles + Point data for polygon and polyline objects + Automatic flipping and rotation of tiles + Supports base64, csv, gzip, zlib and uncompressed XML + Image loading with pygame + +New in .15: + loader: fixed issue where objects with tile gid did not load properties + loader: polygon and polyline objects + loader: new lookup methods use iterators + loader: loading function moved into classes + loader: data/images can be reloaded on the fly + loader: uses etree for faster xml parsing + +New in .14: + loader: Fixed gid lookup for "buildDistributionRects" + loader: Added useful output to a few classes "__repr__" + loader: Fixed a gid mapping issue that broke rotated tiles + pygame: fixed colorkey handling + pygame: correctly handles margins and spacing between tiles in tilesets + pygame: b/c of changes, now correctly renders tiled's example maps + added scrolling demo + +New in .13: + loader: Renamed "get_tile_image" to "getTileImage" + loader: Removed duplicates returned from getTilePropertiesByLayer + loader: Modified confusing messages for GID errors + loader: Fixed bug where transformed tile properties are not available + loader: No longer loads metadata for tiles that are not used + loader: Reduced tile cache to 256 unique tiles + loader: Removed 'visible' from list of reserved words + loader: Added 'buildDistributionRects' and maputils module + loader: Added some misc. functions for retrieving properties + pygame: Smarter tile management made tile loading cache useless; removed it + pygame: pygame.RLEACCEL flag added when appropriate + +New in .12: + loader: Fixed bug where tile properties could contain reserved words + loader: Reduced size of image index by only allocating space for used tiles + +New in .11: + loader: Added support for tileset properties + loader: Now checks for property names that are reserved for internal use + loader: Added support for rotated tiles + pygame: Only the tiles that are used in the map will be loaded into memory + pygame: Added support for rotated tiles + pygame: Added option to force a bitsize (depth) for surfaces + pygame: Added option to convert alpha transparency to colorkey transparency + pygame: Tilesets no longer load with per-pixel alphas by default + pygame: Colorkey transparency should be correctly handled now + + +NOTES: + +* The Tiled "properties" have reserved names. + +If you use "properties" for any of the following object types, you cannot use +any of theese words as a name for your property. A ValueError will be raised +if there are any conflicts. + +As of 0.8.1, these values are: + +map: version, orientation, width, height, tilewidth, tileheight + properties, tileset, layer, objectgroup + +tileset: firstgid, source, name, tilewidth, tileheight, spacing, margin, + image, tile, properties + +tile: id, image, properties + +layer: name, x, y, width, height, opacity, properties, data + +objectgroup: name, color, x, y, width, height, opacity, object, properties + +object: name, type, x, y, width, height, gid, properties, polygon, + polyline, image + + + +I have been intentionally not including a module rendering utility since +rendering a map will have different requirements for every situation. However, +I can appreciate that some poeple won't understand how it works unless they see +it, so I am including a sample map and viewer. It includes a scrolling/zooming +renderer. They are for demonstation purposes, and may not be suitable for all +projects. Use at your own risk. + +I've also included a copy of this loader that may work with python 3.x. I +personally do not think that python 3.x should be used with pygame, yet (and I +am not the only person). You can try it if you insist on using pygame with +python 3.x, but I don't update that often. + +=============================================================================== + +Basic usage sample: + + >>> from pytmx import tmxloader + >>> tmxdata = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "getTileImage": + + >>> image = tmxdata.getTileImage(x, y, layer) + >>> screen.blit(position, image) + + +Maps, tilesets, layers, objectgroups, and objects all have a simple way to +access metadata that was set inside tiled: they all become object attributes. + + >>> layer = tmxdata.layers[0] + >>> print layer.tilewidth + 32 + >>> print layer.weather + 'sunny' + + +Tiles properties are the exception here, and must be accessed through +"getTileProperties". The data is a regular Python dictionary: + + >>> tile = tmxdata.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + + + +Please see the TiledMap class for some api information. +""" +from utils import types +from constants import * + + + +def load_tmx(filename, *args, **kwargs): + # for .14 compatibility + from pytmx import TiledMap + + tiledmap = TiledMap(filename) + return tiledmap + + +def load_images_pygame(tmxdata, mapping, *args, **kwargs): + """ + due to the way the tiles are loaded, they will be in the same pixel format + as the display when it is loaded. take this into consideration if you + intend to support different screen pixel formats. + + by default, the images will not have per-pixel alphas. this can be + changed by including "pixelalpha=True" in the keywords. this will result + in much slower blitting speeds. + + if the tileset's image has colorkey transparency set in Tiled, the loader + will return images that have their transparency already set. using a + tileset with colorkey transparency will greatly increase the speed of + rendering the map. + + optionally, you can force the loader to strip the alpha channel of the + tileset image and to fill in the missing areas with a color, then use that + new color as a colorkey. the resulting tiles will render much faster, but + will not preserve the transparency of the tile if it uses partial + transparency (which you shouldn't be doing anyway, this is SDL). + """ + from itertools import product + from pygame import Surface, mask + import pygame, os + + + def handle_transformation(tile, flags): + if flags: + fx = flags & TRANS_FLIPX == TRANS_FLIPX + fy = flags & TRANS_FLIPY == TRANS_FLIPY + r = flags & TRANS_ROT == TRANS_ROT + + if r: + # not sure why the flip is required...but it is. + newtile = pygame.transform.rotate(tile, 270) + newtile = pygame.transform.flip(newtile, 1, 0) + + if fx or fy: + newtile = pygame.transform.flip(newtile, fx, fy) + + elif fx or fy: + newtile = pygame.transform.flip(tile, fx, fy) + + # preserve any flags that may have been lost after the transformation + return newtile.convert(tile) + + else: + return tile + + + pixelalpha = kwargs.get("pixelalpha", False) + force_colorkey = kwargs.get("force_colorkey", False) + force_bitdepth = kwargs.get("depth", False) + + if force_colorkey: + try: + force_colorkey = pygame.Color(*force_colorkey) + except: + msg = "Cannot understand color: {}" + raise Exception, msg.format(force_colorkey) + + tmxdata.images = [0] * tmxdata.maxgid + + for firstgid, t in sorted((t.firstgid, t) for t in tmxdata.tilesets): + path = os.path.join(os.path.dirname(tmxdata.filename), t.source) + + image = pygame.image.load(path) + + w, h = image.get_size() + tile_size = (t.tilewidth, t.tileheight) + real_gid = t.firstgid - 1 + + if t.trans: + tileset_colorkey = pygame.Color("#{}".format(t.trans)) + + # i dont agree with margins and spacing, but i'll support it anyway + # such is life. okay.jpg + tilewidth = t.tilewidth + t.spacing + tileheight = t.tileheight + t.spacing + + # some tileset images may be slightly larger than the tile area + # ie: may include a banner, copyright, ect. this compensates for that + width = ((int((w-t.margin*2) + t.spacing) / tilewidth) * tilewidth) - t.spacing + height = ((int((h-t.margin*2) + t.spacing) / tileheight) * tileheight) - t.spacing + + # using product avoids the overhead of nested loops + p = product(xrange(t.margin, height+t.margin, tileheight), + xrange(t.margin, width+t.margin, tilewidth)) + + for (y, x) in p: + real_gid += 1 + gids = tmxdata.mapGID(real_gid) + if gids == []: continue + + # we do some tests to correctly handle the tile and set the right + # blitting flags. just grab a section of it. + temp = image.subsurface(((x,y), tile_size)) + + # count the number of pixels in the tile that are not transparent + px = mask.from_surface(temp).count() + + # there are no transparent pixels in the image + if px == tile_size[0] * tile_size[1]: + tile = temp.convert() + + # there are transparent pixels, and set to force a colorkey + elif force_colorkey: + tile = Surface(tile_size) + tile.fill(force_colorkey) + tile.blit(temp, (0,0)) + tile.set_colorkey(force_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and tiled set a colorkey + elif t.trans: + tile = temp.convert() + tile.set_colorkey(tileset_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and set for perpixel alpha + elif pixelalpha: + tile = temp.convert_alpha() + + # there are transparent pixels, and we won't handle them + else: + tile = temp.convert() + + for gid, flags in gids: + tmxdata.images[gid] = handle_transformation(tile, flags) + + +def load_pygame(filename, *args, **kwargs): + tmxdata = load_tmx(filename, *args, **kwargs) + load_images_pygame(tmxdata, None, *args, **kwargs) + + return tmxdata + + + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/001/pytmx/tmxloader3.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/001/pytmx/tmxloader3.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,612 @@ +""" +Map loader for TMX Files +bitcraft (leif.theden at gmail.com) +v.7 -- for python 3.x + +If you have any problems, please contact me via email. +Tested with Tiled 0.7.1 for Mac. + +released under the LGPL v3 + +====================================================================== +Design Goals: + Simple api + Memory efficient and fast + Quick access to tiles, attributes, and properties + +Non-Goals: + Rendering + +Works: + Pygame image loading + Map loading with all required types + Properties for all types: maps, layers, objects, tiles + Automatic flipping of tiles + +Todo: + Pygame: test colorkey transparency + + Optimized for maps that do not make heavy use of tile + properties. If i find that it is used a lot then i can rework + it for better performance. + +====================================================================== + +Basic usage sample: + + >>> import tmxloader + >>> tiledmap = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "get_tile_image": + + >>> image = tiledmap.get_tile_image(x, y, layer) + >>> screen.blit(position, image) + + +Layers, objectgroups, tilesets, and maps all have a simple way to access +metadata that was set inside tiled: they all become class attributes. + + >>> print(layer.tilewidth) + 32 + >>> print(layer.weather) + 'sunny' + + +Tiles are the exception here, and must be accessed through "getTileProperties" +and are regular Python dictionaries: + + >>> tile = tiledmap.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + +""" + +from itertools import chain + + +# internal flags +FLIP_X = 1 +FLIP_Y = 2 + + +# Tiled gid flags +GID_FLIP_X = 1<<31 +GID_FLIP_Y = 1<<30 + + +class TiledElement(object): + pass + +class TiledMap(TiledElement): + """ + not really useful unless "loaded" ie: don't instance directly. + see the pygame loader for inspiration + """ + + def __init__(self): + TiledElement.__init__(self) + self.layers = [] # list of all layer types (tile layers + object layers) + self.tilesets = [] # list of TiledTileset objects + self.tilelayers = [] # list of TiledLayer objects + self.objectgroups = [] # list of TiledObjectGroup objects + self.tile_properties = {} # dict of tiles that have additional metadata (properties) + self.filename = None + + # this is a work around to tiled's strange way of storing gid's + self.images = [0] + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 + self.height = 0 + self.tilewidth = 0 + self.tileheight = 0 + + def get_tile_image(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + usefull if you don't want to repeatedly call get_tile_image + probably not the most effecient way of doing this, but oh well. + """ + + raise NotImplementedError + + def getObjects(self): + """ + Return iterator all of the objects associated with this map + """ + + return chain(*[ i.objects for i in self.objectgroups ]) + + def getTileProperties(self, x, y, layer): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + +# the following classes get their attributes filled in with the loader + +class TiledTileset(TiledElement): + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.firstgid = 0 + self.lastgid = 0 + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + +class TiledLayer(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.data = None + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = 1 + +class TiledObjectGroup(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.objects = [] + + # defaults from the specification + self.name = None + +class TiledObject(TiledElement): + __slots__ = ['name', 'type', 'x', 'y', 'width', 'height', 'gid'] + + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + +def load_tmx(filename): + """ + Utility function to parse a Tiled TMX and return a usable object. + Images will not be loaded, so probably not useful to call this directly + + See the load_pygame func for an idea of what to do + """ + + from xml.dom.minidom import parse + from itertools import tee, islice, chain + from collections import defaultdict + from struct import unpack + import array, os + + # used to change the unicode string returned from minidom to + # proper python variable types. + types = { + "version": float, + "orientation": str, + "width": int, + "height": int, + "tilewidth": int, + "tileheight": int, + "firstgid": int, + "source": str, + "name": str, + "spacing": int, + "margin": int, + "source": str, + "trans": str, + "id": int, + "opacity": float, + "visible": bool, + "encoding": str, + "compression": str, + "gid": int, + "type": str, + "x": int, + "y": int, + "value": str, + } + + def pairwise(iterable): + # return a list as a sequence of pairs + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + def group(l, n): + # return a list as a sequence of n tuples + return zip(*[islice(l, i, None, n) for i in range(n)]) + + def parse_properties(node): + """ + parse a node and return a dict that represents a tiled "property" + """ + + d = {} + + for child in node.childNodes: + if child.nodeName == "properties": + for subnode in child.getElementsByTagName("property"): + # the "properties" from tiled's tmx have an annoying + # quality that "name" and "value" is included as part of it. + # so we mangle it to get that stuff out. + d.update(dict(pairwise([ str(i.value) for i in list(subnode.attributes.values()) ]))) + + return d + + def get_properties(node): + """ + parses a node and returns a dict that contains the data from the node's + attributes and any data from "property" elements as well. + """ + + d = {} + + # get tag attributes + d.update(get_attributes(node)) + + # get vlues of the properties element, if any + d.update(parse_properties(node)) + + return d + + def set_properties(obj, node): + """ + read the xml attributes and tiled "properties" from a xml node and fill in + the values into an object's dictionary + """ + + [ setattr(obj, k, v) for k,v in list(get_properties(node).items()) ] + + def get_attributes(node): + """ + get the attributes from a node and fix them to the correct type + """ + + d = defaultdict(lambda:None) + + for k, v in list(node.attributes.items()): + k = str(k) + d[k] = types[k](v) + + return d + + def decode_gid(raw_gid): + # gid's are encoded with extra information + # as of 0.7.0 it determines if the tile should be flipped when rendered + + flags = 0 + if raw_gid & GID_FLIP_X == GID_FLIP_X: flags += FLIP_X + if raw_gid & GID_FLIP_Y == GID_FLIP_Y: flags += FLIP_Y + gid = raw_gid & ~(GID_FLIP_X | GID_FLIP_Y) + + return gid, flags + + + def parse_map(node): + """ + parse a map node from a tiled tmx file + return a tiledmap + """ + + tiledmap = TiledMap() + tiledmap.filename = filename + set_properties(tiledmap, map_node) + + for node in map_node.getElementsByTagName("tileset"): + t, tiles = parse_tileset(node) + tiledmap.tilesets.append(t) + tiledmap.tile_properties.update(tiles) + + for node in dom.getElementsByTagName("layer"): + l = parse_layer(tiledmap.tilesets, node) + tiledmap.tilelayers.append(l) + tiledmap.layers.append(l) + + for node in dom.getElementsByTagName("objectgroup"): + o = parse_objectgroup(node) + tiledmap.objectgroups.append(o) + tiledmap.layers.append(o) + + return tiledmap + + + def parse_tileset(node, firstgid=None): + """ + parse a tileset element and return a tileset object and properties for tiles as a dict + """ + + tileset = TiledTileset() + set_properties(tileset, node) + tiles = {} + + if firstgid != None: + tileset.firstgid = firstgid + + # since tile objects probably don't have a lot of metadata, + # we store it seperately from the class itself + for child in node.childNodes: + if child.nodeName == "tile": + p = get_properties(child) + gid = p["id"] + tileset.firstgid + del p["id"] + tiles[gid] = p + + # check for tiled "external tilesets" + if hasattr(tileset, "source"): + if tileset.source[-4:].lower() == ".tsx": + try: + # we need to mangle the path some because tiled stores relative paths + path = os.path.join(os.path.dirname(filename), tileset.source) + tsx = parse(path) + except IOError: + raise IOError("Cannot load external tileset: " + path) + + tileset_node = tsx.getElementsByTagName("tileset")[0] + tileset, tiles = parse_tileset(tileset_node, tileset.firstgid) + else: + raise Exception("Found external tileset, but cannot handle type: " + tileset.source) + + # if we have an "image" tag, process it here + try: + image_node = node.getElementsByTagName("image")[0] + except IndexError: + pass + else: + attr = get_attributes(image_node) + tileset.source = attr["source"] + tileset.trans = attr["trans"] + + # calculate the number of tiles in this tileset + x, r = divmod(attr["width"], tileset.tilewidth) + y, r = divmod(attr["height"], tileset.tileheight) + + tileset.lastgid = tileset.firstgid + x + y + + return tileset, tiles + + + def parse_layer(tilesets, node): + """ + parse a layer element and return a layer object + + tilesets is required since we need to mangle gid's here + """ + + layer = TiledLayer() + layer.data = [] + layer.flipped_tiles = [] + set_properties(layer, node) + + data = None + next_gid = None + + data_node = node.getElementsByTagName("data")[0] + attr = get_attributes(data_node) + + if attr["encoding"] == "base64": + from base64 import b64decode + data = b64decode(bytes(data_node.lastChild.nodeValue, 'ascii')) + + elif attr["encoding"] == "csv": + next_gid = map(int, "".join([line.strip() for line in data_node.lastChild.nodeValue]).split(",")) + + elif not attr["encoding"] == None: + raise Exception("TMX encoding type: " + str(attr["encoding"]) + " is not supported.") + + if attr["compression"] == "gzip": + from io import BytesIO + import gzip + with gzip.GzipFile(fileobj=BytesIO(data)) as fh: + data = fh.read() + + elif not attr["compression"] == None: + raise Exception("TMX compression type: " + str(attr["compression"]) + " is not supported.") + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + if attr["encoding"] == next_gid == None: + def get_children(parent): + for child in parent.getElementsByTagName("tile"): + yield int(child.getAttribute("gid")) + + next_gid = get_children(data_node) + + elif not data == None: + # cast the data as a list of 32-bit integers + def u(i): return unpack(" + + + + + + + + + + + + + + + + + + + + + + + eJxjZGBgYKQDpiUY7nYwUYBH7Ri1Y9SOoWEHNcFws4PWGABJRwF/ + + + + + eJyTZ2BgkKcDVqIhHu52UDPcR6Id+MJ61A762zFc0hW17aBHWTJU7aA1BgDmfzlJ + + + + + eJy1VdtqwzAMFWQbFHaBwNq1DyFhD92lD7Zr//+v1WY65ERTRyFMIOzKro6OJCtJRJL8SNA1qi3KLNG5x8L/xd1E+7uqH/+g94Q7OnF1tM/mrNBanDt91XPVQeZcjHT/tep2he4Ia9A1EI9i7EJ7rk3642yk/UB+rN1KvNEGX4+Or2YveoYcIta2f6u6d/RAPsDnVh5BMXA+mVizxvRu4mFfmeJFPc4OP+RjonPwFcXge168ydiBLbJ8g5P63hAOYzw4voABns3ey9xXQZb8OVdFOWXFsPMAGB3xQJ3whry++qr67ehJzzE/PB4Wm+0v9H87n7jO3BuoLXLqYXTyu+acMwj7tdjwhXeAvDOPo6ybg5+y7FGuB3qlSSa+ntgZCWk+evrN7+NJ1s3Ba/qsWODC3xbuRc47vjtB5tlh5yMk0noBeX8qEg== + + + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/001/resources/spritesheet.png Binary file 004/001/resources/spritesheet.png has changed diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/001/resources/tilesheet.png Binary file 004/001/resources/tilesheet.png has changed diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/001/sprites.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/001/sprites.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +import pygame + +import resources + + +FACING = { + (0, -1): 0, + (-1, 0): 1, + (0, 1): 2, + (1, 0): 3, +} + + +class ShadowSprite(pygame.sprite.Sprite): + def __init__(self, sprite): + super(ShadowSprite, self).__init__() + self.spritesheet = resources.load_sheet('effects.png', 32) + self.image = self.spritesheet[0] + self.rect = self.image.get_rect() + self.image.set_alpha(0.6 * 255) + self.sprite = sprite + self.rect.midbottom = self.sprite.rect.midbottom + + def update(self): + self.rect.midbottom = self.sprite.rect.midbottom + + +class HeroSprite(pygame.sprite.Sprite): + def __init__(self, (x, y)): + super(HeroSprite, self).__init__() + self.spritesheet = resources.load_sheet('spritesheet.png', 64) + self.facing = 2 + self.image = self.spritesheet[self.facing] + self.rect = self.image.get_rect() + self.rect.midbottom = (x * 32 + 16, y * 32 + 32) + self.animations = [] + + def move(self, (dx, dy)): + self.facing = FACING[dx, dy] + self.animations.append(self.animation_walk((dx, dy))) + + def animation_walk(self, (dx, dy)): + for step in xrange(8): + self.image = self.spritesheet[self.facing + step * 4 + 4] + self.rect.move_ip(dx * 4, dy * 4) + yield + + def animation_stand(self): + self.image = self.spritesheet[self.facing] + return + yield + + def update(self): + if not self.animations: + self.animations.append(self.animation_stand()) + while self.animations: + try: + self.animations[0].next() + return + except StopIteration: + self.animations.pop(0) diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/__init__.py diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/__main__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/__main__.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from game import Game + + +game = Game() +game.load_level('level.tmx') +game.run() diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/game.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/game.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +import pygame +import pygame.locals as pg + +import level +import sprites + + +class Game(object): + def __init__(self): + pygame.display.init() + pygame.font.init() + self.screen = pygame.display.set_mode((800, 600)) + self.screen.fill((0x66, 0x66, 0x66)) + self.keys = { + pg.K_UP: (self.move_hero, (0, -1)), + pg.K_DOWN: (self.move_hero, (0, 1)), + pg.K_LEFT: (self.move_hero, (-1, 0)), + pg.K_RIGHT: (self.move_hero, (1, 0)), + pg.K_ESCAPE: (self.quit,), + } + self.shadow_sprites = pygame.sprite.RenderUpdates() + self.sprites = pygame.sprite.RenderUpdates() + self.key = None + + def load_level(self, filename): + self.level = level.Level(filename) + for name, pos in self.level.objects(): + sprite = sprites.DecorationSprite(pos) + self.sprites.add(sprites.DecorationSprite(pos)) + self.shadow_sprites.add(sprites.ShadowSprite(sprite)) + self.hero_sprite = sprites.HeroSprite(self.level.hero_position) + self.sprites.add(self.hero_sprite) + self.shadow_sprites.add(sprites.ShadowSprite(self.hero_sprite)) + self.background = self.screen.copy() + self.level.render(self.background) + self.screen.blit(self.background, (0, 0)) + self.sprites.draw(self.screen) + pygame.display.update() + + def quit(self): + self.game_over = True + + def move_hero(self, direction): + if self.level.move_hero(direction): + self.hero_sprite.move(direction) + + def control(self): + for event in pygame.event.get(): + if event.type == pg.QUIT: + self.quit() + elif event.type == pg.KEYDOWN: + self.key = event.key + if not self.hero_sprite.animations: + keys = pygame.key.get_pressed() + for key, action in self.keys.iteritems(): + if keys[key] or self.key == key: + action[0](*action[1:]) + break + self.key = None + + def display(self): + self.sprites.clear(self.screen, self.background) + self.shadow_sprites.clear(self.screen, self.background) + self.sprites.update() + self.shadow_sprites.update() + dirty = [] + dirty.extend(self.shadow_sprites.draw(self.screen)) + dirty.extend(self.sprites.draw(self.screen)) + pygame.display.update(dirty) + + def run(self): + self.clock = pygame.time.Clock() + self.game_over = False + while not self.game_over: + self.display() + self.control() + self.clock.tick(30) + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/level.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/level.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +from pytmx import tmxloader + +import resources + + +class Level(object): + def __init__(self, filename): + path = resources.file_path(filename) + self.tiled_map = tmxloader.load_pygame(path) + self.hero_position = 1, 1 + + def render(self, screen): + w = self.tiled_map.tilewidth + h = self.tiled_map.tileheight + get_image = self.tiled_map.getTileImage + for l, layer in enumerate(self.tiled_map.tilelayers): + for y in xrange(self.tiled_map.height): + for x in xrange(self.tiled_map.width): + image = get_image(x, y, l) + if image: + screen.blit(image, (x * w, y * h)) + x, y = self.hero_position + + def get_properties(self, (x, y)): + properties = {} + for l, layer in enumerate(self.tiled_map.tilelayers): + properties.update(self.tiled_map.getTileProperties((x, y, l)) or {}) + return properties + + def is_blocked(self, (x, y)): + return self.get_properties((x, y)).get('block') + + def move_hero(self, (dx, dy)): + x, y = self.hero_position + if not self.is_blocked((x + dx, y + dy)): + self.hero_position = x + dx, y + dy + return True + return False + + def objects(self): + w = self.tiled_map.tilewidth + h = self.tiled_map.tileheight + for og in self.tiled_map.objectgroups: + for o in og: + yield o.name, (o.x // w, o.y // h) + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/pytmx/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/pytmx/__init__.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,5 @@ +from tmxloader import load_tmx, load_pygame +from pytmx import TiledElement, TiledMap, TiledTileset, TiledLayer, \ + TiledObjectGroup, TiledObject +from utils import buildDistributionRects + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/pytmx/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/pytmx/constants.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,11 @@ +# internal flags +TRANS_FLIPX = 1 +TRANS_FLIPY = 2 +TRANS_ROT = 4 + + +# Tiled gid flags +GID_TRANS_FLIPX = 1<<31 +GID_TRANS_FLIPY = 1<<30 +GID_TRANS_ROT = 1<<29 + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/pytmx/pytmx.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/pytmx/pytmx.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,572 @@ +from itertools import chain, product +from xml.etree import ElementTree +from collections import defaultdict +from utils import decode_gid, types, parse_properties, read_points +from constants import * + + + +""" +Class declarations and other important stuff +""" + + + +class TiledElement(object): + def set_properties(self, node): + """ + read the xml attributes and tiled "properties" from a xml node and fill + in the values into the object's dictionary. Names will be checked to + make sure that they do not conflict with reserved names. + """ + + # set the attributes reserved for tiled + [ setattr(self, k, types[str(k)](v)) for (k,v) in node.items() ] + + # set the attributes that are derived from tiled 'properties' + for k,v in parse_properties(node).items(): + if k in self.reserved: + msg = "{} \"{}\" has a property called \"{}\"" + print msg.format(self.__class__.__name__, self.name, k, self.__class__.__name__) + msg = "This name is reserved for {} objects and can cannot be used." + print msg.format(self.__class__.__name__) + print "Please change the name in Tiled and try again." + print v + raise ValueError + setattr(self, k, types[str(k)](v)) + + +class TiledMap(TiledElement): + """ + Contains the tile layers, tile images, object groups, and objects from a + Tiled TMX map. + """ + + reserved = "version orientation width height tilewidth tileheight properties tileset layer objectgroup".split() + + + def __init__(self, filename): + from collections import defaultdict + + TiledElement.__init__(self) + self.tilesets = [] # list of TiledTileset objects + self.tilelayers = [] # list of TiledLayer objects + self.objectgroups = [] # list of TiledObjectGroup objects + self.tile_properties = {} # dict of tiles that have metadata + self.filename = filename + + # only used tiles are actually loaded, so there will be a difference + # between the GID's in the Tile map data (tmx) and the data in this + # class and the layers. This dictionary keeps track of that difference. + self.gidmap = defaultdict(list) + + # should be filled in by a loader function + self.images = [] + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 + self.height = 0 + self.tilewidth = 0 + self.tileheight = 0 + + self.imagemap = {} # mapping of gid and trans flags to real gids + self.maxgid = 1 + + self.load() + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.filename) + + + def getTileImage(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is not valid." + raise Exception, msg.format(x, y, layer) + except TypeError: + msg = "Tiles must be specified in integers." + raise TypeError, msg + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid" + raise Exception, msg.format(x, y, layer) + + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + useful if you don't want to repeatedly call getTileImage + """ + + raise NotImplementedError + + + def getObjects(self): + """ + Return iterator of all the objects associated with this map + """ + + return chain(*(i for i in self.objectgroups)) + + + def getTileProperties(self, (x, y, layer)): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid." + raise Exception, msg.format(x, y, layer) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + except KeyError: + return None + + + def getLayerData(self, layer): + """ + Return the data for a layer. + + Data is an array of arrays. + + >>> pos = data[y][x] + """ + + try: + return self.tilelayers[layer].data + except IndexError: + msg = "Layer {} does not exist." + raise ValueError, msg.format(layer) + + + def getTileLocation(self, gid): + # experimental way to find locations of a tile by the GID + + p = product(xrange(self.width), + xrange(self.height), + xrange(len(self.tilelayers))) + + return [ (x,y,l) for (x,y,l) in p + if self.tilelayers[l].data[y][x] == gid ] + + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + + + def setTileProperties(self, gid, d): + """ + set the properties of a tile by GID. + must use a standard python dict as d + """ + + try: + self.tile_properties[gid] = d + except KeyError: + msg = "GID #{} does not exist." + raise ValueError, msg.format(gid) + + + def getTilePropertiesByLayer(self, layer): + """ + Return a list of tile properties (dict) in use in this tile layer. + """ + + try: + layer = int(layer) + except: + msg = "Layer must be an integer. Got {} instead." + raise ValueError, msg.format(type(layer)) + + p = product(range(self.width),range(self.height)) + layergids = set( self.tilelayers[layer].data[y][x] for x, y in p ) + + props = [] + for gid in layergids: + try: + props.append((gid, self.tile_properties[gid])) + except: + continue + + return props + + + def registerGID(self, real_gid, flags=0): + """ + used to manage the mapping of GID between the tmx data and the internal + data. + + number returned is gid used internally + """ + + if real_gid: + try: + return self.imagemap[(real_gid, flags)][0] + except KeyError: + # this tile has not been encountered before, or it has been + # transformed in some way. make a new GID for it. + gid = self.maxgid + self.maxgid += 1 + self.imagemap[(real_gid, flags)] = (gid, flags) + self.gidmap[real_gid].append((gid, flags)) + return gid + + else: + return 0 + + + + def mapGID(self, real_gid): + """ + used to lookup a GID read from a TMX file's data + """ + + try: + return self.gidmap[int(real_gid)] + except KeyError: + return None + except TypeError: + msg = "GID's must be an integer" + raise TypeError, msg + + + def loadTileImages(self, filename): + raise NotImplementedError + + + def load(self): + """ + parse a map node from a tiled tmx file + """ + etree = ElementTree.parse(self.filename).getroot() + self.set_properties(etree) + + # initialize the gid mapping + self.imagemap[(0,0)] = 0 + + for node in etree.findall('layer'): + self.tilelayers.append(TiledLayer(self, node)) + + for node in etree.findall('objectgroup'): + self.objectgroups.append(TiledObjectGroup(self, node)) + + for node in etree.findall('tileset'): + self.tilesets.append(TiledTileset(self, node)) + + # "tile objects", objects with a GID, have need to have their + # attributes set after the tileset is loaded + for o in self.getObjects(): + p = self.getTilePropertiesByGID(o.gid) + if p: + o.name = "TileObject" + o.__dict__.update(p) + + + @property + def visibleTileLayers(self): + return [layer for layer in self.tilelayers if layer.visible] + + +class TiledTileset(TiledElement): + reserved = "firstgid source name tilewidth tileheight spacing margin image tile properties".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.firstgid = 0 + self.source = None + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + self.tiles = {} + + self.parse(node) + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a tileset element and return a tileset object and properties for + tiles as a dict + + a bit of mangling is doew here so that tilesets that have external + TSX files appear the same as those that don't + """ + import os + + + # if true, then node references an external tileset + source = node.get('source', False) + if source: + if source[-4:].lower() == ".tsx": + + # external tilesets don't save this, store it for later + self.firstgid = int(node.get('firstgid')) + + # we need to mangle the path - tiled stores relative paths + dirname = os.path.dirname(self.parent.filename) + path = os.path.abspath(os.path.join(dirname, source)) + try: + node = ElementTree.parse(path).getroot() + except IOError: + msg = "Cannot load external tileset: {}" + raise Exception, msg.format(path) + + else: + msg = "Found external tileset, but cannot handle type: {}" + raise Exception, msg.format(self.source) + + self.set_properties(node) + + # since tile objects [probably] don't have a lot of metadata, + # we store it seperately in the parent (a TiledMap instance) + for child in node.iter('tile'): + real_gid = int(child.get("id")) + p = parse_properties(child) + p['width'] = self.tilewidth + p['height'] = self.tileheight + for gid, flags in self.parent.mapGID(real_gid + self.firstgid): + self.parent.setTileProperties(gid, p) + + + image_node = node.find('image') + self.source = image_node.get('source') + self.trans = image_node.get("trans", None) + + +class TiledLayer(TiledElement): + reserved = "name x y width height opacity properties data".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.data = [] + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = True + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a layer element + """ + from utils import group + from itertools import product, imap + from struct import unpack + import array + + self.set_properties(node) + + data = None + next_gid = None + + data_node = node.find('data') + + encoding = data_node.get("encoding", None) + if encoding == "base64": + from base64 import decodestring + data = decodestring(data_node.text.strip()) + + elif encoding == "csv": + next_gid = imap(int, "".join( + line.strip() for line in data_node.text.strip() + ).split(",")) + + elif encoding: + msg = "TMX encoding type: {} is not supported." + raise Exception, msg.format(encoding) + + compression = data_node.get("compression", None) + if compression == "gzip": + from StringIO import StringIO + import gzip + with gzip.GzipFile(fileobj=StringIO(data)) as fh: + data = fh.read() + + elif compression == "zlib": + import zlib + data = zlib.decompress(data) + + elif compression: + msg = "TMX compression type: {} is not supported." + raise Exception, msg.format(str(attr["compression"])) + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + # TODO: this will probably raise an exception if there are no tiles + if encoding == next_gid == None: + def get_children(parent): + for child in parent.findall('tile'): + yield int(child.get('gid')) + + next_gid = get_children(data_node) + + elif data: + # data is a list of gid's. cast as 32-bit ints to format properly + # create iterator to effeciently parse data + next_gid=imap(lambda i:unpack("".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a objectgroup element and return a object group + """ + + self.set_properties(node) + + for child in node.findall('object'): + o = TiledObject(self.parent, child) + self.append(o) + + +class TiledObject(TiledElement): + reserved = "name type x y width height gid properties polygon polyline image".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + self.set_properties(node) + + # correctly handle "tile objects" (object with gid set) + if self.gid: + self.gid = self.parent.registerGID(self.gid) + + polygon = node.find('polygon') + if polygon is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polygon.get('points')) + self.closed = 1 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) + + polyline = node.find('polyline') + if polyline is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polyline.get('points')) + self.closed = 0 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/pytmx/tmxloader.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/pytmx/tmxloader.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,304 @@ +""" +Map loader for TMX Files +bitcraft (leif dot theden at gmail.com) +v.15 - for python 2.7 + +If you have any problems or suggestions, please contact me via email. +Tested with Tiled 0.8.1 for Mac. + +released under the LGPL v3 + +=============================================================================== + +This map loader can be used to load maps created in the Tiled map editor. It +provides a simple way to get tiles and associated metadata so that you can draw +a map onto the screen. It does not force you to draw your map in any +particular way. + +This is *not* a rendering engine. It will load the data that is necessary to +render a map onto the screen. All tiles will be loaded into in memory and +available to blit onto the screen. + + +Design Goals: + Simple api + Memory efficient and fast + +Features: + Loads data and "properties" metadata from Tile's TMX format + "Properties" for: maps, tilesets, layers, objectgroups, objects, and tiles + Point data for polygon and polyline objects + Automatic flipping and rotation of tiles + Supports base64, csv, gzip, zlib and uncompressed XML + Image loading with pygame + +New in .15: + loader: fixed issue where objects with tile gid did not load properties + loader: polygon and polyline objects + loader: new lookup methods use iterators + loader: loading function moved into classes + loader: data/images can be reloaded on the fly + loader: uses etree for faster xml parsing + +New in .14: + loader: Fixed gid lookup for "buildDistributionRects" + loader: Added useful output to a few classes "__repr__" + loader: Fixed a gid mapping issue that broke rotated tiles + pygame: fixed colorkey handling + pygame: correctly handles margins and spacing between tiles in tilesets + pygame: b/c of changes, now correctly renders tiled's example maps + added scrolling demo + +New in .13: + loader: Renamed "get_tile_image" to "getTileImage" + loader: Removed duplicates returned from getTilePropertiesByLayer + loader: Modified confusing messages for GID errors + loader: Fixed bug where transformed tile properties are not available + loader: No longer loads metadata for tiles that are not used + loader: Reduced tile cache to 256 unique tiles + loader: Removed 'visible' from list of reserved words + loader: Added 'buildDistributionRects' and maputils module + loader: Added some misc. functions for retrieving properties + pygame: Smarter tile management made tile loading cache useless; removed it + pygame: pygame.RLEACCEL flag added when appropriate + +New in .12: + loader: Fixed bug where tile properties could contain reserved words + loader: Reduced size of image index by only allocating space for used tiles + +New in .11: + loader: Added support for tileset properties + loader: Now checks for property names that are reserved for internal use + loader: Added support for rotated tiles + pygame: Only the tiles that are used in the map will be loaded into memory + pygame: Added support for rotated tiles + pygame: Added option to force a bitsize (depth) for surfaces + pygame: Added option to convert alpha transparency to colorkey transparency + pygame: Tilesets no longer load with per-pixel alphas by default + pygame: Colorkey transparency should be correctly handled now + + +NOTES: + +* The Tiled "properties" have reserved names. + +If you use "properties" for any of the following object types, you cannot use +any of theese words as a name for your property. A ValueError will be raised +if there are any conflicts. + +As of 0.8.1, these values are: + +map: version, orientation, width, height, tilewidth, tileheight + properties, tileset, layer, objectgroup + +tileset: firstgid, source, name, tilewidth, tileheight, spacing, margin, + image, tile, properties + +tile: id, image, properties + +layer: name, x, y, width, height, opacity, properties, data + +objectgroup: name, color, x, y, width, height, opacity, object, properties + +object: name, type, x, y, width, height, gid, properties, polygon, + polyline, image + + + +I have been intentionally not including a module rendering utility since +rendering a map will have different requirements for every situation. However, +I can appreciate that some poeple won't understand how it works unless they see +it, so I am including a sample map and viewer. It includes a scrolling/zooming +renderer. They are for demonstation purposes, and may not be suitable for all +projects. Use at your own risk. + +I've also included a copy of this loader that may work with python 3.x. I +personally do not think that python 3.x should be used with pygame, yet (and I +am not the only person). You can try it if you insist on using pygame with +python 3.x, but I don't update that often. + +=============================================================================== + +Basic usage sample: + + >>> from pytmx import tmxloader + >>> tmxdata = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "getTileImage": + + >>> image = tmxdata.getTileImage(x, y, layer) + >>> screen.blit(position, image) + + +Maps, tilesets, layers, objectgroups, and objects all have a simple way to +access metadata that was set inside tiled: they all become object attributes. + + >>> layer = tmxdata.layers[0] + >>> print layer.tilewidth + 32 + >>> print layer.weather + 'sunny' + + +Tiles properties are the exception here, and must be accessed through +"getTileProperties". The data is a regular Python dictionary: + + >>> tile = tmxdata.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + + + +Please see the TiledMap class for some api information. +""" +from utils import types +from constants import * + + + +def load_tmx(filename, *args, **kwargs): + # for .14 compatibility + from pytmx import TiledMap + + tiledmap = TiledMap(filename) + return tiledmap + + +def load_images_pygame(tmxdata, mapping, *args, **kwargs): + """ + due to the way the tiles are loaded, they will be in the same pixel format + as the display when it is loaded. take this into consideration if you + intend to support different screen pixel formats. + + by default, the images will not have per-pixel alphas. this can be + changed by including "pixelalpha=True" in the keywords. this will result + in much slower blitting speeds. + + if the tileset's image has colorkey transparency set in Tiled, the loader + will return images that have their transparency already set. using a + tileset with colorkey transparency will greatly increase the speed of + rendering the map. + + optionally, you can force the loader to strip the alpha channel of the + tileset image and to fill in the missing areas with a color, then use that + new color as a colorkey. the resulting tiles will render much faster, but + will not preserve the transparency of the tile if it uses partial + transparency (which you shouldn't be doing anyway, this is SDL). + """ + from itertools import product + from pygame import Surface, mask + import pygame, os + + + def handle_transformation(tile, flags): + if flags: + fx = flags & TRANS_FLIPX == TRANS_FLIPX + fy = flags & TRANS_FLIPY == TRANS_FLIPY + r = flags & TRANS_ROT == TRANS_ROT + + if r: + # not sure why the flip is required...but it is. + newtile = pygame.transform.rotate(tile, 270) + newtile = pygame.transform.flip(newtile, 1, 0) + + if fx or fy: + newtile = pygame.transform.flip(newtile, fx, fy) + + elif fx or fy: + newtile = pygame.transform.flip(tile, fx, fy) + + # preserve any flags that may have been lost after the transformation + return newtile.convert(tile) + + else: + return tile + + + pixelalpha = kwargs.get("pixelalpha", False) + force_colorkey = kwargs.get("force_colorkey", False) + force_bitdepth = kwargs.get("depth", False) + + if force_colorkey: + try: + force_colorkey = pygame.Color(*force_colorkey) + except: + msg = "Cannot understand color: {}" + raise Exception, msg.format(force_colorkey) + + tmxdata.images = [0] * tmxdata.maxgid + + for firstgid, t in sorted((t.firstgid, t) for t in tmxdata.tilesets): + path = os.path.join(os.path.dirname(tmxdata.filename), t.source) + + image = pygame.image.load(path) + + w, h = image.get_size() + tile_size = (t.tilewidth, t.tileheight) + real_gid = t.firstgid - 1 + + if t.trans: + tileset_colorkey = pygame.Color("#{}".format(t.trans)) + + # i dont agree with margins and spacing, but i'll support it anyway + # such is life. okay.jpg + tilewidth = t.tilewidth + t.spacing + tileheight = t.tileheight + t.spacing + + # some tileset images may be slightly larger than the tile area + # ie: may include a banner, copyright, ect. this compensates for that + width = ((int((w-t.margin*2) + t.spacing) / tilewidth) * tilewidth) - t.spacing + height = ((int((h-t.margin*2) + t.spacing) / tileheight) * tileheight) - t.spacing + + # using product avoids the overhead of nested loops + p = product(xrange(t.margin, height+t.margin, tileheight), + xrange(t.margin, width+t.margin, tilewidth)) + + for (y, x) in p: + real_gid += 1 + gids = tmxdata.mapGID(real_gid) + if gids == []: continue + + # we do some tests to correctly handle the tile and set the right + # blitting flags. just grab a section of it. + temp = image.subsurface(((x,y), tile_size)) + + # count the number of pixels in the tile that are not transparent + px = mask.from_surface(temp).count() + + # there are no transparent pixels in the image + if px == tile_size[0] * tile_size[1]: + tile = temp.convert() + + # there are transparent pixels, and set to force a colorkey + elif force_colorkey: + tile = Surface(tile_size) + tile.fill(force_colorkey) + tile.blit(temp, (0,0)) + tile.set_colorkey(force_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and tiled set a colorkey + elif t.trans: + tile = temp.convert() + tile.set_colorkey(tileset_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and set for perpixel alpha + elif pixelalpha: + tile = temp.convert_alpha() + + # there are transparent pixels, and we won't handle them + else: + tile = temp.convert() + + for gid, flags in gids: + tmxdata.images[gid] = handle_transformation(tile, flags) + + +def load_pygame(filename, *args, **kwargs): + tmxdata = load_tmx(filename, *args, **kwargs) + load_images_pygame(tmxdata, None, *args, **kwargs) + + return tmxdata + + + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/002/pytmx/tmxloader3.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/002/pytmx/tmxloader3.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,612 @@ +""" +Map loader for TMX Files +bitcraft (leif.theden at gmail.com) +v.7 -- for python 3.x + +If you have any problems, please contact me via email. +Tested with Tiled 0.7.1 for Mac. + +released under the LGPL v3 + +====================================================================== +Design Goals: + Simple api + Memory efficient and fast + Quick access to tiles, attributes, and properties + +Non-Goals: + Rendering + +Works: + Pygame image loading + Map loading with all required types + Properties for all types: maps, layers, objects, tiles + Automatic flipping of tiles + +Todo: + Pygame: test colorkey transparency + + Optimized for maps that do not make heavy use of tile + properties. If i find that it is used a lot then i can rework + it for better performance. + +====================================================================== + +Basic usage sample: + + >>> import tmxloader + >>> tiledmap = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "get_tile_image": + + >>> image = tiledmap.get_tile_image(x, y, layer) + >>> screen.blit(position, image) + + +Layers, objectgroups, tilesets, and maps all have a simple way to access +metadata that was set inside tiled: they all become class attributes. + + >>> print(layer.tilewidth) + 32 + >>> print(layer.weather) + 'sunny' + + +Tiles are the exception here, and must be accessed through "getTileProperties" +and are regular Python dictionaries: + + >>> tile = tiledmap.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + +""" + +from itertools import chain + + +# internal flags +FLIP_X = 1 +FLIP_Y = 2 + + +# Tiled gid flags +GID_FLIP_X = 1<<31 +GID_FLIP_Y = 1<<30 + + +class TiledElement(object): + pass + +class TiledMap(TiledElement): + """ + not really useful unless "loaded" ie: don't instance directly. + see the pygame loader for inspiration + """ + + def __init__(self): + TiledElement.__init__(self) + self.layers = [] # list of all layer types (tile layers + object layers) + self.tilesets = [] # list of TiledTileset objects + self.tilelayers = [] # list of TiledLayer objects + self.objectgroups = [] # list of TiledObjectGroup objects + self.tile_properties = {} # dict of tiles that have additional metadata (properties) + self.filename = None + + # this is a work around to tiled's strange way of storing gid's + self.images = [0] + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 + self.height = 0 + self.tilewidth = 0 + self.tileheight = 0 + + def get_tile_image(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + usefull if you don't want to repeatedly call get_tile_image + probably not the most effecient way of doing this, but oh well. + """ + + raise NotImplementedError + + def getObjects(self): + """ + Return iterator all of the objects associated with this map + """ + + return chain(*[ i.objects for i in self.objectgroups ]) + + def getTileProperties(self, x, y, layer): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + +# the following classes get their attributes filled in with the loader + +class TiledTileset(TiledElement): + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.firstgid = 0 + self.lastgid = 0 + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + +class TiledLayer(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.data = None + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = 1 + +class TiledObjectGroup(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.objects = [] + + # defaults from the specification + self.name = None + +class TiledObject(TiledElement): + __slots__ = ['name', 'type', 'x', 'y', 'width', 'height', 'gid'] + + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + +def load_tmx(filename): + """ + Utility function to parse a Tiled TMX and return a usable object. + Images will not be loaded, so probably not useful to call this directly + + See the load_pygame func for an idea of what to do + """ + + from xml.dom.minidom import parse + from itertools import tee, islice, chain + from collections import defaultdict + from struct import unpack + import array, os + + # used to change the unicode string returned from minidom to + # proper python variable types. + types = { + "version": float, + "orientation": str, + "width": int, + "height": int, + "tilewidth": int, + "tileheight": int, + "firstgid": int, + "source": str, + "name": str, + "spacing": int, + "margin": int, + "source": str, + "trans": str, + "id": int, + "opacity": float, + "visible": bool, + "encoding": str, + "compression": str, + "gid": int, + "type": str, + "x": int, + "y": int, + "value": str, + } + + def pairwise(iterable): + # return a list as a sequence of pairs + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + def group(l, n): + # return a list as a sequence of n tuples + return zip(*[islice(l, i, None, n) for i in range(n)]) + + def parse_properties(node): + """ + parse a node and return a dict that represents a tiled "property" + """ + + d = {} + + for child in node.childNodes: + if child.nodeName == "properties": + for subnode in child.getElementsByTagName("property"): + # the "properties" from tiled's tmx have an annoying + # quality that "name" and "value" is included as part of it. + # so we mangle it to get that stuff out. + d.update(dict(pairwise([ str(i.value) for i in list(subnode.attributes.values()) ]))) + + return d + + def get_properties(node): + """ + parses a node and returns a dict that contains the data from the node's + attributes and any data from "property" elements as well. + """ + + d = {} + + # get tag attributes + d.update(get_attributes(node)) + + # get vlues of the properties element, if any + d.update(parse_properties(node)) + + return d + + def set_properties(obj, node): + """ + read the xml attributes and tiled "properties" from a xml node and fill in + the values into an object's dictionary + """ + + [ setattr(obj, k, v) for k,v in list(get_properties(node).items()) ] + + def get_attributes(node): + """ + get the attributes from a node and fix them to the correct type + """ + + d = defaultdict(lambda:None) + + for k, v in list(node.attributes.items()): + k = str(k) + d[k] = types[k](v) + + return d + + def decode_gid(raw_gid): + # gid's are encoded with extra information + # as of 0.7.0 it determines if the tile should be flipped when rendered + + flags = 0 + if raw_gid & GID_FLIP_X == GID_FLIP_X: flags += FLIP_X + if raw_gid & GID_FLIP_Y == GID_FLIP_Y: flags += FLIP_Y + gid = raw_gid & ~(GID_FLIP_X | GID_FLIP_Y) + + return gid, flags + + + def parse_map(node): + """ + parse a map node from a tiled tmx file + return a tiledmap + """ + + tiledmap = TiledMap() + tiledmap.filename = filename + set_properties(tiledmap, map_node) + + for node in map_node.getElementsByTagName("tileset"): + t, tiles = parse_tileset(node) + tiledmap.tilesets.append(t) + tiledmap.tile_properties.update(tiles) + + for node in dom.getElementsByTagName("layer"): + l = parse_layer(tiledmap.tilesets, node) + tiledmap.tilelayers.append(l) + tiledmap.layers.append(l) + + for node in dom.getElementsByTagName("objectgroup"): + o = parse_objectgroup(node) + tiledmap.objectgroups.append(o) + tiledmap.layers.append(o) + + return tiledmap + + + def parse_tileset(node, firstgid=None): + """ + parse a tileset element and return a tileset object and properties for tiles as a dict + """ + + tileset = TiledTileset() + set_properties(tileset, node) + tiles = {} + + if firstgid != None: + tileset.firstgid = firstgid + + # since tile objects probably don't have a lot of metadata, + # we store it seperately from the class itself + for child in node.childNodes: + if child.nodeName == "tile": + p = get_properties(child) + gid = p["id"] + tileset.firstgid + del p["id"] + tiles[gid] = p + + # check for tiled "external tilesets" + if hasattr(tileset, "source"): + if tileset.source[-4:].lower() == ".tsx": + try: + # we need to mangle the path some because tiled stores relative paths + path = os.path.join(os.path.dirname(filename), tileset.source) + tsx = parse(path) + except IOError: + raise IOError("Cannot load external tileset: " + path) + + tileset_node = tsx.getElementsByTagName("tileset")[0] + tileset, tiles = parse_tileset(tileset_node, tileset.firstgid) + else: + raise Exception("Found external tileset, but cannot handle type: " + tileset.source) + + # if we have an "image" tag, process it here + try: + image_node = node.getElementsByTagName("image")[0] + except IndexError: + pass + else: + attr = get_attributes(image_node) + tileset.source = attr["source"] + tileset.trans = attr["trans"] + + # calculate the number of tiles in this tileset + x, r = divmod(attr["width"], tileset.tilewidth) + y, r = divmod(attr["height"], tileset.tileheight) + + tileset.lastgid = tileset.firstgid + x + y + + return tileset, tiles + + + def parse_layer(tilesets, node): + """ + parse a layer element and return a layer object + + tilesets is required since we need to mangle gid's here + """ + + layer = TiledLayer() + layer.data = [] + layer.flipped_tiles = [] + set_properties(layer, node) + + data = None + next_gid = None + + data_node = node.getElementsByTagName("data")[0] + attr = get_attributes(data_node) + + if attr["encoding"] == "base64": + from base64 import b64decode + data = b64decode(bytes(data_node.lastChild.nodeValue, 'ascii')) + + elif attr["encoding"] == "csv": + next_gid = map(int, "".join([line.strip() for line in data_node.lastChild.nodeValue]).split(",")) + + elif not attr["encoding"] == None: + raise Exception("TMX encoding type: " + str(attr["encoding"]) + " is not supported.") + + if attr["compression"] == "gzip": + from io import BytesIO + import gzip + with gzip.GzipFile(fileobj=BytesIO(data)) as fh: + data = fh.read() + + elif not attr["compression"] == None: + raise Exception("TMX compression type: " + str(attr["compression"]) + " is not supported.") + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + if attr["encoding"] == next_gid == None: + def get_children(parent): + for child in parent.getElementsByTagName("tile"): + yield int(child.getAttribute("gid")) + + next_gid = get_children(data_node) + + elif not data == None: + # cast the data as a list of 32-bit integers + def u(i): return unpack("".format(self.__class__.__name__, self.filename) + + + def getTileImage(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is not valid." + raise Exception, msg.format(x, y, layer) + except TypeError: + msg = "Tiles must be specified in integers." + raise TypeError, msg + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid" + raise Exception, msg.format(x, y, layer) + + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + useful if you don't want to repeatedly call getTileImage + """ + + raise NotImplementedError + + + def getObjects(self): + """ + Return iterator of all the objects associated with this map + """ + + return chain(*(i for i in self.objectgroups)) + + + def getTileProperties(self, (x, y, layer)): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid." + raise Exception, msg.format(x, y, layer) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}" + raise Exception, msg.format(x, y, layer, gid) + except KeyError: + return None + + + def getLayerData(self, layer): + """ + Return the data for a layer. + + Data is an array of arrays. + + >>> pos = data[y][x] + """ + + try: + return self.tilelayers[layer].data + except IndexError: + msg = "Layer {} does not exist." + raise ValueError, msg.format(layer) + + + def getTileLocation(self, gid): + # experimental way to find locations of a tile by the GID + + p = product(xrange(self.width), + xrange(self.height), + xrange(len(self.tilelayers))) + + return [ (x,y,l) for (x,y,l) in p + if self.tilelayers[l].data[y][x] == gid ] + + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + + + def setTileProperties(self, gid, d): + """ + set the properties of a tile by GID. + must use a standard python dict as d + """ + + try: + self.tile_properties[gid] = d + except KeyError: + msg = "GID #{} does not exist." + raise ValueError, msg.format(gid) + + + def getTilePropertiesByLayer(self, layer): + """ + Return a list of tile properties (dict) in use in this tile layer. + """ + + try: + layer = int(layer) + except: + msg = "Layer must be an integer. Got {} instead." + raise ValueError, msg.format(type(layer)) + + p = product(range(self.width),range(self.height)) + layergids = set( self.tilelayers[layer].data[y][x] for x, y in p ) + + props = [] + for gid in layergids: + try: + props.append((gid, self.tile_properties[gid])) + except: + continue + + return props + + + def registerGID(self, real_gid, flags=0): + """ + used to manage the mapping of GID between the tmx data and the internal + data. + + number returned is gid used internally + """ + + if real_gid: + try: + return self.imagemap[(real_gid, flags)][0] + except KeyError: + # this tile has not been encountered before, or it has been + # transformed in some way. make a new GID for it. + gid = self.maxgid + self.maxgid += 1 + self.imagemap[(real_gid, flags)] = (gid, flags) + self.gidmap[real_gid].append((gid, flags)) + return gid + + else: + return 0 + + + + def mapGID(self, real_gid): + """ + used to lookup a GID read from a TMX file's data + """ + + try: + return self.gidmap[int(real_gid)] + except KeyError: + return None + except TypeError: + msg = "GID's must be an integer" + raise TypeError, msg + + + def loadTileImages(self, filename): + raise NotImplementedError + + + def load(self): + """ + parse a map node from a tiled tmx file + """ + etree = ElementTree.parse(self.filename).getroot() + self.set_properties(etree) + + # initialize the gid mapping + self.imagemap[(0,0)] = 0 + + for node in etree.findall('layer'): + self.tilelayers.append(TiledLayer(self, node)) + + for node in etree.findall('objectgroup'): + self.objectgroups.append(TiledObjectGroup(self, node)) + + for node in etree.findall('tileset'): + self.tilesets.append(TiledTileset(self, node)) + + # "tile objects", objects with a GID, have need to have their + # attributes set after the tileset is loaded + for o in self.getObjects(): + p = self.getTilePropertiesByGID(o.gid) + if p: + o.name = "TileObject" + o.__dict__.update(p) + + + @property + def visibleTileLayers(self): + return [layer for layer in self.tilelayers if layer.visible] + + +class TiledTileset(TiledElement): + reserved = "firstgid source name tilewidth tileheight spacing margin image tile properties".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.firstgid = 0 + self.source = None + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + self.tiles = {} + + self.parse(node) + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a tileset element and return a tileset object and properties for + tiles as a dict + + a bit of mangling is doew here so that tilesets that have external + TSX files appear the same as those that don't + """ + import os + + + # if true, then node references an external tileset + source = node.get('source', False) + if source: + if source[-4:].lower() == ".tsx": + + # external tilesets don't save this, store it for later + self.firstgid = int(node.get('firstgid')) + + # we need to mangle the path - tiled stores relative paths + dirname = os.path.dirname(self.parent.filename) + path = os.path.abspath(os.path.join(dirname, source)) + try: + node = ElementTree.parse(path).getroot() + except IOError: + msg = "Cannot load external tileset: {}" + raise Exception, msg.format(path) + + else: + msg = "Found external tileset, but cannot handle type: {}" + raise Exception, msg.format(self.source) + + self.set_properties(node) + + # since tile objects [probably] don't have a lot of metadata, + # we store it seperately in the parent (a TiledMap instance) + for child in node.iter('tile'): + real_gid = int(child.get("id")) + p = parse_properties(child) + p['width'] = self.tilewidth + p['height'] = self.tileheight + for gid, flags in self.parent.mapGID(real_gid + self.firstgid): + self.parent.setTileProperties(gid, p) + + + image_node = node.find('image') + self.source = image_node.get('source') + self.trans = image_node.get("trans", None) + + +class TiledLayer(TiledElement): + reserved = "name x y width height opacity properties data".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.data = [] + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = True + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a layer element + """ + from utils import group + from itertools import product, imap + from struct import unpack + import array + + self.set_properties(node) + + data = None + next_gid = None + + data_node = node.find('data') + + encoding = data_node.get("encoding", None) + if encoding == "base64": + from base64 import decodestring + data = decodestring(data_node.text.strip()) + + elif encoding == "csv": + next_gid = imap(int, "".join( + line.strip() for line in data_node.text.strip() + ).split(",")) + + elif encoding: + msg = "TMX encoding type: {} is not supported." + raise Exception, msg.format(encoding) + + compression = data_node.get("compression", None) + if compression == "gzip": + from StringIO import StringIO + import gzip + with gzip.GzipFile(fileobj=StringIO(data)) as fh: + data = fh.read() + + elif compression == "zlib": + import zlib + data = zlib.decompress(data) + + elif compression: + msg = "TMX compression type: {} is not supported." + raise Exception, msg.format(str(attr["compression"])) + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + # TODO: this will probably raise an exception if there are no tiles + if encoding == next_gid == None: + def get_children(parent): + for child in parent.findall('tile'): + yield int(child.get('gid')) + + next_gid = get_children(data_node) + + elif data: + # data is a list of gid's. cast as 32-bit ints to format properly + # create iterator to effeciently parse data + next_gid=imap(lambda i:unpack("".format(self.__class__.__name__, self.name) + + + def parse(self, node): + """ + parse a objectgroup element and return a object group + """ + + self.set_properties(node) + + for child in node.findall('object'): + o = TiledObject(self.parent, child) + self.append(o) + + +class TiledObject(TiledElement): + reserved = "name type x y width height gid properties polygon polyline image".split() + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + self.parse(node) + + + def __repr__(self): + return "<{}: \"{}\">".format(self.__class__.__name__, self.name) + + + def parse(self, node): + self.set_properties(node) + + # correctly handle "tile objects" (object with gid set) + if self.gid: + self.gid = self.parent.registerGID(self.gid) + + polygon = node.find('polygon') + if polygon is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polygon.get('points')) + self.closed = 1 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) + + polyline = node.find('polyline') + if polyline is not None: + x1 = x2 = y1 = y2 = 0 + self.points = read_points(polyline.get('points')) + self.closed = 0 + for x, y in self.points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/003/pytmx/tmxloader.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/003/pytmx/tmxloader.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,304 @@ +""" +Map loader for TMX Files +bitcraft (leif dot theden at gmail.com) +v.15 - for python 2.7 + +If you have any problems or suggestions, please contact me via email. +Tested with Tiled 0.8.1 for Mac. + +released under the LGPL v3 + +=============================================================================== + +This map loader can be used to load maps created in the Tiled map editor. It +provides a simple way to get tiles and associated metadata so that you can draw +a map onto the screen. It does not force you to draw your map in any +particular way. + +This is *not* a rendering engine. It will load the data that is necessary to +render a map onto the screen. All tiles will be loaded into in memory and +available to blit onto the screen. + + +Design Goals: + Simple api + Memory efficient and fast + +Features: + Loads data and "properties" metadata from Tile's TMX format + "Properties" for: maps, tilesets, layers, objectgroups, objects, and tiles + Point data for polygon and polyline objects + Automatic flipping and rotation of tiles + Supports base64, csv, gzip, zlib and uncompressed XML + Image loading with pygame + +New in .15: + loader: fixed issue where objects with tile gid did not load properties + loader: polygon and polyline objects + loader: new lookup methods use iterators + loader: loading function moved into classes + loader: data/images can be reloaded on the fly + loader: uses etree for faster xml parsing + +New in .14: + loader: Fixed gid lookup for "buildDistributionRects" + loader: Added useful output to a few classes "__repr__" + loader: Fixed a gid mapping issue that broke rotated tiles + pygame: fixed colorkey handling + pygame: correctly handles margins and spacing between tiles in tilesets + pygame: b/c of changes, now correctly renders tiled's example maps + added scrolling demo + +New in .13: + loader: Renamed "get_tile_image" to "getTileImage" + loader: Removed duplicates returned from getTilePropertiesByLayer + loader: Modified confusing messages for GID errors + loader: Fixed bug where transformed tile properties are not available + loader: No longer loads metadata for tiles that are not used + loader: Reduced tile cache to 256 unique tiles + loader: Removed 'visible' from list of reserved words + loader: Added 'buildDistributionRects' and maputils module + loader: Added some misc. functions for retrieving properties + pygame: Smarter tile management made tile loading cache useless; removed it + pygame: pygame.RLEACCEL flag added when appropriate + +New in .12: + loader: Fixed bug where tile properties could contain reserved words + loader: Reduced size of image index by only allocating space for used tiles + +New in .11: + loader: Added support for tileset properties + loader: Now checks for property names that are reserved for internal use + loader: Added support for rotated tiles + pygame: Only the tiles that are used in the map will be loaded into memory + pygame: Added support for rotated tiles + pygame: Added option to force a bitsize (depth) for surfaces + pygame: Added option to convert alpha transparency to colorkey transparency + pygame: Tilesets no longer load with per-pixel alphas by default + pygame: Colorkey transparency should be correctly handled now + + +NOTES: + +* The Tiled "properties" have reserved names. + +If you use "properties" for any of the following object types, you cannot use +any of theese words as a name for your property. A ValueError will be raised +if there are any conflicts. + +As of 0.8.1, these values are: + +map: version, orientation, width, height, tilewidth, tileheight + properties, tileset, layer, objectgroup + +tileset: firstgid, source, name, tilewidth, tileheight, spacing, margin, + image, tile, properties + +tile: id, image, properties + +layer: name, x, y, width, height, opacity, properties, data + +objectgroup: name, color, x, y, width, height, opacity, object, properties + +object: name, type, x, y, width, height, gid, properties, polygon, + polyline, image + + + +I have been intentionally not including a module rendering utility since +rendering a map will have different requirements for every situation. However, +I can appreciate that some poeple won't understand how it works unless they see +it, so I am including a sample map and viewer. It includes a scrolling/zooming +renderer. They are for demonstation purposes, and may not be suitable for all +projects. Use at your own risk. + +I've also included a copy of this loader that may work with python 3.x. I +personally do not think that python 3.x should be used with pygame, yet (and I +am not the only person). You can try it if you insist on using pygame with +python 3.x, but I don't update that often. + +=============================================================================== + +Basic usage sample: + + >>> from pytmx import tmxloader + >>> tmxdata = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "getTileImage": + + >>> image = tmxdata.getTileImage(x, y, layer) + >>> screen.blit(position, image) + + +Maps, tilesets, layers, objectgroups, and objects all have a simple way to +access metadata that was set inside tiled: they all become object attributes. + + >>> layer = tmxdata.layers[0] + >>> print layer.tilewidth + 32 + >>> print layer.weather + 'sunny' + + +Tiles properties are the exception here, and must be accessed through +"getTileProperties". The data is a regular Python dictionary: + + >>> tile = tmxdata.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + + + +Please see the TiledMap class for some api information. +""" +from utils import types +from constants import * + + + +def load_tmx(filename, *args, **kwargs): + # for .14 compatibility + from pytmx import TiledMap + + tiledmap = TiledMap(filename) + return tiledmap + + +def load_images_pygame(tmxdata, mapping, *args, **kwargs): + """ + due to the way the tiles are loaded, they will be in the same pixel format + as the display when it is loaded. take this into consideration if you + intend to support different screen pixel formats. + + by default, the images will not have per-pixel alphas. this can be + changed by including "pixelalpha=True" in the keywords. this will result + in much slower blitting speeds. + + if the tileset's image has colorkey transparency set in Tiled, the loader + will return images that have their transparency already set. using a + tileset with colorkey transparency will greatly increase the speed of + rendering the map. + + optionally, you can force the loader to strip the alpha channel of the + tileset image and to fill in the missing areas with a color, then use that + new color as a colorkey. the resulting tiles will render much faster, but + will not preserve the transparency of the tile if it uses partial + transparency (which you shouldn't be doing anyway, this is SDL). + """ + from itertools import product + from pygame import Surface, mask + import pygame, os + + + def handle_transformation(tile, flags): + if flags: + fx = flags & TRANS_FLIPX == TRANS_FLIPX + fy = flags & TRANS_FLIPY == TRANS_FLIPY + r = flags & TRANS_ROT == TRANS_ROT + + if r: + # not sure why the flip is required...but it is. + newtile = pygame.transform.rotate(tile, 270) + newtile = pygame.transform.flip(newtile, 1, 0) + + if fx or fy: + newtile = pygame.transform.flip(newtile, fx, fy) + + elif fx or fy: + newtile = pygame.transform.flip(tile, fx, fy) + + # preserve any flags that may have been lost after the transformation + return newtile.convert(tile) + + else: + return tile + + + pixelalpha = kwargs.get("pixelalpha", False) + force_colorkey = kwargs.get("force_colorkey", False) + force_bitdepth = kwargs.get("depth", False) + + if force_colorkey: + try: + force_colorkey = pygame.Color(*force_colorkey) + except: + msg = "Cannot understand color: {}" + raise Exception, msg.format(force_colorkey) + + tmxdata.images = [0] * tmxdata.maxgid + + for firstgid, t in sorted((t.firstgid, t) for t in tmxdata.tilesets): + path = os.path.join(os.path.dirname(tmxdata.filename), t.source) + + image = pygame.image.load(path) + + w, h = image.get_size() + tile_size = (t.tilewidth, t.tileheight) + real_gid = t.firstgid - 1 + + if t.trans: + tileset_colorkey = pygame.Color("#{}".format(t.trans)) + + # i dont agree with margins and spacing, but i'll support it anyway + # such is life. okay.jpg + tilewidth = t.tilewidth + t.spacing + tileheight = t.tileheight + t.spacing + + # some tileset images may be slightly larger than the tile area + # ie: may include a banner, copyright, ect. this compensates for that + width = ((int((w-t.margin*2) + t.spacing) / tilewidth) * tilewidth) - t.spacing + height = ((int((h-t.margin*2) + t.spacing) / tileheight) * tileheight) - t.spacing + + # using product avoids the overhead of nested loops + p = product(xrange(t.margin, height+t.margin, tileheight), + xrange(t.margin, width+t.margin, tilewidth)) + + for (y, x) in p: + real_gid += 1 + gids = tmxdata.mapGID(real_gid) + if gids == []: continue + + # we do some tests to correctly handle the tile and set the right + # blitting flags. just grab a section of it. + temp = image.subsurface(((x,y), tile_size)) + + # count the number of pixels in the tile that are not transparent + px = mask.from_surface(temp).count() + + # there are no transparent pixels in the image + if px == tile_size[0] * tile_size[1]: + tile = temp.convert() + + # there are transparent pixels, and set to force a colorkey + elif force_colorkey: + tile = Surface(tile_size) + tile.fill(force_colorkey) + tile.blit(temp, (0,0)) + tile.set_colorkey(force_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and tiled set a colorkey + elif t.trans: + tile = temp.convert() + tile.set_colorkey(tileset_colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and set for perpixel alpha + elif pixelalpha: + tile = temp.convert_alpha() + + # there are transparent pixels, and we won't handle them + else: + tile = temp.convert() + + for gid, flags in gids: + tmxdata.images[gid] = handle_transformation(tile, flags) + + +def load_pygame(filename, *args, **kwargs): + tmxdata = load_tmx(filename, *args, **kwargs) + load_images_pygame(tmxdata, None, *args, **kwargs) + + return tmxdata + + + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/003/pytmx/tmxloader3.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/003/pytmx/tmxloader3.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,612 @@ +""" +Map loader for TMX Files +bitcraft (leif.theden at gmail.com) +v.7 -- for python 3.x + +If you have any problems, please contact me via email. +Tested with Tiled 0.7.1 for Mac. + +released under the LGPL v3 + +====================================================================== +Design Goals: + Simple api + Memory efficient and fast + Quick access to tiles, attributes, and properties + +Non-Goals: + Rendering + +Works: + Pygame image loading + Map loading with all required types + Properties for all types: maps, layers, objects, tiles + Automatic flipping of tiles + +Todo: + Pygame: test colorkey transparency + + Optimized for maps that do not make heavy use of tile + properties. If i find that it is used a lot then i can rework + it for better performance. + +====================================================================== + +Basic usage sample: + + >>> import tmxloader + >>> tiledmap = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "get_tile_image": + + >>> image = tiledmap.get_tile_image(x, y, layer) + >>> screen.blit(position, image) + + +Layers, objectgroups, tilesets, and maps all have a simple way to access +metadata that was set inside tiled: they all become class attributes. + + >>> print(layer.tilewidth) + 32 + >>> print(layer.weather) + 'sunny' + + +Tiles are the exception here, and must be accessed through "getTileProperties" +and are regular Python dictionaries: + + >>> tile = tiledmap.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + +""" + +from itertools import chain + + +# internal flags +FLIP_X = 1 +FLIP_Y = 2 + + +# Tiled gid flags +GID_FLIP_X = 1<<31 +GID_FLIP_Y = 1<<30 + + +class TiledElement(object): + pass + +class TiledMap(TiledElement): + """ + not really useful unless "loaded" ie: don't instance directly. + see the pygame loader for inspiration + """ + + def __init__(self): + TiledElement.__init__(self) + self.layers = [] # list of all layer types (tile layers + object layers) + self.tilesets = [] # list of TiledTileset objects + self.tilelayers = [] # list of TiledLayer objects + self.objectgroups = [] # list of TiledObjectGroup objects + self.tile_properties = {} # dict of tiles that have additional metadata (properties) + self.filename = None + + # this is a work around to tiled's strange way of storing gid's + self.images = [0] + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 + self.height = 0 + self.tilewidth = 0 + self.tileheight = 0 + + def get_tile_image(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + usefull if you don't want to repeatedly call get_tile_image + probably not the most effecient way of doing this, but oh well. + """ + + raise NotImplementedError + + def getObjects(self): + """ + Return iterator all of the objects associated with this map + """ + + return chain(*[ i.objects for i in self.objectgroups ]) + + def getTileProperties(self, x, y, layer): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception(msg) + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception(msg) + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + +# the following classes get their attributes filled in with the loader + +class TiledTileset(TiledElement): + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.firstgid = 0 + self.lastgid = 0 + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + +class TiledLayer(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.data = None + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = 1 + +class TiledObjectGroup(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.objects = [] + + # defaults from the specification + self.name = None + +class TiledObject(TiledElement): + __slots__ = ['name', 'type', 'x', 'y', 'width', 'height', 'gid'] + + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + +def load_tmx(filename): + """ + Utility function to parse a Tiled TMX and return a usable object. + Images will not be loaded, so probably not useful to call this directly + + See the load_pygame func for an idea of what to do + """ + + from xml.dom.minidom import parse + from itertools import tee, islice, chain + from collections import defaultdict + from struct import unpack + import array, os + + # used to change the unicode string returned from minidom to + # proper python variable types. + types = { + "version": float, + "orientation": str, + "width": int, + "height": int, + "tilewidth": int, + "tileheight": int, + "firstgid": int, + "source": str, + "name": str, + "spacing": int, + "margin": int, + "source": str, + "trans": str, + "id": int, + "opacity": float, + "visible": bool, + "encoding": str, + "compression": str, + "gid": int, + "type": str, + "x": int, + "y": int, + "value": str, + } + + def pairwise(iterable): + # return a list as a sequence of pairs + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + def group(l, n): + # return a list as a sequence of n tuples + return zip(*[islice(l, i, None, n) for i in range(n)]) + + def parse_properties(node): + """ + parse a node and return a dict that represents a tiled "property" + """ + + d = {} + + for child in node.childNodes: + if child.nodeName == "properties": + for subnode in child.getElementsByTagName("property"): + # the "properties" from tiled's tmx have an annoying + # quality that "name" and "value" is included as part of it. + # so we mangle it to get that stuff out. + d.update(dict(pairwise([ str(i.value) for i in list(subnode.attributes.values()) ]))) + + return d + + def get_properties(node): + """ + parses a node and returns a dict that contains the data from the node's + attributes and any data from "property" elements as well. + """ + + d = {} + + # get tag attributes + d.update(get_attributes(node)) + + # get vlues of the properties element, if any + d.update(parse_properties(node)) + + return d + + def set_properties(obj, node): + """ + read the xml attributes and tiled "properties" from a xml node and fill in + the values into an object's dictionary + """ + + [ setattr(obj, k, v) for k,v in list(get_properties(node).items()) ] + + def get_attributes(node): + """ + get the attributes from a node and fix them to the correct type + """ + + d = defaultdict(lambda:None) + + for k, v in list(node.attributes.items()): + k = str(k) + d[k] = types[k](v) + + return d + + def decode_gid(raw_gid): + # gid's are encoded with extra information + # as of 0.7.0 it determines if the tile should be flipped when rendered + + flags = 0 + if raw_gid & GID_FLIP_X == GID_FLIP_X: flags += FLIP_X + if raw_gid & GID_FLIP_Y == GID_FLIP_Y: flags += FLIP_Y + gid = raw_gid & ~(GID_FLIP_X | GID_FLIP_Y) + + return gid, flags + + + def parse_map(node): + """ + parse a map node from a tiled tmx file + return a tiledmap + """ + + tiledmap = TiledMap() + tiledmap.filename = filename + set_properties(tiledmap, map_node) + + for node in map_node.getElementsByTagName("tileset"): + t, tiles = parse_tileset(node) + tiledmap.tilesets.append(t) + tiledmap.tile_properties.update(tiles) + + for node in dom.getElementsByTagName("layer"): + l = parse_layer(tiledmap.tilesets, node) + tiledmap.tilelayers.append(l) + tiledmap.layers.append(l) + + for node in dom.getElementsByTagName("objectgroup"): + o = parse_objectgroup(node) + tiledmap.objectgroups.append(o) + tiledmap.layers.append(o) + + return tiledmap + + + def parse_tileset(node, firstgid=None): + """ + parse a tileset element and return a tileset object and properties for tiles as a dict + """ + + tileset = TiledTileset() + set_properties(tileset, node) + tiles = {} + + if firstgid != None: + tileset.firstgid = firstgid + + # since tile objects probably don't have a lot of metadata, + # we store it seperately from the class itself + for child in node.childNodes: + if child.nodeName == "tile": + p = get_properties(child) + gid = p["id"] + tileset.firstgid + del p["id"] + tiles[gid] = p + + # check for tiled "external tilesets" + if hasattr(tileset, "source"): + if tileset.source[-4:].lower() == ".tsx": + try: + # we need to mangle the path some because tiled stores relative paths + path = os.path.join(os.path.dirname(filename), tileset.source) + tsx = parse(path) + except IOError: + raise IOError("Cannot load external tileset: " + path) + + tileset_node = tsx.getElementsByTagName("tileset")[0] + tileset, tiles = parse_tileset(tileset_node, tileset.firstgid) + else: + raise Exception("Found external tileset, but cannot handle type: " + tileset.source) + + # if we have an "image" tag, process it here + try: + image_node = node.getElementsByTagName("image")[0] + except IndexError: + pass + else: + attr = get_attributes(image_node) + tileset.source = attr["source"] + tileset.trans = attr["trans"] + + # calculate the number of tiles in this tileset + x, r = divmod(attr["width"], tileset.tilewidth) + y, r = divmod(attr["height"], tileset.tileheight) + + tileset.lastgid = tileset.firstgid + x + y + + return tileset, tiles + + + def parse_layer(tilesets, node): + """ + parse a layer element and return a layer object + + tilesets is required since we need to mangle gid's here + """ + + layer = TiledLayer() + layer.data = [] + layer.flipped_tiles = [] + set_properties(layer, node) + + data = None + next_gid = None + + data_node = node.getElementsByTagName("data")[0] + attr = get_attributes(data_node) + + if attr["encoding"] == "base64": + from base64 import b64decode + data = b64decode(bytes(data_node.lastChild.nodeValue, 'ascii')) + + elif attr["encoding"] == "csv": + next_gid = map(int, "".join([line.strip() for line in data_node.lastChild.nodeValue]).split(",")) + + elif not attr["encoding"] == None: + raise Exception("TMX encoding type: " + str(attr["encoding"]) + " is not supported.") + + if attr["compression"] == "gzip": + from io import BytesIO + import gzip + with gzip.GzipFile(fileobj=BytesIO(data)) as fh: + data = fh.read() + + elif not attr["compression"] == None: + raise Exception("TMX compression type: " + str(attr["compression"]) + " is not supported.") + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + if attr["encoding"] == next_gid == None: + def get_children(parent): + for child in parent.getElementsByTagName("tile"): + yield int(child.getAttribute("gid")) + + next_gid = get_children(data_node) + + elif not data == None: + # cast the data as a list of 32-bit integers + def u(i): return unpack(" + + + + + + + + + + + + + + + + + + + + + + + eJxjZGBgYKQDpiUY7nYwUYDpaQeMPdT9gcsOJiSaFnYgqxnqYUWp+YPJH7S2A5saSgGu+KC1HdQG9KibQBgAcvcBjQ== + + + + + eJyTZ2BgkKcDVqIhHu52UDPcR6Id+MJ61A762zFc0hW17aBHWTJU7aA1BgDmfzlJ + + + + + eJy1VdtqwzAMFWQbFHaBwNq1DyFhD92lD7Zr//+v1WY65ERTRyFMIOzKro6OJCtJRJL8SNA1qi3KLNG5x8L/xd1E+7uqH/+g94Q7OnF1tM/mrNBanDt91XPVQeZcjHT/tep2he4Ia9A1EI9i7EJ7rk3642yk/UB+rN1KvNEGX4+Or2YveoYcIta2f6u6d/RAPsDnVh5BMXA+mVizxvRu4mFfmeJFPc4OP+RjonPwFcXge168ydiBLbJ8g5P63hAOYzw4voABns3ey9xXQZb8OVdFOWXFsPMAGB3xQJ3whry++qr67ehJzzE/PB4Wm+0v9H87n7jO3BuoLXLqYXTyu+acMwj7tdjwhXeAvDOPo6ybg5+y7FGuB3qlSSa+ntgZCWk+evrN7+NJ1s3Ba/qsWODC3xbuRc47vjtB5tlh5yMk0noBeX8qEg== + + + + + + + + + + + + + + + + + + diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/003/resources/objects.png Binary file 004/003/resources/objects.png has changed diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/003/resources/spritesheet.png Binary file 004/003/resources/spritesheet.png has changed diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/003/resources/tilesheet.png Binary file 004/003/resources/tilesheet.png has changed diff -r 2a0f43c87b80 -r ca34c14ea3a8 004/003/sprites.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/004/003/sprites.py Fri Jun 29 23:06:48 2012 +0200 @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +import pygame + +import resources + + +FACING = { + (0, -1): 0, + (-1, 0): 1, + (0, 1): 2, + (1, 0): 3, +} + + +class SortedUpdates(pygame.sprite.RenderUpdates): + def sprites(self): + return sorted(self.spritedict.keys(), + key=lambda sprite: getattr(sprite, "depth", 0)) + + +class DecorationSprite(pygame.sprite.Sprite): + def __init__(self, (x, y)): + super(DecorationSprite, self).__init__() + self.spritesheet = resources.load_sheet('objects.png', 64) + self.image = self.spritesheet[0] + self.rect = self.image.get_rect() + self.rect.midbottom = (x * 32 + 16, y * 32 + 28) + self.depth = self.rect.bottom + + +class ShadowSprite(pygame.sprite.Sprite): + def __init__(self, sprite): + super(ShadowSprite, self).__init__() + self.spritesheet = resources.load_sheet('effects.png', 32) + self.image = self.spritesheet[0] + self.rect = self.image.get_rect() + self.image.set_alpha(0.6 * 255) + self.sprite = sprite + self.rect.midbottom = self.sprite.rect.midbottom + + def update(self): + self.rect.midbottom = self.sprite.rect.midbottom + + +class HeroSprite(pygame.sprite.Sprite): + def __init__(self, (x, y)): + super(HeroSprite, self).__init__() + self.spritesheet = resources.load_sheet('spritesheet.png', 64) + self.facing = 2 + self.image = self.spritesheet[self.facing] + self.rect = self.image.get_rect() + self.rect.midbottom = (x * 32 + 16, y * 32 + 32) + self.depth = self.rect.bottom + self.animations = [] + + def move(self, (dx, dy)): + self.facing = FACING[dx, dy] + self.animations.append(self.animation_walk((dx, dy))) + + def animation_walk(self, (dx, dy)): + for step in xrange(8): + self.image = self.spritesheet[self.facing + step * 4 + 4] + self.rect.move_ip(dx * 4, dy * 4) + self.depth = self.rect.bottom + yield + + def animation_stand(self): + self.image = self.spritesheet[self.facing] + return + yield + + def update(self): + if not self.animations: + self.animations.append(self.animation_stand()) + while self.animations: + try: + self.animations[0].next() + return + except StopIteration: + self.animations.pop(0)