1 # -*- coding: utf-8 -*-
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.
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
20 # major.minor[.build[.revision]]
22 # +-|* 0 for alpha (status)
23 # |* 1 for beta (status)
24 # |* 2 for release candidate
25 # |* 3 for (public) release
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 # -----------------------------------------------------------------------------
46 from . import tmxreader
48 # -----------------------------------------------------------------------------
50 # -----------------------------------------------------------------------------
52 class ResourceLoaderPygame(tmxreader
.AbstractResourceLoader
):
54 Resource loader for pygame. Loads the images as pygame.Surfaces and saves
55 them in the variable indexed_tiles.
60 res_loader = ResourceLoaderPygame()
61 # tile_map loaded the the TileMapParser.parse() method
62 res_loader.load(tile_map)
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
]
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()
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
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
)
105 def _load_image_part(self
, filename
, xpos
, ypos
, width
, height
, \
108 Loads a image from a sprite sheet.
110 source_img
= self
._load
_image
(filename
, colorkey
)
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
)
120 ## Set the colorkey BEFORE we blit the source_img
122 img_part
.set_colorkey(colorkey
, pygame
.RLEACCEL
)
123 img_part
.fill(colorkey
)
125 img_part
.blit(source_img
, (0, 0), source_rect
)
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)
137 img
= pygame
.image
.load(filename
)
138 self
._img
_cache
[filename
] = img
140 img
.set_colorkey(colorkey
, pygame
.RLEACCEL
)
143 # def get_sprites(self):
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):
168 image : pygame.Surface
169 the image of this sprite
171 the rect used when drawing
172 source_rect : pygame.Rect
173 source area rect, defaults to None
175 flags for the blit method, defaults to 0
177 used internally for collapsing sprites
181 # TODO: dont use a rect for position
182 self
.rect
= rect
# blit rect
183 self
.source_rect
= source_rect
189 def get_draw_cond(self
):
191 Defines if the sprite lays on the floor or if it is up-right.
194 The bottom y coordinate so the sprites can be sorted in right
198 return self
.rect
.top
+ self
.z
200 return self
.rect
.bottom
202 def __init__(self
, tile_layer_idx
, resource_loader
):
207 Index of the tile layer to build upon
208 resource_loader : ResourceLoaderPygame
209 Instance of the ResourceLoaderPygame class which has loaded
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
227 # TODO: change scale attributes to properties?
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
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
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
)
261 key
, sprites
= SpriteLayer
._get
_sprites
_fromt
_tiled
_layer
(\
262 coords
, _layer
, self
._resource
_loader
.indexed_tiles
)
266 sprite
= SpriteLayer
._union
_sprites
(sprites
, key
, \
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
274 print('%s: Sprite Cache hits: %d' % \
275 (self
.__class
__.__name
__, _img_cache
["hits"]))
278 def get_collapse_level(self
):
280 The level of collapsing.
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
297 Width scale factor in range (0, ...]
299 Height scale factor in range (0, ...]
301 if layer_orig
.is_object_group
:
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
):
327 w
, h
= sprite
.image
.get_size()
332 # prevent fractional numbers and scaling glitches
333 if w
!= ceil(new_w
) or h
!= ceil(new_h
):
336 image
= pygame
.transform
.smoothscale(sprite
.image
, \
338 x
, y
= sprite
.rect
.topleft
339 rect
= pygame
.Rect(x
* scale_w
, y
* scale_h
, \
342 layer
.content2D
[yidx
][xidx
] = \
343 SpriteLayer
.Sprite(image
, rect
)
345 layer
.content2D
[yidx
][xidx
] = None
349 # TODO: implement merge
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.
359 The SpriteLayer to be merged
361 :returns: new SpriteLayer with merged tiles
373 if layer
.is_object_group
:
374 # skip object group layers
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
]
408 new_sprite
= new_layer
.content2D
[ypos_new
][xpos_new
]
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
)
416 new_layer
.content2D
[ypos_new
][xpos_new
] = new_sprite
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
429 :not: This only works for static layers without any dynamic sprites.
431 :note: use with caution
435 The layer to collapse
437 :returns: new SpriteLayer with fewer sprites but double the size.
443 # 0' 0 +----+----+----+----+
445 # 1 +----+----+----+----+
447 # 1' 2 +----+----+----+----+
449 # 3 +----+----+----+----+
451 # 2' 4 +----+----+----+----+
453 if layer
.is_object_group
:
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
:
463 if new_num_tiles_y
* level
< layer
.num_tiles_y
:
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
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
)
484 sprite
= SpriteLayer
._get
_sprite
_from
(coords
, layer
, \
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
500 new_layer
._level
= layer
._level
* 2
502 if __debug__
and level
> 1:
503 print('%s: Sprite Cache hits: %d' % ("collapse", _img_cache
["hits"]))
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
518 collapse level because this uses original tiles
520 number of tiles in x direction
522 number of tiles in y direction
524 list of coordinates of the neighbour tiles
526 xpos
= xpos_new
* level
527 ypos
= ypos_new
* level
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
))
537 def _union_sprites(sprites
, key
, _img_cache
):
539 Unions sprites into one big one.
543 list of sprites to union
545 key of the sprite, internal use only
549 new Sprite that unites all the given sprites.
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:
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
570 image
= pygame
.Surface(rect
.size
, pygame
.SRCALPHA | pygame
.RLEACCEL
)
571 image
.fill((0, 0, 0, 0))
574 image
.blit(spr
.image
, spr
.rect
.move(-x
, -y
))
576 _img_cache
[key
] = image
578 return SpriteLayer
.Sprite(image
, rect
, key
=key
)
581 def _get_sprites_fromt_tiled_layer(coords
, layer
, indexed_tiles
):
583 Get the sprites at the given coordinates from a tiled layer.
587 list of coordinates tuples
589 layer to extract the sprites from
591 indexed tiles list loaded by the resource loader.
594 (keys, sprites) the new keys and sprites
599 for xpos
, ypos
in coords
:
600 ## ISSUE 14: maps was displayed only sqared because wrong
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!
607 idx
= layer
.content2D
[xpos
][ypos
]
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
)
616 sprites
.append(sprite
)
622 def _get_sprite_from(coords
, layer
, _img_cache
):
624 Get one sprite for the given coordinates on the given layer.
628 tuples of coordinates (x, y)
630 the layer to get the united sprite from
632 dict for caching, internal use only
635 a single sprite, uniting all given sprites on the fiven coordinates.
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!
646 idx
= layer
.content2D
[ypos
][xpos
]
649 key
.append(sprite
.key
)
650 sprites
.append(sprite
)
655 sprite
= SpriteLayer
._union
_sprites
(sprites
, key
, _img_cache
)
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())
668 def add_sprite(self
, sprite
):
670 Add dynamic sprite to this layer.
673 sprite : SpriteLayer.Sprite
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.
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.
696 sprite : SpriteLayer.Sprite
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.
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.
724 sprite : SpriteLayer.Sprite
728 bool, true if sprite is in this layer
730 if sprite
in self
.sprites
:
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
753 Paralax factor in x direction. Defaults to 1.0
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
760 self
.paralax_factor_y
= factor_y
762 self
.paralax_factor_y
= factor_x
764 def get_layer_paralax_factor_x(self
):
766 Retrieve the current x paralax factor.
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.
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.
789 resource_loader : ResourceLoaderPygame
790 a resource loader instance
792 :Returns: list of SpriteLayers
795 for idx
, layer
in enumerate(resource_loader
.world_map
.layers
):
796 layers
.append(get_layer_at_index(idx
, resource_loader
))
799 def get_layer_at_index(layer_idx
, resource_loader
):
801 Creates one SpriteLayer from index out of the map.
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
:
815 return SpriteLayer(layer_idx
, resource_loader
)
817 # -----------------------------------------------------------------------------
819 class RendererPygame(object):
821 A renderer for pygame. Should be fast enough for most purposes.
826 sprite_layers = get_layers_from_map(resources)
827 renderer = RendererPygame()
833 renderer.set_camera_position(x, y)
836 for sprite_layer in sprite_layers:
837 renderer.render_layer(screen, sprite_layer, clip_sprites)
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.
855 position in x in world coordinates
857 position in y in world coordinates
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.
873 Position in x in world coordinates.
875 Position in y in world coordinates.
877 With of the camera rect (the rendered area).
879 The height of the camera rect (the rendered area).
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.
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).
909 number of pixels of the left side marging
911 number of pixels of the right side marging
913 number of pixels of the top side marging
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
)
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
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.
940 Surface to render onto.
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.
947 Optional: The sort function for the parameter 'key' of the sort
953 if layer
.is_object_group
:
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
)
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
+ \
970 cam_world_pos_y
= cam_rect
.top
* layer
.paralax_factor_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
) // \
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
989 all_sprites
= layer
.sprites
991 # TODO: make filter visible sprites optional (maybe sorting too)
992 # use a marging around it
994 sprites
= [all_sprites
[idx
] \
995 for idx
in cam_rect
.collidelistall(all_sprites
)]
997 sprites
= all_sprites
999 # could happend that all sprites are not visible by the camera
1002 sprites
.sort(key
=sort_key
)
1004 len_sprites
= len(sprites
)
1008 for ypos
in range(top
, bottom
):
1009 # draw sprites in this layer
1010 # (skip the ones outside visible area/map)
1012 while spr_idx
< len_sprites
and sprite
.get_draw_cond() <= \
1014 surf_blit(sprite
.image
, \
1015 sprite
.rect
.move(-cam_world_pos_x
, \
1016 -cam_world_pos_y
- sprite
.z
),\
1017 sprite
.source_rect
, \
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
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
, \
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.
1042 the layer to pick from
1044 The screen position in x direction.
1046 The screen position in y direction.
1049 None if there is no sprite or the sprite
1050 (SpriteLayer.Sprite instance).
1052 if layer
.is_object_group
:
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
]
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.
1077 the layer to pick from
1079 The screen position in x direction.
1081 The screen position in y direction.
1084 A list of sprites or an empty list.
1086 if layer
.is_object_group
:
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
]
1097 def screen_to_world(self
, layer
, screen_x
, screen_y
):
1099 Returns the world coordinates for the given screen location and layer.
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)
1107 the layer to pick from
1109 The screen position in x direction.
1111 The screen position in y direction.
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
):
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.
1138 Surface to render onto.
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.
1145 Optional: The sort function for the parameter 'key' of the sort
1151 if layer
.is_object_group
:
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
)
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
+ \
1172 cam_world_pos_y
= cam_rect
.centery
* layer
.paralax_factor_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) // \
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
1201 right
= layer
.num_tiles_x
1203 bottom
= layer
.num_tiles_y
1208 all_sprites
= layer
.sprites
1210 # TODO: make filter visible sprites optional (maybe sorting too)
1211 # use a marging around it
1213 sprites
= [all_sprites
[idx
] \
1214 for idx
in cam_rect
.collidelistall(all_sprites
)]
1216 sprites
= all_sprites
1218 # could happend that all sprites are not visible by the camera
1221 sprites
.sort(key
=sort_key
)
1223 len_sprites
= len(sprites
)
1225 half_tile_width
= layer
.tilewidth
// 2
1226 half_tile_height
= tile_h
// 2
1229 for ypos
in range(top
, bottom
):
1230 # draw sprites in this layer
1231 # (skip the ones outside visible area/map)
1233 while spr_idx
< len_sprites
and sprite
.get_draw_cond() <= \
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, \
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
, \
1247 (sx
- cam_world_pos_x
, \
1248 sy
- cam_world_pos_y
- sprite
.z
- sprite
.rect
.height
), \
1249 sprite
.source_rect
, \
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
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
, \
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.
1286 the layer to pick from
1288 The screen position in x direction.
1290 The screen position in y direction.
1293 None if there is no sprite or the sprite
1294 (SpriteLayer.Sprite instance).
1296 if layer
.is_object_group
:
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
]
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.
1321 the layer to pick from
1323 The screen position in x direction.
1325 The screen position in y direction.
1328 A list of sprites or an empty list.
1330 if layer
.is_object_group
:
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
]
1341 def screen_to_world(self
, layer
, screen_x
, screen_y
):
1343 Returns the world coordinates for the given screen location and layer.
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)
1351 the layer to pick from
1353 The screen position in x direction.
1355 The screen position in y direction.
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
):
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)