Improved Lab experiment
[2dworld.git] / tiledtmxloader / helperspygame.py
blobecd26efd7b90975d852174b161178fae9ba1f347
1 # -*- coding: utf-8 -*-
3 """
5 TileMap loader for python for Tiled, a generic tile map editor
6 from http://mapeditor.org/ .
7 It loads the \*.tmx files produced by Tiled.
9 This is the code that helps using the tmx files using pygame. In this
10 module there is a pygame specific loader and renderer.
12 """
14 # Versioning scheme based on:
15 # http://en.wikipedia.org/wiki/Versioning#Designating_development_stage
17 # +-- api change, probably incompatible with older versions
18 # | +-- enhancements but no api change
19 # | |
20 # major.minor[.build[.revision]]
21 # |
22 # +-|* 0 for alpha (status)
23 # |* 1 for beta (status)
24 # |* 2 for release candidate
25 # |* 3 for (public) release
27 # For instance:
28 # * 1.2.0.1 instead of 1.2-a
29 # * 1.2.1.2 instead of 1.2-b2 (beta with some bug fixes)
30 # * 1.2.2.3 instead of 1.2-rc (release candidate)
31 # * 1.2.3.0 instead of 1.2-r (commercial distribution)
32 # * 1.2.3.5 instead of 1.2-r5 (commercial distribution with many bug fixes)
35 __revision__ = "$Rev$"
36 __version__ = "3.0.0." + __revision__[6:-2]
37 __author__ = 'DR0ID @ 2009-2011'
40 # -----------------------------------------------------------------------------
42 from math import ceil
44 import pygame
46 from . import tmxreader
48 # -----------------------------------------------------------------------------
50 # -----------------------------------------------------------------------------
52 class ResourceLoaderPygame(tmxreader.AbstractResourceLoader):
53 """
54 Resource loader for pygame. Loads the images as pygame.Surfaces and saves
55 them in the variable indexed_tiles.
58 Example::
60 res_loader = ResourceLoaderPygame()
61 # tile_map loaded the the TileMapParser.parse() method
62 res_loader.load(tile_map)
64 """
66 def __init__(self):
67 tmxreader.AbstractResourceLoader.__init__(self)
69 def load(self, tile_map):
70 tmxreader.AbstractResourceLoader.load(self, tile_map)
71 # delete the original images from memory, they are all saved as tiles
72 self._img_cache.clear()
73 # ISSUE 17: flipped tiles
74 for layer in self.world_map.layers:
75 if not layer.is_object_group:
76 for gid in layer.decoded_content:
77 if gid not in self.indexed_tiles:
78 if gid & self.FLIP_X or gid & self.FLIP_Y:
79 image_gid = gid & ~(self.FLIP_X | self.FLIP_Y)
80 offx, offy, img = self.indexed_tiles[image_gid]
81 img = img.copy()
82 img = pygame.transform.flip(img, \
83 bool(gid & self.FLIP_X), \
84 bool(gid & self.FLIP_Y))
85 self.indexed_tiles[gid] = (offx, offy, img)
87 def _load_image_parts(self, filename, margin, spacing, \
88 tile_width, tile_height, colorkey=None): #-> [images]
89 source_img = self._load_image(filename, colorkey)
90 width, height = source_img.get_size()
91 # ISSUE 16
92 # if the image size does not match a multiple of tile_width or
93 # tile_height it will mess up the number of tiles resulting in
94 # wrong GID's for the tiles
95 width = (width // tile_width) * tile_width
96 height = (height // tile_height) * tile_height
97 images = []
98 for ypos in range(margin, height, tile_height + spacing):
99 for xpos in range(margin, width, tile_width + spacing):
100 img_part = self._load_image_part(filename, xpos, ypos, \
101 tile_width, tile_height, colorkey)
102 images.append(img_part)
103 return images
105 def _load_image_part(self, filename, xpos, ypos, width, height, \
106 colorkey=None):
108 Loads a image from a sprite sheet.
110 source_img = self._load_image(filename, colorkey)
111 ## ISSUE 4:
112 ## The following usage seems to be broken in pygame (1.9.1.):
113 ## img_part = pygame.Surface((tile_width, tile_height), 0, source_img)
114 img_part = pygame.Surface((width, height), \
115 source_img.get_flags(), \
116 source_img.get_bitsize())
117 source_rect = pygame.Rect(xpos, ypos, width, height)
119 ## ISSUE 8:
120 ## Set the colorkey BEFORE we blit the source_img
121 if colorkey:
122 img_part.set_colorkey(colorkey, pygame.RLEACCEL)
123 img_part.fill(colorkey)
125 img_part.blit(source_img, (0, 0), source_rect)
127 return img_part
129 def _load_image_file_like(self, file_like_obj, colorkey=None): # -> image
130 # pygame.image.load can load from a path and from a file-like object
131 # that is why here it is redirected to the other method
132 return self._load_image(file_like_obj, colorkey)
134 def _load_image(self, filename, colorkey=None):
135 img = self._img_cache.get(filename, None)
136 if img is None:
137 img = pygame.image.load(filename)
138 self._img_cache[filename] = img
139 if colorkey:
140 img.set_colorkey(colorkey, pygame.RLEACCEL)
141 return img
143 # def get_sprites(self):
144 # pass
148 # -----------------------------------------------------------------------------
149 # -----------------------------------------------------------------------------
150 class SpriteLayerNotCompatibleError(Exception): pass
152 class SpriteLayer(object):
154 The SpriteLayer class. This class is used by the RendererPygame.
159 class Sprite(object):
161 The Sprite class used by the SpriteLayer class and the RendererPygame.
164 def __init__(self, image, rect, source_rect=None, flags=0, key=None):
166 Constructor.
167 :Parameters:
168 image : pygame.Surface
169 the image of this sprite
170 rect : pygame.Rect
171 the rect used when drawing
172 source_rect : pygame.Rect
173 source area rect, defaults to None
174 flags : int
175 flags for the blit method, defaults to 0
176 key : any
177 used internally for collapsing sprites
180 self.image = image
181 # TODO: dont use a rect for position
182 self.rect = rect # blit rect
183 self.source_rect = source_rect
184 self.flags = flags
185 self.is_flat = False
186 self.z = 0
187 self.key = key
189 def get_draw_cond(self):
191 Defines if the sprite lays on the floor or if it is up-right.
193 :returns:
194 The bottom y coordinate so the sprites can be sorted in right
195 draw order.
197 if self.is_flat:
198 return self.rect.top + self.z
199 else:
200 return self.rect.bottom
202 def __init__(self, tile_layer_idx, resource_loader):
205 :Parameters:
206 tile_layer_idx : int
207 Index of the tile layer to build upon
208 resource_loader : ResourceLoaderPygame
209 Instance of the ResourceLoaderPygame class which has loaded
210 the resouces
212 self._resource_loader = resource_loader
213 _world_map = self._resource_loader.world_map
214 self.layer_idx = tile_layer_idx
215 _layer = _world_map.layers[tile_layer_idx]
217 self.tilewidth = _world_map.tilewidth
218 self.tileheight = _world_map.tileheight
219 self.num_tiles_x = _world_map.width
220 self.num_tiles_y = _world_map.height
221 self.position_x = _layer.x
222 self.position_y = _layer.y
225 self._level = 1
227 # TODO: change scale attributes to properties?
228 self.scale_x = 1.0
229 self.scale_y = 1.0
231 # TODO: either change paralax_* attributes to properties
232 # or make them private
233 self.paralax_factor_x = 1.0
234 self.paralax_factor_y = 1.0
236 self.sprites = []
237 self.is_object_group = _layer.is_object_group
238 self.visible = _layer.visible
239 self.bottom_margin = 0
240 self._bottom_margin = 0
243 # init data to default
244 # self.content2D = []
245 # generate the needed lists
246 # for xpos in xrange(self.num_tiles_x):
247 # self.content2D.append([None] * self.num_tiles_y)
249 self.content2D = [None] * self.num_tiles_y
250 for ypos in range(self.num_tiles_y):
251 self.content2D[ypos] = [None] * self.num_tiles_x
253 # fill them
254 _img_cache = {}
255 _img_cache["hits"] = 0
256 for ypos_new in range(0, self.num_tiles_y):
257 for xpos_new in range(0, self.num_tiles_x):
258 coords = self._get_list_of_neighbour_coord(xpos_new, ypos_new, \
259 1, self.num_tiles_x, self.num_tiles_y)
260 if coords:
261 key, sprites = SpriteLayer._get_sprites_fromt_tiled_layer(\
262 coords, _layer, self._resource_loader.indexed_tiles)
264 sprite = None
265 if sprites:
266 sprite = SpriteLayer._union_sprites(sprites, key, \
267 _img_cache)
268 if sprite.rect.height > self._bottom_margin:
269 self._bottom_margin = sprite.rect.height
271 self.content2D[ypos_new][xpos_new] = sprite
272 self.bottom_margin = self._bottom_margin
273 if __debug__:
274 print('%s: Sprite Cache hits: %d' % \
275 (self.__class__.__name__, _img_cache["hits"]))
276 del _img_cache
278 def get_collapse_level(self):
280 The level of collapsing.
282 :returns:
283 The collapse level.
285 return self._level
287 # TODO: test scale
288 @staticmethod
289 def scale(layer_orig, scale_w, scale_h): # -> sprite_layer
291 Scales a layer and returns a new, scaled SpriteLayer.
293 :Note: This method is slow and inefficient
295 :Parameters:
296 scale_w : float
297 Width scale factor in range (0, ...]
298 scale_h : float
299 Height scale factor in range (0, ...]
301 if layer_orig.is_object_group:
302 return layer
304 layer = SpriteLayer(layer_orig.layer_idx, layer_orig._resource_loader)
306 layer.tilewidth = layer_orig.tilewidth * scale_w
307 layer.tileheight = layer_orig.tileheight * scale_h
308 layer.position_x = layer_orig.position_x
309 layer.position_y = layer_orig.position_y
312 layer._level = layer_orig._level
314 layer.paralax_factor_x = layer_orig.paralax_factor_x
315 layer.paralax_factor_y = layer_orig.paralax_factor_y
316 layer.sprites = layer_orig.sprites
317 layer.is_object_group = layer_orig.is_object_group
318 layer.visible = layer_orig.visible
319 layer.scale_x = scale_w
320 layer.scale_y = scale_h
322 layer.content2D = [0] * len(layer_orig.content2D)
323 for yidx, row in enumerate(layer_orig.content2D):
324 layer.content2D[yidx] = [0] * len(row)
325 for xidx, sprite in enumerate(row):
326 if sprite:
327 w, h = sprite.image.get_size()
328 new_w = w * scale_w
329 new_h = h * scale_h
330 rect = sprite.rect
331 image = sprite.image
332 # prevent fractional numbers and scaling glitches
333 if w != ceil(new_w) or h != ceil(new_h):
334 new_w = ceil(new_w)
335 new_h = ceil(new_h)
336 image = pygame.transform.smoothscale(sprite.image, \
337 (new_w, new_h))
338 x, y = sprite.rect.topleft
339 rect = pygame.Rect(x * scale_w, y * scale_h, \
340 new_w, new_h)
342 layer.content2D[yidx][xidx] = \
343 SpriteLayer.Sprite(image, rect)
344 else:
345 layer.content2D[yidx][xidx] = None
347 return layer
349 # TODO: implement merge
350 @staticmethod
351 def merge(layers): # -> sprite_layer
353 Merges multiple Sprite layers into one. Only SpriteLayers are supported.
354 All layers need to be equal in tile size, number of tiles and layer
355 position. Otherwise a SpriteLayerNotCompatibleError is raised.
357 :Parameters:
358 layers : list
359 The SpriteLayer to be merged
361 :returns: new SpriteLayer with merged tiles
364 tile_width = None
365 tile_height = None
366 num_tiles_x = None
367 num_tiles_y = None
368 position_x = None
369 position_y = None
370 new_layer = None
372 for layer in layers:
373 if layer.is_object_group:
374 # skip object group layers
375 continue
377 assert isinstance(layer, SpriteLayer), "layer is not an instance of SpriteLayer"
379 # just use the values from first layer
380 tile_width = tile_width if tile_width else layer.tile_width
381 tile_height = tile_height if tile_height else layer.tile_height
382 num_tiles_x = num_tiles_x if num_tiles_x else layer.num_tiles_x
383 num_tiles_y = num_tiles_y if num_tiles_y else layer.num_tiles_y
384 position_x = position_x if position_x else layer.position_x
385 position_y = position_y if position_y else layer.position_y
387 # check they are equal for all layers
388 if layer.tile_width != tile_width:
389 raise SpriteLayerNotCompatibleError("layers do not have same tile_width")
390 if layer.tile_height != tile_height:
391 raise SpriteLayerNotCompatibleError("layers do not have same tile_height")
392 if layer.num_tiles_x != num_tiles_x:
393 raise SpriteLayerNotCompatibleError("layers do not have same number of tiles in x direction")
394 if layer.num_tiles_y != num_tiles_y:
395 raise SpriteLayerNotCompatibleError("layers do not have same number of tiles in y direction")
396 if layer.position_x != position_x:
397 raise SpriteLayerNotCompatibleError("layers are not at same position in x")
398 if layer.position_y != position_y:
399 raise SpriteLayerNotCompatibleError("layers are not at same position in y")
401 if new_layer is None:
402 new_layer = SpriteLayer(-2, layer._resource_loader)
404 for ypos_new in range(0, num_tiles_y):
405 for xpos_new in range(0, num_tiles_x):
406 sprite = layer.content2D[ypos_new][xpos_new]
407 if sprite:
408 new_sprite = new_layer.content2D[ypos_new][xpos_new]
409 if new_sprite:
410 assert sprite.rect.topleft == new_sprite.rect.topleft
411 assert sprite.rect.size == new_sprite.rect.size
412 new_sprite.image.blit(sprite.image, (0, 0), \
413 sprite.source_rect, sprite.flags)
414 else:
415 new_sprite = sprite
416 new_layer.content2D[ypos_new][xpos_new] = new_sprite
418 return new_layer
421 @staticmethod
422 def collapse(layer):
424 Makes 1 tile out of 4. The idea behind is that fewer tiles
425 are faster to render, but that is not always true.
426 Grouping them together into one bigger sprite is one way to get fewer
427 sprites.
429 :not: This only works for static layers without any dynamic sprites.
431 :note: use with caution
433 :Parameters:
434 laser : SpriteLayer
435 The layer to collapse
437 :returns: new SpriteLayer with fewer sprites but double the size.
441 # + 0' 1' 2'
442 # 0 1 2 3 4
443 # 0' 0 +----+----+----+----+
444 # | | | | |
445 # 1 +----+----+----+----+
446 # | | | | |
447 # 1' 2 +----+----+----+----+
448 # | | | | |
449 # 3 +----+----+----+----+
450 # | | | | |
451 # 2' 4 +----+----+----+----+
453 if layer.is_object_group:
454 return layer
455 level = 2
457 new_tilewidth = layer.tilewidth * level
458 new_tileheight = layer.tileheight * level
459 new_num_tiles_x = int(layer.num_tiles_x / level)
460 new_num_tiles_y = int(layer.num_tiles_y / level)
461 if new_num_tiles_x * level < layer.num_tiles_x:
462 new_num_tiles_x += 1
463 if new_num_tiles_y * level < layer.num_tiles_y:
464 new_num_tiles_y += 1
466 # print "old size", layer.num_tiles_x, layer.num_tiles_y
467 # print "new size", new_num_tiles_x, new_num_tiles_y
469 _content2D = [None] * new_num_tiles_y
470 # generate the needed lists
472 for ypos in range(new_num_tiles_y):
473 _content2D[ypos] = [None] * new_num_tiles_x
475 # fill them
476 _img_cache = {}
477 _img_cache["hits"] = 0
478 for ypos_new in range(0, new_num_tiles_y):
479 for xpos_new in range(0, new_num_tiles_x):
480 coords = SpriteLayer._get_list_of_neighbour_coord(\
481 xpos_new, ypos_new, level, \
482 layer.num_tiles_x, layer.num_tiles_y)
483 if coords:
484 sprite = SpriteLayer._get_sprite_from(coords, layer, \
485 _img_cache)
486 _content2D[ypos_new][xpos_new] = sprite
488 # print "len content2D:", len(self.content2D)
489 # TODO: separate constructor from init code (here the layer is parsed
490 # for nothing, content2D will be replaced)
491 new_layer = SpriteLayer( layer.layer_idx, layer._resource_loader)
493 new_layer.tilewidth = new_tilewidth
494 new_layer.tileheight = new_tileheight
495 new_layer.num_tiles_x = new_num_tiles_x
496 new_layer.num_tiles_y = new_num_tiles_y
497 new_layer.content2D = _content2D
499 # HACK:
500 new_layer._level = layer._level * 2
502 if __debug__ and level > 1:
503 print('%s: Sprite Cache hits: %d' % ("collapse", _img_cache["hits"]))
504 return new_layer
506 @staticmethod
507 def _get_list_of_neighbour_coord(xpos_new, ypos_new, level, \
508 num_tiles_x, num_tiles_y):
510 Finds the neighbours of a tile and returns them
512 :Parameters:
513 xpos_new : int
514 x position
515 ypos_new : int
516 y position
517 level : int
518 collapse level because this uses original tiles
519 num_tiles_x : int
520 number of tiles in x direction
521 num_tiles_y : int
522 number of tiles in y direction
523 :Returns:
524 list of coordinates of the neighbour tiles
526 xpos = xpos_new * level
527 ypos = ypos_new * level
529 coords = []
530 for y in range(ypos, ypos + level):
531 for x in range(xpos, xpos + level):
532 if x <= num_tiles_x and y <= num_tiles_y:
533 coords.append((x, y))
534 return coords
536 @staticmethod
537 def _union_sprites(sprites, key, _img_cache):
539 Unions sprites into one big one.
541 :Parameters:
542 sprites : list
543 list of sprites to union
544 key : iterable
545 key of the sprite, internal use only
546 _img_cache : dict
547 cache dict
548 :Returns:
549 new Sprite that unites all the given sprites.
551 key = tuple(key)
553 # dont copy to a new image if only one sprite is in sprites
554 # (reduce memory usage)
555 # NOTE: this messes up the cache hits (only on non-collapsed maps)
556 if len(sprites) == 1:
557 sprite = sprites[0]
558 sprite.key = key
559 return sprite
561 # combine found sprites into one sprite
562 rect = sprites[0].rect.unionall(sprites)
564 # cache the images to save memory
565 if key in _img_cache:
566 image = _img_cache[key]
567 _img_cache["hits"] = _img_cache["hits"] + 1
568 else:
569 # make new image
570 image = pygame.Surface(rect.size, pygame.SRCALPHA | pygame.RLEACCEL)
571 image.fill((0, 0, 0, 0))
572 x, y = rect.topleft
573 for spr in sprites:
574 image.blit(spr.image, spr.rect.move(-x, -y))
576 _img_cache[key] = image
578 return SpriteLayer.Sprite(image, rect, key=key)
580 @staticmethod
581 def _get_sprites_fromt_tiled_layer(coords, layer, indexed_tiles):
583 Get the sprites at the given coordinates from a tiled layer.
585 :Parameters:
586 coords : list
587 list of coordinates tuples
588 layer : TiledLayer
589 layer to extract the sprites from
590 indexed_tiles : dict
591 indexed tiles list loaded by the resource loader.
593 :Returns:
594 (keys, sprites) the new keys and sprites
597 sprites = []
598 key = []
599 for xpos, ypos in coords:
600 ## ISSUE 14: maps was displayed only sqared because wrong
601 ## boundary checks
602 if xpos >= len(layer.content2D) or \
603 ypos >= len(layer.content2D[xpos]):
604 # print "CONTINUE", xpos, ypos
605 key.append(-1) # border and corner cases!
606 continue
607 idx = layer.content2D[xpos][ypos]
608 if idx:
609 offx, offy, img = indexed_tiles[idx]
610 world_x = xpos * layer.tilewidth + offx
611 world_y = ypos * layer.tileheight + offy
612 w, h = img.get_size()
613 rect = pygame.Rect(world_x, world_y, w, h)
614 sprite = SpriteLayer.Sprite(img, rect, key=idx)
615 key.append(idx)
616 sprites.append(sprite)
617 else:
618 key.append(-1)
619 return key, sprites
621 @staticmethod
622 def _get_sprite_from(coords, layer, _img_cache):
624 Get one sprite for the given coordinates on the given layer.
626 :Parameters:
627 coords : list
628 tuples of coordinates (x, y)
629 layer : SpriteLayer
630 the layer to get the united sprite from
631 _img_cache : dict
632 dict for caching, internal use only
634 :returns:
635 a single sprite, uniting all given sprites on the fiven coordinates.
638 sprites = []
639 key = []
640 for xpos, ypos in coords:
641 if ypos >= len(layer.content2D) or \
642 xpos >= len(layer.content2D[ypos]):
643 # print "CONTINUE", xpos, ypos
644 key.append(-1) # border and corner cases!
645 continue
646 idx = layer.content2D[ypos][xpos]
647 if idx:
648 sprite = idx
649 key.append(sprite.key)
650 sprites.append(sprite)
651 else:
652 key.append(-1)
654 if sprites:
655 sprite = SpriteLayer._union_sprites(sprites, key, _img_cache)
657 if __debug__:
658 x, y = sprite.rect.topleft
659 pygame.draw.rect(sprite.image, (255, 0, 0), \
660 sprite.rect.move(-x, -y), \
661 layer.get_collapse_level())
663 del sprites
664 return sprite
666 return None
668 def add_sprite(self, sprite):
670 Add dynamic sprite to this layer.
672 :Parameters:
673 sprite : SpriteLayer.Sprite
674 sprite to add
676 self.sprites.append(sprite)
677 if sprite.rect.height > self.bottom_margin:
678 self.bottom_margin = sprite.rect.height
680 def add_sprites(self, sprites):
682 Add multiple dynamic sprites to this layer.
684 :Parameters:
685 sprites : list
686 list of SpriteLayer.Sprite to add
688 for sprite in sprites:
689 self.add_sprite(sprite)
691 def remove_sprite(self, sprite):
693 Removes a dynamic sprite from this layer.
695 :Parameters:
696 sprite : SpriteLayer.Sprite
697 sprite to remove
699 if sprite in self.sprites:
700 self.sprites.remove(sprite)
702 self.bottom_margin = self._bottom_margin
703 for spr in self.sprites:
704 if spr.rect.height > self.bottom_margin:
705 self.bottom_margin = spr.rect.height
707 def remove_sprites(self, sprites):
709 Remove multiple sprites at once.
711 :Parameters:
712 sprites : list
713 list of SpriteLayer.Sprite to remove
716 for sprite in sprites:
717 self.remove_sprite(sprite)
719 def contains_sprite(self, sprite):
721 Check if the given sprites is already in this layer.
723 :Parameters:
724 sprite : SpriteLayer.Sprite
725 sprite to check
727 :Returns:
728 bool, true if sprite is in this layer
730 if sprite in self.sprites:
731 return True
732 return False
734 def has_sprites(self):
736 Checks if this layer has dynamic sprites at all.
738 :Returns: bool, true if it contains at least 1 dynamic sprite.
740 return (len(self.sprites) > 0)
742 def set_layer_paralax_factor(self, factor_x=1.0, factor_y=None):
744 Set the paralax factor. This is for paralax scrolling this layer.
745 Values x < 0.0 will make the layer scroll in opposite direction
746 Value x == 0.0 makes the layer fix to the screen (wont scroll)
747 Values 0.0 < x < 1.0 will make scroll the layer slower.
748 Value x == 1.0 is default and make scroll the layer normal.
749 Values x > 1.0 make scroll the layer faster than normal
751 :Parameters:
752 factor_x : float
753 Paralax factor in x direction. Defaults to 1.0
754 factor_y : float
755 Paralax factor in y direction. If this is None then it will have
756 the same value as the factor_x argument.
758 self.paralax_factor_x = factor_x
759 if factor_y:
760 self.paralax_factor_y = factor_y
761 else:
762 self.paralax_factor_y = factor_x
764 def get_layer_paralax_factor_x(self):
766 Retrieve the current x paralax factor.
768 :Returns:
769 returns the current x paralax factor.
771 return self.paralax_factor_x
773 def get_layer_paralax_factor_y(self):
775 Retrieve the current y paralax factor.
777 :Returns:
778 returns the current y paralax factor.
780 return self.paralax_factor_y
782 # -----------------------------------------------------------------------------
784 def get_layers_from_map(resource_loader):
786 Creates SpriteLayers out of the map.
788 :Parameters:
789 resource_loader : ResourceLoaderPygame
790 a resource loader instance
792 :Returns: list of SpriteLayers
794 layers = []
795 for idx, layer in enumerate(resource_loader.world_map.layers):
796 layers.append(get_layer_at_index(idx, resource_loader))
797 return layers
799 def get_layer_at_index(layer_idx, resource_loader):
801 Creates one SpriteLayer from index out of the map.
803 :Parameters:
804 layer_idx : int
805 Index of the layer to create.
806 resource_loader : ResourceLoaderPygame
807 a resource loader instance
809 :Returns: a SpriteLayer instance
812 layer = resource_loader.world_map.layers[layer_idx]
813 if layer.is_object_group:
814 return layer
815 return SpriteLayer(layer_idx, resource_loader)
817 # -----------------------------------------------------------------------------
819 class RendererPygame(object):
821 A renderer for pygame. Should be fast enough for most purposes.
823 Example::
825 # init
826 sprite_layers = get_layers_from_map(resources)
827 renderer = RendererPygame()
829 # in main loop
830 while running:
832 # move camera
833 renderer.set_camera_position(x, y)
835 # draw layers
836 for sprite_layer in sprite_layers:
837 renderer.render_layer(screen, sprite_layer, clip_sprites)
841 def __init__(self):
843 Constructor.
846 self._cam_rect = pygame.Rect(0, 0, 10, 10)
847 self._margin = (0, 0, 0, 0) # left, right, top, bottom
849 def set_camera_position(self, world_pos_x, world_pos_y, alignment='center'):
851 Set the camera position in the world.
853 :Parameters:
854 world_pos_x : int
855 position in x in world coordinates
856 world_pos_y : int
857 position in y in world coordinates
858 alignment : string
859 defines to which part of the cam rect the position belongs,
860 can be any pygame.Rect
861 attribute: 'center', 'topleft', 'topright', ...
863 setattr(self._cam_rect, alignment, (world_pos_x, world_pos_y))
864 self.set_camera_margin(*self._margin)
866 def set_camera_position_and_size(self, world_pos_x, world_pos_y, \
867 width, height, alignment='center'):
869 Set the camera position and size in the world.
871 :Parameters:
872 world_pos_x : int
873 Position in x in world coordinates.
874 world_pos_y : int
875 Position in y in world coordinates.
876 witdh : int
877 With of the camera rect (the rendered area).
878 height : int
879 The height of the camera rect (the rendered area).
880 alignment : string
881 Defines to which part of the cam rect the position belongs,
882 can be any pygame.Rect
883 attribute: 'center', 'topleft', 'topright', ...
886 self._cam_rect.width = width
887 self._cam_rect.height = height
888 setattr(self._cam_rect, alignment, (world_pos_x, world_pos_y))
889 self.set_camera_margin(*self._margin)
891 def set_camera_rect(self, cam_rect_world_coord):
893 Set the camera position and size using a rect in world coordinates.
895 :Parameters:
896 cam_rect_world_coord : pygame.Rect
897 A rect describing the cameras position and size in the world.
900 self._cam_rect = cam_rect_world_coord
901 self.set_camera_margin(*self._margin)
903 def set_camera_margin(self, margin_left, margin_right, margin_top, margin_bottom):
905 Set the margin around the camera (in pixels).
907 :Parameters:
908 margin_left : int
909 number of pixels of the left side marging
910 margin_right : int
911 number of pixels of the right side marging
912 margin_top : int
913 number of pixels of the top side marging
914 margin_bottom : int
915 number of pixels of the left bottom marging
918 self._margin = (margin_left, margin_right, margin_top, margin_bottom)
919 self._render_cam_rect = pygame.Rect(self._cam_rect)
920 # adjust left margin
921 self._render_cam_rect.left = self._render_cam_rect.left - margin_left
922 # adjust right margin
923 self._render_cam_rect.width = self._render_cam_rect.width + \
924 margin_left + margin_right
925 # adjust top margin
926 self._render_cam_rect.top = self._render_cam_rect.top - margin_top
927 # adjust bottom margin
928 self._render_cam_rect.height = self._render_cam_rect.height + \
929 margin_top + margin_bottom
930 self._render_cam_rect.left = self._cam_rect.left - margin_left
931 self._render_cam_rect.top = self._cam_rect.top - margin_top
933 def render_layer(self, surf, layer, clip_sprites=True, \
934 sort_key=lambda spr: spr.get_draw_cond()):
936 Renders a layer onto the given surface.
938 :Parameters:
939 surf : Surface
940 Surface to render onto.
941 layer : SpriteLayer
942 The layer to render. Invisible layers will be skipped.
943 clip_sprites : boolean
944 Optional, defaults to True. Clip the sprites of this layer to
945 only draw the ones intersecting the visible part of the world.
946 sort_key : function
947 Optional: The sort function for the parameter 'key' of the sort
948 method of the list.
951 if layer.visible:
953 if layer.is_object_group:
954 return
956 if layer.bottom_margin > self._margin[3]:
957 left, right, top, bottom = self._margin
958 self.set_camera_margin(left, right, top, layer.bottom_margin)
960 # optimizations
961 surf_blit = surf.blit
962 layer_content2D = layer.content2D
964 tile_h = layer.tileheight
966 cam_rect = self._render_cam_rect
968 cam_world_pos_x = cam_rect.left * layer.paralax_factor_x + \
969 layer.position_x
970 cam_world_pos_y = cam_rect.top * layer.paralax_factor_y + \
971 layer.position_y
973 # camera bounds, restricting number of tiles to draw
974 left = int(round(float(cam_world_pos_x) // layer.tilewidth))
975 right = int(round(float(cam_world_pos_x + cam_rect.width) // \
976 layer.tilewidth)) + 1
977 top = int(round(float(cam_world_pos_y) // tile_h))
978 bottom = int(round(float(cam_world_pos_y + cam_rect.height) // \
979 tile_h)) + 1
981 left = left if left > 0 else 0
982 right = right if right < layer.num_tiles_x else layer.num_tiles_x
983 top = top if top > 0 else 0
984 bottom = bottom if bottom < layer.num_tiles_y else layer.num_tiles_y
986 # sprites
987 spr_idx = 0
988 len_sprites = 0
989 all_sprites = layer.sprites
990 if all_sprites:
991 # TODO: make filter visible sprites optional (maybe sorting too)
992 # use a marging around it
993 if clip_sprites:
994 sprites = [all_sprites[idx] \
995 for idx in cam_rect.collidelistall(all_sprites)]
996 else:
997 sprites = all_sprites
999 # could happend that all sprites are not visible by the camera
1000 if sprites:
1001 if sort_key:
1002 sprites.sort(key=sort_key)
1003 sprite = sprites[0]
1004 len_sprites = len(sprites)
1007 # render
1008 for ypos in range(top, bottom):
1009 # draw sprites in this layer
1010 # (skip the ones outside visible area/map)
1011 y = ypos + 1
1012 while spr_idx < len_sprites and sprite.get_draw_cond() <= \
1013 y * tile_h:
1014 surf_blit(sprite.image, \
1015 sprite.rect.move(-cam_world_pos_x, \
1016 -cam_world_pos_y - sprite.z),\
1017 sprite.source_rect, \
1018 sprite.flags)
1019 spr_idx += 1
1020 if spr_idx < len_sprites:
1021 sprite = sprites[spr_idx]
1022 # next line of the map
1023 for xpos in range(left, right):
1024 tile_sprite = layer_content2D[ypos][xpos]
1025 # print '?', xpos, ypos, tile_sprite
1026 if tile_sprite:
1027 surf_blit(tile_sprite.image, \
1028 tile_sprite.rect.move( -cam_world_pos_x, \
1029 -cam_world_pos_y), \
1030 tile_sprite.source_rect, \
1031 tile_sprite.flags)
1033 def pick_layer(self, layer, screen_x, screen_y):
1035 Returns the sprite at the given screen position or None regardless of
1036 the layers visibility.
1038 :Note: This does not work wir object group layers.
1040 :Parameters:
1041 layer : SpriteLayer
1042 the layer to pick from
1043 screen_x : int
1044 The screen position in x direction.
1045 screen_y : int
1046 The screen position in y direction.
1048 :Returns:
1049 None if there is no sprite or the sprite
1050 (SpriteLayer.Sprite instance).
1052 if layer.is_object_group:
1053 pass
1054 else:
1055 world_pos_x, world_pos_y = \
1056 self.screen_to_world(layer, screen_x, screen_y)
1058 tile_x = int(world_pos_x / layer.tilewidth)
1059 tile_y = int(world_pos_y / layer.tileheight)
1061 if 0 <= tile_x < layer.num_tiles_x and \
1062 0 <= tile_y < layer.num_tiles_y:
1063 sprite = layer.content2D[tile_y][tile_x]
1064 if sprite:
1065 return sprite
1066 return None
1068 def pick_layers_sprites(self, layer, screen_x, screen_y):
1070 Returns the sprites at the given screen positions or an empty list.
1071 The sprites are the same order as in the layers.sprites list.
1073 :Note: This does not work wir object group layers.
1075 :Parameters:
1076 layer : SpriteLayer
1077 the layer to pick from
1078 screen_x : int
1079 The screen position in x direction.
1080 screen_y : int
1081 The screen position in y direction.
1083 :Returns:
1084 A list of sprites or an empty list.
1086 if layer.is_object_group:
1087 pass
1088 else:
1089 world_pos_x, world_pos_y = \
1090 self.screen_to_world(layer, screen_x, screen_y)
1092 r = pygame.Rect(world_pos_x, world_pos_y, 1, 1)
1093 indices = r.collidelistall(layer.sprites)
1094 return [layer.sprites[idx] for idx in indices]
1095 return []
1097 def screen_to_world(self, layer, screen_x, screen_y):
1099 Returns the world coordinates for the given screen location and layer.
1101 :Note:
1102 this is important so one can check which entity is there in the
1103 model (knowing which sprite is there does not help much)
1105 :Parameters:
1106 layer : SpriteLayer
1107 the layer to pick from
1108 screen_x : int
1109 The screen position in x direction.
1110 screen_y : int
1111 The screen position in y direction.
1113 :Returns:
1114 Tuple of world coordinates: (world_x, world_y)
1117 # TODO: also use layer.x and layer.y offset
1118 return (screen_x + self._render_cam_rect.x * layer.paralax_factor_x, \
1119 screen_y + self._render_cam_rect.y * layer.paralax_factor_y)
1121 # -----------------------------------------------------------------------------
1123 class IsometricRendererPygame(RendererPygame):
1125 Isometric renderer.
1127 :Warning: !!EXPERIMENTAL!!
1131 def render_layer(self, surf, layer, clip_sprites=True, \
1132 sort_key=lambda spr: spr.get_draw_cond()):
1134 Renders a layer onto the given surface.
1136 :Parameters:
1137 surf : Surface
1138 Surface to render onto.
1139 layer : SpriteLayer
1140 The layer to render. Invisible layers will be skipped.
1141 clip_sprites : boolean
1142 Optional, defaults to True. Clip the sprites of this layer to
1143 only draw the ones intersecting the visible part of the world.
1144 sort_key : function
1145 Optional: The sort function for the parameter 'key' of the sort
1146 method of the list.
1149 if layer.visible:
1151 if layer.is_object_group:
1152 return
1154 if layer.bottom_margin > self._margin[3]:
1155 left, right, top, bottom = self._margin
1156 self.set_camera_margin(left, right, top, layer.bottom_margin)
1158 # optimizations
1159 surf_blit = surf.blit
1160 layer_content2D = layer.content2D
1162 tile_h = layer.tileheight
1164 # self.paralax_factor_y = 1.0
1165 # self.paralax_center_x = 0.0
1166 cam_rect = self._render_cam_rect
1167 # print 'cam rect:', self._cam_rect
1168 # print 'render r:', self._render_cam_rect
1170 cam_world_pos_x = cam_rect.centerx * layer.paralax_factor_x + \
1171 layer.position_x
1172 cam_world_pos_y = cam_rect.centery * layer.paralax_factor_y + \
1173 layer.position_y
1175 # cam_world_pos_x, cam_world_pos_y = 0, 0
1176 cam_world_pos_x, cam_world_pos_y = self.world_to_screen(layer, cam_world_pos_x / layer.tilewidth, cam_world_pos_y / layer.tileheight, surf.get_size(), cam_world_pos_x, cam_world_pos_y)
1177 cam_world_pos_x -= surf.get_size()[0] // 2
1178 cam_world_pos_y -= surf.get_size()[1] // 2
1180 # cam_world_pos_x -= cam_rect.width // 2
1181 # cam_world_pos_y -= cam_rect.height // 2
1182 # print("0,0", self.world_to_screen(layer, 0, 0))
1183 # cam_world_pos_x = 0
1184 # cam_world_pos_y = 0
1185 print("cam pos:", cam_world_pos_x, cam_world_pos_y, cam_rect, cam_rect.center)
1187 # camera bounds, restricting number of tiles to draw
1188 # left = int(round(float(cam_world_pos_x) // layer.tilewidth))
1189 # right = int(round(float(cam_world_pos_x + cam_rect.width) // \
1190 # layer.tilewidth)) + 1
1191 # top = int(round(float(cam_world_pos_y) // tile_h))
1192 # bottom = int(round(float(cam_world_pos_y + cam_rect.height) // \
1193 # tile_h)) + 1
1195 # left = left if left > 0 else 0
1196 # right = right if right < layer.num_tiles_x else layer.num_tiles_x
1197 # top = top if top > 0 else 0
1198 # bottom = bottom if bottom < layer.num_tiles_y else layer.num_tiles_y
1200 left = 0
1201 right = layer.num_tiles_x
1202 top = 0
1203 bottom = layer.num_tiles_y
1205 # sprites
1206 spr_idx = 0
1207 len_sprites = 0
1208 all_sprites = layer.sprites
1209 if all_sprites:
1210 # TODO: make filter visible sprites optional (maybe sorting too)
1211 # use a marging around it
1212 if clip_sprites:
1213 sprites = [all_sprites[idx] \
1214 for idx in cam_rect.collidelistall(all_sprites)]
1215 else:
1216 sprites = all_sprites
1218 # could happend that all sprites are not visible by the camera
1219 if sprites:
1220 if sort_key:
1221 sprites.sort(key=sort_key)
1222 sprite = sprites[0]
1223 len_sprites = len(sprites)
1225 half_tile_width = layer.tilewidth // 2
1226 half_tile_height = tile_h // 2
1228 # render
1229 for ypos in range(top, bottom):
1230 # draw sprites in this layer
1231 # (skip the ones outside visible area/map)
1232 y = ypos + 1
1233 while spr_idx < len_sprites and sprite.get_draw_cond() <= \
1234 y * tile_h:
1235 # surf_blit(sprite.image, \
1236 # ( sprite.rect.left // 2 - ypos * half_tile_width - cam_world_pos_x, \
1237 # sprite.rect.top // 2 + sprite.rect.left // half_tile_width * half_tile_height - cam_world_pos_y), \
1238 # # sprite.rect.top // 2 + sprite.rect.left // half_tile_width * half_tile_height - cam_world_pos_y), \
1239 # sprite.source_rect, \
1240 # sprite.flags)
1241 sx, sy = self.world_to_screen(layer, 1.0 * sprite.rect.left / layer.tilewidth, \
1242 # 1.0 * (sprite.rect.top - sprite.z) / layer.tileheight, surf.get_size(), cam_world_pos_x, cam_world_pos_y)
1243 1.0 * (sprite.rect.bottom) / layer.tileheight, surf.get_size(), cam_world_pos_x, cam_world_pos_y)
1244 print("hero: ", sx, sy, sprite.rect, sprite.z)
1245 surf_blit(sprite.image, \
1246 # (sx, sy), \
1247 (sx - cam_world_pos_x, \
1248 sy - cam_world_pos_y - sprite.z - sprite.rect.height), \
1249 sprite.source_rect, \
1250 sprite.flags)
1251 spr_idx += 1
1252 if spr_idx < len_sprites:
1253 sprite = sprites[spr_idx]
1254 # next line of the map
1255 for xpos in range(left, right):
1256 tile_sprite = layer_content2D[ypos][xpos]
1257 # print '?', xpos, ypos, tile_sprite
1258 if tile_sprite:
1259 # surf_blit(tile_sprite.image, \
1260 # ( tile_sprite.rect.left // 2 - ypos * half_tile_width - cam_world_pos_x, \
1261 # tile_sprite.rect.top // 2 + xpos * half_tile_height - cam_world_pos_y), \
1262 # tile_sprite.source_rect, \
1263 # tile_sprite.flags)
1264 sx, sy = self.world_to_screen(layer, xpos, ypos, surf.get_size(), cam_world_pos_x, cam_world_pos_y)
1265 # sx, sy = self.world_to_screen(layer, tile_sprite.rect.left, tile_sprite.rect.top)
1266 surf_blit(tile_sprite.image, \
1267 ( sx - cam_world_pos_x, \
1268 sy - cam_world_pos_y), \
1269 tile_sprite.source_rect, \
1270 tile_sprite.flags)
1271 pygame.draw.line(surf, (255, 255, 0), (surf.get_size()[0] // 2, 0), (surf.get_size()[0] // 2, surf.get_size()[1]), 1)
1272 pygame.draw.line(surf, (255, 255, 0), (0, surf.get_size()[1] // 2), (surf.get_size()[0], surf.get_size()[1] // 2), 1)
1274 pygame.draw.line(surf, (255, 0, 0), (self._render_cam_rect.centerx // 2, 0), (self._render_cam_rect.centerx // 2, surf.get_size()[1]), 1)
1275 pygame.draw.line(surf, (255, 0, 0), (0, self._render_cam_rect.centery // 2), (surf.get_size()[0], self._render_cam_rect.centery // 2), 1)
1277 def pick_layer(self, layer, screen_x, screen_y):
1279 Returns the sprite at the given screen position or None regardless of
1280 the layers visibility.
1282 :Note: This does not work wir object group layers.
1284 :Parameters:
1285 layer : SpriteLayer
1286 the layer to pick from
1287 screen_x : int
1288 The screen position in x direction.
1289 screen_y : int
1290 The screen position in y direction.
1292 :Returns:
1293 None if there is no sprite or the sprite
1294 (SpriteLayer.Sprite instance).
1296 if layer.is_object_group:
1297 pass
1298 else:
1299 world_pos_x, world_pos_y = \
1300 self.screen_to_world(layer, screen_x, screen_y)
1302 tile_x = int(world_pos_x / layer.tilewidth)
1303 tile_y = int(world_pos_y / layer.tileheight)
1305 if 0 <= tile_x < layer.num_tiles_x and \
1306 0 <= tile_y < layer.num_tiles_y:
1307 sprite = layer.content2D[tile_y][tile_x]
1308 if sprite:
1309 return sprite
1310 return None
1312 def pick_layers_sprites(self, layer, screen_x, screen_y):
1314 Returns the sprites at the given screen positions or an empty list.
1315 The sprites are the same order as in the layers.sprites list.
1317 :Note: This does not work wir object group layers.
1319 :Parameters:
1320 layer : SpriteLayer
1321 the layer to pick from
1322 screen_x : int
1323 The screen position in x direction.
1324 screen_y : int
1325 The screen position in y direction.
1327 :Returns:
1328 A list of sprites or an empty list.
1330 if layer.is_object_group:
1331 pass
1332 else:
1333 world_pos_x, world_pos_y = \
1334 self.screen_to_world(layer, screen_x, screen_y)
1336 r = pygame.Rect(world_pos_x, world_pos_y, 1, 1)
1337 indices = r.collidelistall(layer.sprites)
1338 return [layer.sprites[idx] for idx in indices]
1339 return []
1341 def screen_to_world(self, layer, screen_x, screen_y):
1343 Returns the world coordinates for the given screen location and layer.
1345 :Note:
1346 this is important so one can check which entity is there in the
1347 model (knowing which sprite is there does not help much)
1349 :Parameters:
1350 layer : SpriteLayer
1351 the layer to pick from
1352 screen_x : int
1353 The screen position in x direction.
1354 screen_y : int
1355 The screen position in y direction.
1357 :Returns:
1358 Tuple of world coordinates: (world_x, world_y)
1361 # TODO: also use layer.x and layer.y offset
1362 return (screen_x + self._render_cam_rect.x * layer.paralax_factor_x, \
1363 screen_y + self._render_cam_rect.y * layer.paralax_factor_y)
1365 def world_to_screen(self, layer, world_x, world_y, screen_size, cam_w_pos_x, cam_w_pos_y):
1367 TODO:
1369 origin_x = 0 * layer.tileheight * layer.tilewidth // 2
1370 # origin_x -= layer.tilewidth // 2
1371 # print("world -> screen", world_x, world_y, ( (world_x - world_y) * layer.tilewidth / 2.0 + origin_x, \
1372 # (world_x + world_y) * layer.tileheight / 2.0))
1373 return ( (world_x - world_y) * layer.tilewidth / 2.0 + origin_x, \
1374 (world_x + world_y) * layer.tileheight / 2.0)