Source code for tiledtmxloader.helperspygame
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
TileMap loader for python for Tiled, a generic tile map editor
from http://mapeditor.org/ .
It loads the \*.tmx files produced by Tiled.
This is the code that helps using the tmx files using pygame. In this
module there is a pygame specific loader and renderer.
TODO: mini example and usage of the classes from this module
TODO: link to homepage
TODO: examples
"""
# Versioning scheme based on:
# http://en.wikipedia.org/wiki/Versioning#Designating_development_stage
#
# +-- api change, probably incompatible with older versions
# | +-- enhancements but no api change
# | |
# major.minor[.build[.revision]]
# |
# +-|* 0 for alpha (status)
# |* 1 for beta (status)
# |* 2 for release candidate
# |* 3 for (public) release
#
# For instance:
# * 1.2.0.1 instead of 1.2-a
# * 1.2.1.2 instead of 1.2-b2 (beta with some bug fixes)
# * 1.2.2.3 instead of 1.2-rc (release candidate)
# * 1.2.3.0 instead of 1.2-r (commercial distribution)
# * 1.2.3.5 instead of 1.2-r5 (commercial distribution with many bug fixes)
from __future__ import division
__revision__ = "$Rev: 77 $"
__version__ = "3.0.0." + __revision__[6:-2]
__author__ = u'DR0ID @ 2009-2011'
if __debug__:
print __version__
import sys
sys.stdout.write(u'%s loading ... \n' % (__name__))
import time
_start_time = time.time()
# -----------------------------------------------------------------------------
import sys
from xml.dom import minidom, Node
import StringIO
import os.path
import pygame
import tiledtmxloader
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
[docs]class ResourceLoaderPygame(tiledtmxloader.AbstractResourceLoader):
"""
Resource loader for pygame. Loads the images as pygame.Surfaces and saves them in
the variable indexed_tiles.
Example::
res_loader = ResourceLoaderPygame()
# tile_map loaded the the TileMapParser.parse() method
res_loader.load(tile_map)
"""
def __init__(self):
tiledtmxloader.AbstractResourceLoader.__init__(self)
[docs] def load(self, tile_map):
tiledtmxloader.AbstractResourceLoader.load(self, tile_map)
self._img_cache.clear() # delete the original images from memory, they are all saved as tiles
# ISSUE 17: flipped tiles
for layer in self.world_map.layers:
if not layer.is_object_group:
for gid in layer.decoded_content:
if gid not in self.indexed_tiles:
if gid & self.FLIP_X or gid & self.FLIP_Y:
image_gid = gid & ~(self.FLIP_X | self.FLIP_Y)
offx, offy, img = self.indexed_tiles[image_gid]
img = img.copy()
img = pygame.transform.flip(img, bool(gid & self.FLIP_X), bool(gid & self.FLIP_Y))
self.indexed_tiles[gid] = (offx, offy, img)
def _load_image_parts(self, filename, margin, spacing, \
tile_width, tile_height, colorkey=None): #-> [images]
source_img = self._load_image(filename, colorkey)
width, height = source_img.get_size()
# ISSUE 16
# if the image size does not match a multiple of tile_width or
# tile_height it will mess up the number of tiles resulting in
# wrong GID's for the tiles
width = (width // tile_width) * tile_width
height = (height // tile_height) * tile_height
images = []
for ypos in xrange(margin, height, tile_height + spacing):
for xpos in xrange(margin, width, tile_width + spacing):
img_part = self._load_image_part(filename, xpos, ypos, \
tile_width, tile_height, colorkey)
images.append(img_part)
return images
def _load_image_part(self, filename, xpos, ypos, width, height, \
colorkey=None):
"""
Loads a image from a sprite sheet.
"""
source_img = self._load_image(filename, colorkey)
## ISSUE 4:
## The following usage seems to be broken in pygame (1.9.1.):
## img_part = pygame.Surface((tile_width, tile_height), 0, source_img)
img_part = pygame.Surface((width, height), \
source_img.get_flags(), \
source_img.get_bitsize())
source_rect = pygame.Rect(xpos, ypos, width, height)
## ISSUE 8:
## Set the colorkey BEFORE we blit the source_img
if colorkey:
img_part.set_colorkey(colorkey, pygame.RLEACCEL)
img_part.fill(colorkey)
img_part.blit(source_img, (0, 0), source_rect)
return img_part
def _load_image_file_like(self, file_like_obj, colorkey=None): # -> image
# pygame.image.load can load from a path and from a file-like object
# that is why here it is redirected to the other method
return self._load_image(file_like_obj, colorkey)
def _load_image(self, filename, colorkey=None):
img = self._img_cache.get(filename, None)
if img is None:
img = pygame.image.load(filename)
self._img_cache[filename] = img
if colorkey:
img.set_colorkey(colorkey, pygame.RLEACCEL)
return img
# def get_sprites(self):
# pass
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
[docs]class SpriteLayerNotCompatibleError(Exception): pass
[docs]class SpriteLayer(object):
"""
The SpriteLayer class. This class is used by the RendererPygame.
"""
[docs] class Sprite(object):
"""
The Sprite class used by the SpriteLayer class and the RendererPygame.
"""
def __init__(self, image, rect, source_rect=None, flags=0, key=None):
"""
Constructor.
:Parameters:
image : pygame.Surface
the image of this sprite
rect : pygame.Rect
the rect used when drawing
source_rect : pygame.Rect
source area rect, defaults to None
flags : int
flags for the blit method, defaults to 0
key : any
used internally for collapsing sprites
"""
self.image = image
# TODO: dont use a rect for position
self.rect = rect # blit rect
self.source_rect = source_rect
self.flags = flags
self.is_flat = False
self.z = 0
self.key = key
[docs] def get_draw_cond(self):
"""
Defines if the sprite lays on the floor or if it is up-right.
:returns:
The bottom y coordinate so the sprites can be sorted in right
draw order.
"""
if self.is_flat:
return self.rect.top + self.z
else:
return self.rect.bottom
def __init__(self, tile_layer_idx, resource_loader):
"""
:Parameters:
tile_layer_idx : int
Index of the tile layer to build upon
resource_loader : ResourceLoaderPygame
Instance of the ResourceLoaderPygame class which has loaded the resouces
"""
self._resource_loader = resource_loader
_world_map = self._resource_loader.world_map
self.layer_idx = tile_layer_idx
_layer = _world_map.layers[tile_layer_idx]
self.tilewidth = _world_map.tilewidth
self.tileheight = _world_map.tileheight
self.num_tiles_x = _world_map.width
self.num_tiles_y = _world_map.height
self.position_x = _layer.x
self.position_y = _layer.y
self._level = 1 # start with an invalid level
self.paralax_factor_x = 1.0
self.paralax_factor_y = 1.0
self.sprites = []
self.is_object_group = _layer.is_object_group
self.visible = _layer.visible
self.bottom_margin = 0
self._bottom_margin = 0
# init data to default
# self.content2D = []
# generate the needed lists
# for xpos in xrange(self.num_tiles_x):
# self.content2D.append([None] * self.num_tiles_y)
self.content2D = [None] * self.num_tiles_y
for ypos in xrange(self.num_tiles_y):
self.content2D[ypos] = [None] * self.num_tiles_x
# fill them
_img_cache = {}
_img_cache["hits"] = 0
for ypos_new in xrange(0, self.num_tiles_y):
for xpos_new in xrange(0, self.num_tiles_x):
coords = self._get_list_of_neighbour_coord(xpos_new, ypos_new, \
1, self.num_tiles_x, self.num_tiles_y)
if coords:
key, sprites = SpriteLayer._get_sprites_fromt_tiled_layer(\
coords, _layer, self._resource_loader.indexed_tiles)
sprite = None
if sprites:
sprite = SpriteLayer._union_sprites(sprites, key, \
_img_cache)
if sprite.rect.height > self._bottom_margin:
self._bottom_margin = sprite.rect.height
self.content2D[ypos_new][xpos_new] = sprite
self.bottom_margin = self._bottom_margin
if __debug__:
print '%s: Sprite Cache hits: %d' % \
(self.__class__.__name__, _img_cache["hits"])
del _img_cache
[docs] def get_collapse_level(self):
"""
The level of collapsing.
:returns:
The collapse level.
"""
return self._level
# TODO: test scale
@staticmethod
[docs] def scale(layer_orig, scale_w, scale_h): # -> sprite_layer
"""
Scales a layer and returns a new, scaled SpriteLayer.
:Parameters:
scale_w : float
Width scale factor in range (0, ...]
scale_h : float
Height scale factor in range (0, ...]
"""
layer = SpriteLayer(layer_orig.layer_idx, layer_orig._resource_loader)
layer.tilewidth = layer_orig.tilewidth * scale_w
layer.tileheight = layer_orig.tileheight * scale_h
layer.num_tiles_x = layer_orig.width * scale_w
layer.num_tiles_y = layer_orig.height * scale_h
layer.position_x = layer_orig.position_x
layer.position_y = layer_orig.position_y
layer._level = layer_orig._level
layer.paralax_factor_x = layer_orig.paralax_factor_x
layer.paralax_factor_y = layer_orig.paralax_factor_y
layer.sprites = layer_orig.sprites
layer.is_object_group = layer_orig.is_object_group
layer.visible = layer_orig.visible
for xidx, row in enumerate(layer_orig.content2D):
for yidx, sprite in enumerate(row):
w, h = sprite.image.get_size()
new_w = w * scale_w
new_h = h * scale_h
image = pygame.transform.smoothscale(sprite.image, \
(new_w, new_h))
x, y = sprite.rect.topleft
rect = pygame.Rect(x * scale_w, y * scale_h, new_w, new_h)
layer.content2D[yidx][xidx] = SpriteLayer.Sprite(image, rect)
return layer
# TODO: implement merge
@staticmethod
[docs] def merge(layers): # -> sprite_layer
"""
Merges multiple Sprite layers into one. Only SpriteLayers are supported.
All layers need to be equal in tile size, number of tiles and layer
position. Otherwise a SpriteLayerNotCompatibleError is raised.
:Parameters:
layers : list
The SpriteLayer to be merged
:returns: new SpriteLayer with merged tiles
"""
tile_width = None
tile_height = None
num_tiles_x = None
num_tiles_y = None
position_x = None
position_y = None
new_layer = None
for layer in layers:
# if layer.is_object_group:
# raise Exception("not a SpriteLayer")
assert isinstance(layer, SpriteLayer), "layer is not an instance of SpriteLayer"
# just use the values from first layer
tile_width = tile_width if tile_width else layer.tile_width
tile_height = tile_height if tile_height else layer.tile_height
num_tiles_x = num_tiles_x if num_tiles_x else layer.num_tiles_x
num_tiles_y = num_tiles_y if num_tiles_y else layer.num_tiles_y
position_x = position_x if position_x else layer.position_x
position_y = position_y if position_y else layer.position_y
# check they are equal for all layers
if layer.tile_width != tile_width:
raise SpriteLayerNotCompatibleError("layers do not have same tile_width")
if layer.tile_height != tile_height:
raise SpriteLayerNotCompatibleError("layers do not have same tile_height")
if layer.num_tiles_x != num_tiles_x:
raise SpriteLayerNotCompatibleError("layers do not have same number of tiles in x direction")
if layer.num_tiles_y != num_tiles_y:
raise SpriteLayerNotCompatibleError("layers do not have same number of tiles in y direction")
if layer.position_x != position_x:
raise SpriteLayerNotCompatibleError("layers are not at same position in x")
if layer.position_y != position_y:
raise SpriteLayerNotCompatibleError("layers are not at same position in y")
if new_layer is None:
new_layer = SpriteLayer(-2, layer._resource_loader)
for ypos_new in xrange(0, num_tiles_y):
for xpos_new in xrange(0, num_tiles_x):
sprite = layer.content2D[ypos_new][xpos_new]
if sprite:
new_sprite = new_layer.content2D[ypos_new][xpos_new]
if new_sprite:
assert sprite.rect.topleft == new_sprite.rect.topleft
assert sprite.rect.size == new_sprite.rect.size
new_sprite.image.blit(sprite.image, (0, 0), sprite.source_rect, sprite.flags)
else:
new_sprite = sprite
new_layer.content2D[ypos_new][xpos_new] = new_sprite
return new_layer
@staticmethod
[docs] def collapse(layer):
"""
Makes 1 tile out of 4. The idea behind is that fewer tiles
are faster to render, but that is not always true.
Grouping them together into one bigger sprite is one way to get fewer
sprites.
:not: This only works for static layers without any dynamic sprites.
:note: use with caution
:Parameters:
laser : SpriteLayer
The layer to collapse
:returns: new SpriteLayer with fewer sprites but double the size.
"""
# + 0' 1' 2'
# 0 1 2 3 4
# 0' 0 +----+----+----+----+
# | | | | |
# 1 +----+----+----+----+
# | | | | |
# 1' 2 +----+----+----+----+
# | | | | |
# 3 +----+----+----+----+
# | | | | |
# 2' 4 +----+----+----+----+
level = 2
if layer.is_object_group:
return layer
new_tilewidth = layer.tilewidth * level
new_tileheight = layer.tileheight * level
new_num_tiles_x = int(layer.num_tiles_x / level)
new_num_tiles_y = int(layer.num_tiles_y / level)
if new_num_tiles_x * level < layer.num_tiles_x:
new_num_tiles_x += 1
if new_num_tiles_y * level < layer.num_tiles_y:
new_num_tiles_y += 1
# print "old size", layer.num_tiles_x, layer.num_tiles_y
# print "new size", new_num_tiles_x, new_num_tiles_y
_content2D = [None] * new_num_tiles_y
# generate the needed lists
for ypos in xrange(new_num_tiles_y):
_content2D[ypos] = [None] * new_num_tiles_x
# fill them
_img_cache = {}
_img_cache["hits"] = 0
for ypos_new in xrange(0, new_num_tiles_y):
for xpos_new in xrange(0, new_num_tiles_x):
coords = SpriteLayer._get_list_of_neighbour_coord(\
xpos_new, ypos_new, level, \
layer.num_tiles_x, layer.num_tiles_y)
if coords:
sprite = SpriteLayer._get_sprite_from(coords, layer, \
_img_cache)
_content2D[ypos_new][xpos_new] = sprite
# print "len content2D:", len(self.content2D)
# TODO: separate constructor from init code (here the layer is parsed for nothing, content2D will be replaced)
new_layer = SpriteLayer( layer.layer_idx, layer._resource_loader)
new_layer.tilewidth = new_tilewidth
new_layer.tileheight = new_tileheight
new_layer.num_tiles_x = new_num_tiles_x
new_layer.num_tiles_y = new_num_tiles_y
new_layer.content2D = _content2D
# HACK:
new_layer._level = layer._level * 2
if __debug__ and level > 1:
print '%s: Sprite Cache hits: %d' % ("collapse", _img_cache["hits"])
return new_layer
@staticmethod
def _get_list_of_neighbour_coord(xpos_new, ypos_new, level, \
num_tiles_x, num_tiles_y):
"""
Finds the neighbours of a tile and returns them
:Parameters:
xpos_new : int
x position
ypos_new : int
y position
level : int
collapse level because this uses original tiles
num_tiles_x : int
number of tiles in x direction
num_tiles_y : int
number of tiles in y direction
:Returns:
list of coordinates of the neighbour tiles
"""
xpos = xpos_new * level
ypos = ypos_new * level
coords = []
for y in xrange(ypos, ypos + level):
for x in xrange(xpos, xpos + level):
if x <= num_tiles_x and y <= num_tiles_y:
coords.append((x, y))
return coords
@staticmethod
def _union_sprites(sprites, key, _img_cache):
"""
Unions sprites into one big one.
:Parameters:
sprites : list
list of sprites to union
key : iterable
key of the sprite, internal use only
_img_cache : dict
cache dict
:Returns:
new Sprite that unites all the given sprites.
"""
key = tuple(key)
# dont copy to a new image if only one sprite is in sprites
# (reduce memory usage)
# NOTE: this messes up the cache hits (only on non-collapsed maps)
if len(sprites) == 1:
sprite = sprites[0]
sprite.key = key
return sprite
# combine found sprites into one sprite
rect = sprites[0].rect.unionall(sprites)
# cache the images to save memory
if key in _img_cache:
image = _img_cache[key]
_img_cache["hits"] = _img_cache["hits"] + 1
else:
# make new image
image = pygame.Surface(rect.size, pygame.SRCALPHA | pygame.RLEACCEL)
image.fill((0, 0, 0, 0))
x, y = rect.topleft
for spr in sprites:
image.blit(spr.image, spr.rect.move(-x, -y))
_img_cache[key] = image
return SpriteLayer.Sprite(image, rect, key=key)
@staticmethod
def _get_sprites_fromt_tiled_layer(coords, layer, indexed_tiles):
"""
Get the sprites at the given coordinates from a tiled layer.
:Parameters:
coords : list
list of coordinates tuples
layer : TiledLayer
layer to extract the sprites from
indexed_tiles : dict
indexed tiles list loaded by the resource loader.
:Returns:
(keys, sprites) the new keys and sprites
"""
sprites = []
key = []
for xpos, ypos in coords:
## ISSUE 14: maps was displayed only sqared because wrong
## boundary checks
if xpos >= len(layer.content2D) or \
ypos >= len(layer.content2D[xpos]):
# print "CONTINUE", xpos, ypos
key.append(-1) # border and corner cases!
continue
idx = layer.content2D[xpos][ypos]
if idx:
offx, offy, img = indexed_tiles[idx]
world_x = xpos * layer.tilewidth + offx
world_y = ypos * layer.tileheight + offy
w, h = img.get_size()
rect = pygame.Rect(world_x, world_y, w, h)
sprite = SpriteLayer.Sprite(img, rect, key=idx)
key.append(idx)
sprites.append(sprite)
else:
key.append(-1)
return key, sprites
@staticmethod
def _get_sprite_from(coords, layer, _img_cache):
"""
Get one sprite for the given coordinates on the given layer.
:Parameters:
coords : list
tuples of coordinates (x, y)
layer : SpriteLayer
the layer to get the united sprite from
_img_cache : dict
dict for caching, internal use only
:returns:
a single sprite, uniting all given sprites on the fiven coordinates.
"""
sprites = []
key = []
for xpos, ypos in coords:
if ypos >= len(layer.content2D) or \
xpos >= len(layer.content2D[ypos]):
# print "CONTINUE", xpos, ypos
key.append(-1) # border and corner cases!
continue
idx = layer.content2D[ypos][xpos]
if idx:
sprite = idx
key.append(sprite.key)
sprites.append(sprite)
else:
key.append(-1)
if sprites:
sprite = SpriteLayer._union_sprites(sprites, key, _img_cache)
if __debug__:
x, y = sprite.rect.topleft
pygame.draw.rect(sprite.image, (255, 0, 0), \
sprite.rect.move(-x, -y), \
layer.get_collapse_level())
del sprites
return sprite
return None
[docs] def add_sprite(self, sprite):
"""
Add dynamic sprite to this layer.
:Parameters:
sprite : SpriteLayer.Sprite
sprite to add
"""
self.sprites.append(sprite)
if sprite.rect.height > self.bottom_margin:
self.bottom_margin = sprite.rect.height
[docs] def add_sprites(self, sprites):
"""
Add multiple dynamic sprites to this layer.
:Parameters:
sprites : list
list of SpriteLayer.Sprite to add
"""
for sprite in sprites:
self.add_sprite(sprite)
[docs] def remove_sprite(self, sprite):
"""
Removes a dynamic sprite from this layer.
:Parameters:
sprite : SpriteLayer.Sprite
sprite to remove
"""
if sprite in self.sprites:
self.sprites.remove(sprite)
self.bottom_margin = self._bottom_margin
for spr in self.sprites:
if spr.rect.height > self.bottom_margin:
self.bottom_margin = spr.rect.height
[docs] def remove_sprites(self, sprites):
"""
Remove multiple sprites at once.
:Parameters:
sprites : list
list of SpriteLayer.Sprite to remove
"""
for sprite in sprites:
self.remove_sprite(sprite)
[docs] def contains_sprite(self, sprite):
"""
Check if the given sprites is already in this layer.
:Parameters:
sprite : SpriteLayer.Sprite
sprite to check
:Returns:
bool, true if sprite is in this layer
"""
if sprite in self.sprites:
return True
return False
[docs] def has_sprites(self):
"""
Checks if this layer has dynamic sprites at all.
:Returns: bool, true if it contains at least 1 dynamic sprite.
"""
return (len(self.sprites) > 0)
[docs] def set_layer_paralax_factor(self, factor_x=1.0, factor_y=None):
"""
Set the paralax factor. This is for paralax scrolling this layer.
Values x < 0.0 will make the layer scroll in opposite direction
Value x == 0.0 makes the layer fix to the screen (wont scroll)
Values 0.0 < x < 1.0 will make scroll the layer slower.
Value x == 1.0 is default and make scroll the layer normal.
Values x > 1.0 make scroll the layer faster than normal
:Parameters:
factor_x : float
Paralax factor in x direction. Defaults to 1.0
factor_y : float
Paralax factor in y direction. If this is None then it will have
the same value as the factor_x argument.
"""
self.paralax_factor_x = factor_x
if factor_y:
self.paralax_factor_y = factor_y
else:
self.paralax_factor_y = factor_x
[docs] def get_layer_paralax_factor_x(self):
"""
Retrieve the current x paralax factor.
:Returns:
returns the current x paralax factor.
"""
return self.paralax_factor_x
[docs] def get_layer_paralax_factor_y(self):
"""
Retrieve the current y paralax factor.
:Returns:
returns the current y paralax factor.
"""
return self.paralax_factor_y
# -----------------------------------------------------------------------------
[docs]def get_layers_from_map(resource_loader):
"""
Creates SpriteLayers out of the map.
:Parameters:
resource_loader : ResourceLoaderPygame
a resource loader instance
:Returns: list of SpriteLayers
"""
layers = []
for idx, layer in enumerate(resource_loader.world_map.layers):
layers.append(get_layer_at_index(idx, resource_loader))
return layers
[docs]def get_layer_at_index(layer_idx, resource_loader):
"""
Creates one SpriteLayer from index out of the map.
:Parameters:
layer_idx : int
Index of the layer to create.
resource_loader : ResourceLoaderPygame
a resource loader instance
:Returns: a SpriteLayer instance
"""
layer = resource_loader.world_map.layers[layer_idx]
if layer.is_object_group:
return layer
return SpriteLayer(layer_idx, resource_loader)
# -----------------------------------------------------------------------------
[docs]class RendererPygame(object):
"""
A renderer for pygame. Should be fast enough for most purposes.
Example::
# init
sprite_layers = get_layers_from_map(resources)
renderer = RendererPygame()
# in main loop
while running:
# move camera
renderer.set_camera_position(x, y)
# draw layers
for sprite_layer in sprite_layers:
renderer.render_layer(screen, sprite_layer, clip_sprites)
"""
def __init__(self):
"""
Constructor.
"""
self._cam_rect = pygame.Rect(0, 0, 10, 10)
self._margin = (0, 0, 0, 0) # left, right, top, bottom
[docs] def set_camera_position(self, world_pos_x, world_pos_y, alignment='center'):
"""
Set the camera position in the world.
:Parameters:
world_pos_x : int
position in x in world coordinates
world_pos_y : int
position in y in world coordinates
alignment : string
defines to which part of the cam rect the position belongs,
can be any pygame.Rect attribute: 'center', 'topleft', 'topright', ...
"""
setattr(self._cam_rect, alignment, (world_pos_x, world_pos_y))
self._render_cam_rect.center = self._cam_rect.center
[docs] def set_camera_position_and_size(self, world_pos_x, world_pos_y, \
width, height, alignment='center'):
"""
Set the camera position and size in the world.
:Parameters:
world_pos_x : int
Position in x in world coordinates.
world_pos_y : int
Position in y in world coordinates.
witdh : int
With of the camera rect (the rendered area).
height : int
The height of the camera rect (the rendered area).
alignment : string
Defines to which part of the cam rect the position belongs,
can be any pygame.Rect attribute: 'center', 'topleft', 'topright', ...
"""
self._cam_rect.width = width
self._cam_rect.height = height
setattr(self._cam_rect, alignment, (world_pos_x, world_pos_y))
self.set_camera_margin(*self._margin)
[docs] def set_camera_rect(self, cam_rect_world_coord):
"""
Set the camera position and size using a rect in world coordinates.
:Parameters:
cam_rect_world_coord : pygame.Rect
A rect describing the cameras position and size in the world.
"""
self._cam_rect = cam_rect_world_coord
self.set_camera_margin(*self._margin)
[docs] def set_camera_margin(self, margin_left, margin_right, margin_top, margin_bottom):
"""
Set the margin around the camera (in pixels).
:Parameters:
margin_left : int
number of pixels of the left side marging
margin_right : int
number of pixels of the right side marging
margin_top : int
number of pixels of the top side marging
margin_bottom : int
number of pixels of the left bottom marging
"""
self._margin = (margin_left, margin_right, margin_top, margin_bottom)
self._render_cam_rect = pygame.Rect(self._cam_rect)
# adjust left margin
self._render_cam_rect.left = self._render_cam_rect.left - margin_left
# adjust right margin
self._render_cam_rect.width = self._render_cam_rect.width + margin_left + margin_right
# adjust top margin
self._render_cam_rect.top = self._render_cam_rect.top - margin_top
# adjust bottom margin
self._render_cam_rect.height = self._render_cam_rect.height + margin_top + margin_bottom
[docs] def render_layer(self, surf, layer, clip_sprites=True, \
sort_key=lambda spr: spr.get_draw_cond()):
"""
Renders a layer onto the given surface.
:Parameters:
surf : Surface
Surface to render onto.
layer : SpriteLayer
The layer to render. Invisible layers will be skipped.
clip_sprites : boolean
Optional, defaults to True. Clip the sprites of this layer to only draw the ones
intersecting the visible part of the world.
sort_key : function
Optional: The sort function for the parameter 'key' of the sort
method of the list.
"""
if layer.visible:
if layer.is_object_group:
return
if layer.bottom_margin > self._margin[3]:
left, right, top, bottom = self._margin
self.set_camera_margin(left, right, top, layer.bottom_margin)
# optimizations
surf_blit = surf.blit
layer_content2D = layer.content2D
tile_h = layer.tileheight
# self.paralax_factor_y = 1.0
# self.paralax_center_x = 0.0
cam_rect = self._render_cam_rect
# print 'cam rect:', self._cam_rect
# print 'render r:', self._render_cam_rect
cam_world_pos_x = cam_rect.x * layer.paralax_factor_x + \
layer.position_x
cam_world_pos_y = cam_rect.y * layer.paralax_factor_y + \
layer.position_y
# camera bounds, restricting number of tiles to draw
left = int(round(float(cam_world_pos_x) // layer.tilewidth))
right = int(round(float(cam_world_pos_x + cam_rect.width) // \
layer.tilewidth)) + 1
top = int(round(float(cam_world_pos_y) // tile_h))
bottom = int(round(float(cam_world_pos_y + cam_rect.height) // \
tile_h)) + 1
left = left if left > 0 else 0
right = right if right < layer.num_tiles_x else layer.num_tiles_x
top = top if top > 0 else 0
bottom = bottom if bottom < layer.num_tiles_y else layer.num_tiles_y
# print '???', layer.num_tiles_x, layer.num_tiles_y, left, right, top, bottom, cam_rect
# sprites
spr_idx = 0
len_sprites = 0
all_sprites = layer.sprites
if all_sprites:
# TODO: make filter visible sprites optional (maybe sorting too)
# use a marging around it
if clip_sprites:
sprites = [all_sprites[idx] \
for idx in cam_rect.collidelistall(all_sprites)]
else:
sprites = all_sprites
# could happend that all sprites are not visible by the camera
if sprites:
if sort_key:
sprites.sort(key=sort_key)
sprite = sprites[0]
len_sprites = len(sprites)
# render
for ypos in range(top, bottom):
# draw sprites in this layer
# (skip the ones outside visible area/map)
y = ypos + 1
while spr_idx < len_sprites and sprite.get_draw_cond() <= \
y * tile_h:
surf_blit(sprite.image, \
sprite.rect.move(-cam_world_pos_x, \
-cam_world_pos_y - sprite.z),\
sprite.source_rect, \
sprite.flags)
spr_idx += 1
if spr_idx < len_sprites:
sprite = sprites[spr_idx]
# next line of the map
for xpos in range(left, right):
tile_sprite = layer_content2D[ypos][xpos]
# print '?', xpos, ypos, tile_sprite
if tile_sprite:
surf_blit(tile_sprite.image, \
tile_sprite.rect.move( - cam_world_pos_x, \
-cam_world_pos_y), \
tile_sprite.source_rect, \
tile_sprite.flags)
[docs] def pick_layer(self, layer, screen_x, screen_y):
"""
Returns the sprite at the given screen position or None regardless of
the layers visibility.
:Note: This does not work wir object group layers.
:Parameters:
layer : SpriteLayer
the layer to pick from
screen_x : int
The screen position in x direction.
screen_y : int
The screen position in y direction.
:Returns:
None if there is no sprite or the sprite (SpriteLayer.Sprite instance).
"""
if layer.is_object_group:
pass
else:
world_pos_x, world_pos_y = self.get_world_pos(layer, screen_x, screen_y)
tile_x = int(world_pos_x / layer.tilewidth)
tile_y = int(world_pos_y / layer.tileheight)
if 0 <= tile_x < layer.num_tiles_x and 0 <= tile_y < layer.num_tiles_y:
sprite = layer.content2D[tile_y][tile_x]
if sprite:
return sprite
return None
[docs] def pick_layers_sprites(self, layer, screen_x, screen_y):
"""
Returns the sprites at the given screen positions or an empty list.
The sprites are the same order as in the layers.sprites list.
:Note: This does not work wir object group layers.
:Parameters:
layer : SpriteLayer
the layer to pick from
screen_x : int
The screen position in x direction.
screen_y : int
The screen position in y direction.
:Returns:
A list of sprites or an empty list.
"""
if layer.is_object_group:
pass
else:
world_pos_x, world_pos_y = self.get_world_pos(layer, screen_x, screen_y)
r = pygame.Rect(world_pos_x, world_pos_y, 1, 1)
indices = r.collidelistall(layer.sprites)
return [layer.sprites[idx] for idx in indices]
return []
[docs] def get_world_pos(self, layer, screen_x, screen_y):
"""
Returns the world coordinates for the given screen location and layer.
:Note: this is important so one can check which entity is there in the model
(knowing which sprite is there does not help much)
:Parameters:
layer : SpriteLayer
the layer to pick from
screen_x : int
The screen position in x direction.
screen_y : int
The screen position in y direction.
:Returns:
Tuple of world coordinates: (world_x, world_y)
"""
# TODO: also use layer.x and layer.y offset
return (screen_x + self._render_cam_rect.x * layer.paralax_factor_x, screen_y + self._render_cam_rect.y * layer.paralax_factor_y)
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
[docs]def demo_pygame(file_name):
class Dude(SpriteLayer.Sprite):
def __init__(self, img, rect):
super(Dude, self).__init__(img, rect)
self.random = __import__('random')
self.velocity_x = 0
self.velocity_y = 0
self.position_x = self.random.randint(100, 4000)
self.position_y = self.random.randint(100, 4000)
self.rect.center = (self.position_x, self.position_y)
def update(self, dt):
if self.random.random() < 0.05:
if self.velocity_x:
self.velocity_x = 0
self.velocity_y = 0
else:
self.velocity_x = self.random.randint(-10, 10) * 0.005
self.velocity_y = self.random.randint(-10, 10) * 0.005
self.position_x += self.velocity_x * dt
self.position_y += self.velocity_y * dt
self.rect.center = (self.position_x, self.position_y)
# parser the map (it is done here to initialize the
# window the same size as the map if it is small enough)
world_map = tiledtmxloader.TileMapParser().parse_decode(file_name)
# init pygame and set up a screen
pygame.init()
pygame.display.set_caption("tiledtmxloader - " + file_name)
screen_width = min(1024, world_map.pixel_width)
screen_height = min(768, world_map.pixel_height)
screen = pygame.display.set_mode((screen_width, screen_height))
# load the images using pygame
# image_loader = ImageLoaderPygame()
resources = ResourceLoaderPygame()
resources.load(world_map)
#printer(world_map)
# prepare map rendering
assert world_map.orientation == "orthogonal"
renderer = RendererPygame()
# cam_offset is for scrolling
cam_world_pos_x = 0
cam_world_pos_y = 0
# variables
frames_per_sec = 60.0
clock = pygame.time.Clock()
running = True
draw_obj = True
show_message = True
font = pygame.font.Font(None, 15)
s = "Frames Per Second: 0.0"
message = font.render(s, 0, (255,255,255), (0, 0, 0)).convert()
# for timed fps update
pygame.time.set_timer(pygame.USEREVENT, 1000)
# add additional sprites
num_sprites = 4
my_sprites = []
for i in range(num_sprites):
j = num_sprites - i
# image = pygame.Surface((20, j*40.0/num_sprites+10))
image = pygame.Surface((50, 70), pygame.SRCALPHA)
image.fill(((255+200*j)%255, (2*j+255)%255, (5*j)%255, 200))
# image.fill((255, 255, 255))
# sprite = RendererPygame.Sprite(image, image.get_rect())
sprite = Dude(image, image.get_rect())
my_sprites.append(sprite)
my_sprites[-1].z = 10
# renderer.add_sprites(1, my_sprites)
clip_sprites = True
hero_flat = False
# optimizations
num_keys = [pygame.K_0, pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, \
pygame.K_5, pygame.K_6, pygame.K_7, pygame.K_8, pygame.K_9]
pressed_layer = None
clock_tick = clock.tick
pygame_event_get = pygame.event.get
pygame_key_get_pressed = pygame.key.get_pressed
renderer_render_layer = renderer.render_layer
renderer_set_camera_position = renderer.set_camera_position
pygame_display_flip = pygame.display.flip
sprite_layers = get_layers_from_map(resources)
renderer.set_camera_position_and_size(cam_world_pos_x, cam_world_pos_y, \
screen_width, screen_height)
t = 0
# mainloop
while running:
dt = clock_tick()#60.0)
t += dt
# event handling
for event in pygame_event_get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYUP:
if event.key in num_keys:
pressed_layer = None
print "reset pressed layer", pressed_layer
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_F1:
print "fps:", clock.get_fps()
show_message = not show_message
print "show info:", show_message
# print "visible range x:", renderer._visible_x_range
# print "visible range y:", renderer._visible_y_range
elif event.key == pygame.K_F2:
draw_obj = not draw_obj
print "show objects:", draw_obj
elif event.key == pygame.K_F3:
clip_sprites = not clip_sprites
print "clip sprites:", clip_sprites
elif event.key == pygame.K_F4:
hero_flat = not hero_flat
print "hero is flat:", hero_flat
elif event.key == pygame.K_w:
cam_world_pos_y -= world_map.tileheight
elif event.key == pygame.K_s:
cam_world_pos_y += world_map.tileheight
elif event.key == pygame.K_d:
cam_world_pos_x += world_map.tilewidth
elif event.key == pygame.K_a:
cam_world_pos_x -= world_map.tilewidth
elif event.key in num_keys:
# find out which layer to manipulate
idx = num_keys.index(event.key)
# make sure this layer exists
if idx < len(world_map.layers):
pressed_layer = idx
print "set pressed layer", pressed_layer
if event.mod & pygame.KMOD_CTRL:
# uncollapse
# TODO: better interface
render_layer = sprite_layers[idx]
render_layer = SpriteLayer.collapse(render_layer)
sprite_layers[idx] = render_layer
print "layer has uncollapsed, level:", \
render_layer.get_collapse_level()
elif event.mod & pygame.KMOD_SHIFT:
# collapse
# TODO: better interface
render_layer = sprite_layers[idx]
sprite_layers[idx] = \
renderer.get_layer_at_index(idx, resources)
print "layer", idx, "RESET!"
elif event.mod & pygame.KMOD_ALT:
# hero sprites
# TODO: better interface
if sprite_layers[idx].contains_sprite(\
my_sprites[0]):
# renderer.remove_sprites(idx, my_sprites)
# TODO: better interface
sprite_layers[idx].remove_sprites(my_sprites)
print "removed hero sprites from layer", idx
else:
# renderer.add_sprites(idx, my_sprites)
# TODO: better interface
sprite_layers[idx].add_sprites(my_sprites)
print "added hero sprites to layer", idx
else:
# visibility
sprite_layers[idx].visible = \
not sprite_layers[idx].visible
print "layer", idx, "visible:", \
sprite_layers[idx].visible
else:
print "layer", idx, " does not exist on this map!"
elif event.key == pygame.K_UP:
if pressed_layer is not None:
# TODO: better interface
layer = sprite_layers[pressed_layer]
layer.set_layer_paralax_factor(\
layer.get_layer_paralax_factor_x() + 0.1)
print "increase paralax factox on layer", \
pressed_layer, " to:", \
layer.get_layer_paralax_factor_x()
elif event.key == pygame.K_DOWN:
if pressed_layer is not None:
layer = sprite_layers[pressed_layer]
layer.set_layer_paralax_factor(\
layer.get_layer_paralax_factor_x() - 0.1)
print "reduced paralax factox on layer", pressed_layer,\
" to:", layer.get_layer_paralax_factor_x()
elif event.type == pygame.USEREVENT:
t = 0
print clock.get_fps()
if show_message:
s = "Number of layers: %i (use 0-9 to toggle) F1-F2 for \
other functions Frames Per Second: %.2f" % \
(len(world_map.layers), clock.get_fps())
message = font.render( s, \
0, \
(255, 255, 255), \
(0, 0, 0)).convert()
if pressed_layer is None:
pressed = pygame_key_get_pressed()
# The speed is 3 by default.
# When left Shift is held, the speed increases.
# The speed interpolates based on time passed, so the demo navigates
# at a reasonable pace even on huge maps.
speed = (3.0 + pressed[pygame.K_LSHIFT] * 36.0) * \
(dt / frames_per_sec)
# cam movement
if pressed[pygame.K_DOWN]:
cam_world_pos_y += speed
if pressed[pygame.K_UP]:
cam_world_pos_y -= speed
if pressed[pygame.K_LEFT]:
cam_world_pos_x -= speed
if pressed[pygame.K_RIGHT]:
cam_world_pos_x += speed
# update sprites position
for spr in my_sprites:
spr.update(dt)
my_sprites[0].is_flat = hero_flat
my_sprites[-1].is_flat = hero_flat
my_sprites[0].rect.center = cam_world_pos_x , cam_world_pos_y
my_sprites[-1].rect.center = cam_world_pos_x + 20 , cam_world_pos_y
# adjust camera according the keypresses
renderer_set_camera_position(cam_world_pos_x, cam_world_pos_y)
# clear screen, might be left out if every pixel is redrawn anyway
screen.fill((0,0,0))
sprites = []
for layer in sprite_layers:
spr = renderer.pick_layer(layer, *pygame.mouse.get_pos())
if spr:
sprites.insert(0, spr)
for idx, spr in enumerate(sprites):
dud = my_sprites[2]
dud.rect.topleft = spr.rect.topleft
# render the map
# TODO: manage render layers
for sprite_layer in sprite_layers:
if sprite_layer.is_object_group:
# map objects
if draw_obj:
_draw_obj_group(screen, sprite_layer, cam_world_pos_x, \
cam_world_pos_y, font)
else:
renderer_render_layer(screen, sprite_layer, clip_sprites)
if show_message:
screen.blit(message, (0,0))
# print '??', len(sprites)
for idx, spr in enumerate(sprites):
screen.blit(spr.image, (idx * spr.rect.w, screen.get_size()[1] - spr.rect.h))
# print '>>>>>', dud.rect.topleft
pygame_display_flip()
def _draw_obj_group(screen, obj_group, cam_world_pos_x, cam_world_pos_y, font):
pygame = __import__('pygame')
goffx = obj_group.x
goffy = obj_group.y
for map_obj in obj_group.objects:
size = (map_obj.width, map_obj.height)
if map_obj.image_source:
surf = pygame.image.load(map_obj.image_source)
surf = pygame.transform.scale(surf, size)
screen.blit(surf, (goffx + map_obj.x - cam_world_pos_x, \
goffy + map_obj.y - cam_world_pos_y))
else:
r = pygame.Rect(\
(goffx + map_obj.x - cam_world_pos_x, \
goffy + map_obj.y - cam_world_pos_y),\
size)
pygame.draw.rect(screen, (255, 255, 0), r, 1)
text_img = font.render(map_obj.name, 1, (255, 255, 0))
screen.blit(text_img, r.move(1, 2))
# -----------------------------------------------------------------------------
# TODO:
# - pyglet demo: redo same as for pygame demo, better rendering
# - test if object gid is already read in and resolved
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
[docs]def main():
args = sys.argv[1:]
if len(args) < 1:
#print 'usage: python test.py mapfile.tmx [pygame|pyglet]'
print('usage: python %s your_map.tmx' % \
os.path.basename(__file__))
return
demo_pygame(args[0])
# -----------------------------------------------------------------------------
if __name__ == '__main__':
# import cProfile
# cProfile.run('main()', "stats.profile")
# import pstats
# p = pstats.Stats("stats.profile")
# p.strip_dirs()
# p.sort_stats('time')
# p.print_stats()
main()
if __debug__:
_dt = time.time() - _start_time
sys.stdout.write(u'%s loaded: %fs \n' % (__name__, _dt))