data/tmx.py (view raw)
1# "Tiled" TMX loader/renderer and more
2# Copyright 2012 Richard Jones <richard@mechanicalcat.net>
3# This code is placed in the Public Domain.
4
5# TODO: support properties on more things
6
7import sys
8import struct
9import pygame
10from pygame.locals import *
11from pygame import Rect
12from xml.etree import ElementTree
13
14
15class Tile(object):
16 def __init__(self, gid, surface, tileset):
17 self.gid = gid
18 self.surface = surface
19 self.tile_width = tileset.tile_width
20 self.tile_height = tileset.tile_height
21 self.properties = {}
22
23 @classmethod
24 def fromSurface(cls, surface):
25 '''Create a new Tile object straight from a pygame Surface.
26
27 Its tile_width and tile_height will be set using the Surface dimensions.
28 Its gid will be 0.
29 '''
30 class ts:
31 tile_width, tile_height = surface.get_size()
32 return cls(0, surface, ts)
33
34 def loadxml(self, tag):
35 props = tag.find('properties')
36 if props is None:
37 return
38 for c in props.findall('property'):
39 # store additional properties.
40 name = c.attrib['name']
41 value = c.attrib['value']
42
43 # TODO hax
44 if value.isdigit():
45 value = int(value)
46 self.properties[name] = value
47
48 def __repr__(self):
49 return '<Tile %d>' % self.gid
50
51
52class Tileset(object):
53 def __init__(self, name, tile_width, tile_height, firstgid):
54 self.name = name
55 self.tile_width = tile_width
56 self.tile_height = tile_height
57 self.firstgid = firstgid
58 self.tiles = []
59 self.properties = {}
60
61 @classmethod
62 def fromxml(cls, tag, firstgid=None):
63 if 'source' in tag.attrib:
64 firstgid = int(tag.attrib['firstgid'])
65 with open(tag.attrib['source']) as f:
66 tileset = ElementTree.fromstring(f.read())
67 return cls.fromxml(tileset, firstgid)
68
69 name = tag.attrib['name']
70 if firstgid is None:
71 firstgid = int(tag.attrib['firstgid'])
72 tile_width = int(tag.attrib['tilewidth'])
73 tile_height = int(tag.attrib['tileheight'])
74
75 tileset = cls(name, tile_width, tile_height, firstgid)
76
77 for c in tag.getchildren():
78 if c.tag == "image":
79 # create a tileset
80 tileset.add_image(c.attrib['source'])
81 elif c.tag == 'tile':
82 gid = tileset.firstgid + int(c.attrib['id'])
83 tileset.get_tile(gid).loadxml(c)
84 return tileset
85
86 def add_image(self, file):
87 image = pygame.image.load(file).convert_alpha()
88 if not image:
89 sys.exit("Error creating new Tileset: file %s not found" % file)
90 id = self.firstgid
91 for line in xrange(image.get_height() / self.tile_height):
92 for column in xrange(image.get_width() / self.tile_width):
93 pos = Rect(column * self.tile_width, line * self.tile_height,
94 self.tile_width, self.tile_height)
95 self.tiles.append(Tile(id, image.subsurface(pos), self))
96 id += 1
97
98 def get_tile(self, gid):
99 return self.tiles[gid - self.firstgid]
100
101
102class Tilesets(dict):
103 def add(self, tileset):
104 for i, tile in enumerate(tileset.tiles):
105 i += tileset.firstgid
106 self[i] = tile
107
108
109class Cell(object):
110 '''Layers are made of Cells (or empty space).
111
112 Cells have some basic properties:
113
114 x, y - the cell's index in the layer
115 px, py - the cell's pixel position
116 left, right, top, bottom - the cell's pixel boundaries
117
118 Additionally the cell may have other properties which are accessed using
119 standard dictionary methods:
120
121 cell['property name']
122
123 You may assign a new value for a property to or even delete an existing
124 property from the cell - this will not affect the Tile or any other Cells
125 using the Cell's Tile.
126 '''
127 def __init__(self, x, y, px, py, tile):
128 self.x, self.y = x, y
129 self.px, self.py = px, py
130 self.tile = tile
131 self.topleft = (px, py)
132 self.left = px
133 self.right = px + tile.tile_width
134 self.top = py
135 self.bottom = py + tile.tile_height
136 self.center = (px + tile.tile_width // 2, py + tile.tile_height // 2)
137 self._added_properties = {}
138 self._deleted_properties = set()
139
140 def __repr__(self):
141 return '<Cell %s,%s %d>' % (self.px, self.py, self.tile.gid)
142
143 def __contains__(self, key):
144 if key in self._deleted_properties:
145 return False
146 return key in self._added_properties or key in self.tile.properties
147
148 def __getitem__(self, key):
149 if key in self._deleted_properties:
150 raise KeyError(key)
151 if key in self._added_properties:
152 return self._added_properties[key]
153 if key in self.tile.properties:
154 return self.tile.properties[key]
155 raise KeyError(key)
156
157 def __setitem__(self, key, value):
158 self._added_properties[key] = value
159
160 def __delitem__(self, key):
161 self._deleted_properties.add(key)
162
163 def intersects(self, other):
164 '''Determine whether this Cell intersects with the other rect (which has
165 .x, .y, .width and .height attributes.)
166 '''
167 if self.px + self.tile.tile_width < other.x:
168 return False
169 if other.x + other.width < self.px:
170 return False
171 if self.py + self.tile.tile_height < other.y:
172 return False
173 if other.y + other.height < self.py:
174 return False
175 return True
176
177
178class LayerIterator(object):
179 '''Iterates over all the cells in a layer in column,row order.
180 '''
181 def __init__(self, layer):
182 self.layer = layer
183 self.i, self.j = 0, 0
184
185 def next(self):
186 if self.i == self.layer.width - 1:
187 self.j += 1
188 self.i = 0
189 if self.j == self.layer.height - 1:
190 raise StopIteration()
191 value = self.layer[self.i, self.j]
192 self.i += 1
193 return value
194
195
196class Layer(object):
197 '''A 2d grid of Cells.
198
199 Layers have some basic properties:
200
201 width, height - the dimensions of the Layer in cells
202 tile_width, tile_height - the dimensions of each cell
203 px_width, px_height - the dimensions of the Layer in pixels
204 tilesets - the tilesets used in this Layer (a Tilesets instance)
205 properties - any properties set for this Layer
206 cells - a dict of all the Cell instances for this Layer, keyed off
207 (x, y) index.
208
209 Additionally you may look up a cell using direct item access:
210
211 layer[x, y] is layer.cells[x, y]
212
213 Note that empty cells will be set to None instead of a Cell instance.
214 '''
215 def __init__(self, name, visible, map):
216 self.name = name
217 self.visible = visible
218 self.position = (0, 0)
219 # TODO get from TMX?
220 self.px_width = map.px_width
221 self.px_height = map.px_height
222 self.tile_width = map.tile_width
223 self.tile_height = map.tile_height
224 self.width = map.width
225 self.height = map.height
226 self.tilesets = map.tilesets
227 self.group = pygame.sprite.Group()
228 self.properties = {}
229 self.cells = {}
230
231 def __repr__(self):
232 return '<Layer "%s" at 0x%x>' % (self.name, id(self))
233
234 def __getitem__(self, pos):
235 return self.cells.get(pos)
236
237 def __setitem__(self, pos, tile):
238 x, y = pos
239 px = x * self.tile_width
240 py = y * self.tile_width
241 self.cells[pos] = Cell(x, y, px, py, tile)
242
243 def __iter__(self):
244 return LayerIterator(self)
245
246 @classmethod
247 def fromxml(cls, tag, map):
248 layer = cls(tag.attrib['name'], int(tag.attrib.get('visible', 1)), map)
249
250 data = tag.find('data')
251 if data is None:
252 raise ValueError('layer %s does not contain <data>' % layer.name)
253
254 data = data.text.strip()
255 data = data.decode('base64').decode('zlib')
256 data = struct.unpack('<%di' % (len(data)/4,), data)
257 assert len(data) == layer.width * layer.height
258 for i, gid in enumerate(data):
259 if gid < 1: continue # not set
260 tile = map.tilesets[gid]
261 x = i % layer.width
262 y = i // layer.width
263 layer.cells[x,y] = Cell(x, y, x*map.tile_width, y*map.tile_height, tile)
264
265 return layer
266
267 def update(self, dt, *args):
268 pass
269
270 def set_view(self, x, y, w, h, viewport_ox=0, viewport_oy=0):
271 self.view_x, self.view_y = x, y
272 self.view_w, self.view_h = w, h
273 x -= viewport_ox
274 y -= viewport_oy
275 self.position = (x, y)
276
277 def draw(self, surface):
278 '''Draw this layer, limited to the current viewport, to the Surface.
279 '''
280 ox, oy = self.position
281 w, h = self.view_w, self.view_h
282 for x in range(ox, ox + w + self.tile_width, self.tile_width):
283 i = x // self.tile_width
284 for y in range(oy, oy + h + self.tile_height, self.tile_height):
285 j = y // self.tile_height
286 if (i, j) not in self.cells:
287 continue
288 cell = self.cells[i, j]
289 surface.blit(cell.tile.surface, (cell.px - ox, cell.py - oy))
290
291 def find(self, *properties):
292 '''Find all cells with the given properties set.
293 '''
294 r = []
295 for propname in properties:
296 for cell in self.cells.values():
297 if cell and propname in cell:
298 r.append(cell)
299 return r
300
301 def match(self, **properties):
302 '''Find all cells with the given properties set to the given values.
303 '''
304 r = []
305 for propname in properties:
306 for cell in self.cells.values():
307 if propname not in cell:
308 continue
309 if properties[propname] == cell[propname]:
310 r.append(cell)
311 return r
312
313 def collide(self, rect, propname):
314 '''Find all cells the rect is touching that have the indicated property
315 name set.
316 '''
317 r = []
318 for cell in self.get_in_region(rect.left, rect.top, rect.right,
319 rect.bottom):
320 if not cell.intersects(rect):
321 continue
322 if propname in cell:
323 r.append(cell)
324 return r
325
326 def get_in_region(self, x1, y1, x2, y2):
327 '''Return cells (in [column][row]) that are within the map-space
328 pixel bounds specified by the bottom-left (x1, y1) and top-right
329 (x2, y2) corners.
330
331 Return a list of Cell instances.
332 '''
333 i1 = max(0, x1 // self.tile_width)
334 j1 = max(0, y1 // self.tile_height)
335 i2 = min(self.width, x2 // self.tile_width + 1)
336 j2 = min(self.height, y2 // self.tile_height + 1)
337 return [self.cells[i, j]
338 for i in range(int(i1), int(i2))
339 for j in range(int(j1), int(j2))
340 if (i, j) in self.cells]
341
342 def get_at(self, x, y):
343 '''Return the cell at the nominated (x, y) coordinate.
344
345 Return a Cell instance or None.
346 '''
347 i = x // self.tile_width
348 j = y // self.tile_height
349 return self.cells.get((i, j))
350
351 def neighbors(self, index):
352 '''Return the indexes of the valid (ie. within the map) cardinal (ie.
353 North, South, East, West) neighbors of the nominated cell index.
354
355 Returns a list of 2-tuple indexes.
356 '''
357 i, j = index
358 n = []
359 if i < self.width - 1:
360 n.append((i + 1, j))
361 if i > 0:
362 n.append((i - 1, j))
363 if j < self.height - 1:
364 n.append((i, j + 1))
365 if j > 0:
366 n.append((i, j - 1))
367 return n
368
369
370class Object(object):
371 '''An object in a TMX object layer.
372name: The name of the object. An arbitrary string.
373type: The type of the object. An arbitrary string.
374x: The x coordinate of the object in pixels.
375y: The y coordinate of the object in pixels.
376width: The width of the object in pixels (defaults to 0).
377height: The height of the object in pixels (defaults to 0).
378gid: An reference to a tile (optional).
379visible: Whether the object is shown (1) or hidden (0). Defaults to 1.
380 '''
381 def __init__(self, type, x, y, width=0, height=0, name=None,
382 gid=None, tile=None, visible=1):
383 self.type = type
384 self.px = x
385 self.left = x
386 if tile:
387 y -= tile.tile_height
388 width = tile.tile_width
389 height = tile.tile_height
390 self.py = y
391 self.top = y
392 self.width = width
393 self.right = x + width
394 self.height = height
395 self.bottom = y + height
396 self.name = name
397 self.gid = gid
398 self.tile = tile
399 self.visible = visible
400 self.properties = {}
401
402 self._added_properties = {}
403 self._deleted_properties = set()
404
405 def __repr__(self):
406 if self.tile:
407 return '<Object %s,%s %s,%s tile=%d>' % (self.px, self.py, self.width, self.height, self.gid)
408 else:
409 return '<Object %s,%s %s,%s>' % (self.px, self.py, self.width, self.height)
410
411 def __contains__(self, key):
412 if key in self._deleted_properties:
413 return False
414 if key in self._added_properties:
415 return True
416 if key in self.properties:
417 return True
418 return self.tile and key in self.tile.properties
419
420 def __getitem__(self, key):
421 if key in self._deleted_properties:
422 raise KeyError(key)
423 if key in self._added_properties:
424 return self._added_properties[key]
425 if key in self.properties:
426 return self.properties[key]
427 if self.tile and key in self.tile.properties:
428 return self.tile.properties[key]
429 raise KeyError(key)
430
431 def __setitem__(self, key, value):
432 self._added_properties[key] = value
433
434 def __delitem__(self, key):
435 self._deleted_properties.add(key)
436
437 def draw(self, surface, view_x, view_y):
438 if not self.visible:
439 return
440 x, y = (self.px - view_x, self.py - view_y)
441 if self.tile:
442 surface.blit(self.tile.surface, (x, y))
443 else:
444 r = pygame.Rect((x, y), (self.width, self.height))
445 pygame.draw.rect(surface, (255, 100, 100), r, 2)
446
447 @classmethod
448 def fromxml(cls, tag, map):
449 if 'gid' in tag.attrib:
450 gid = int(tag.attrib['gid'])
451 tile = map.tilesets[gid]
452 w = tile.tile_width
453 h = tile.tile_height
454 else:
455 gid = None
456 tile = None
457 w = int(tag.attrib['width'])
458 h = int(tag.attrib['height'])
459
460 o = cls(tag.attrib.get('type', 'rect'), int(tag.attrib['x']),
461 int(tag.attrib['y']), w, h, tag.attrib.get('name'), gid, tile,
462 int(tag.attrib.get('visible', 1)))
463
464 props = tag.find('properties')
465 if props is None:
466 return o
467
468 for c in props.findall('property'):
469 # store additional properties.
470 name = c.attrib['name']
471 value = c.attrib['value']
472
473 # TODO hax
474 if value.isdigit():
475 value = int(value)
476 o.properties[name] = value
477 return o
478
479 def intersects(self, x1, y1, x2, y2):
480 if x2 < self.px:
481 return False
482 if y2 < self.py:
483 return False
484 if x1 > self.px + self.width:
485 return False
486 if y1 > self.py + self.height:
487 return False
488 return True
489
490
491class ObjectLayer(object):
492 '''A layer composed of basic primitive shapes.
493
494 Actually encompasses a TMX <objectgroup> but even the TMX documentation
495 refers to them as object layers, so I will.
496
497 ObjectLayers have some basic properties:
498
499 position - ignored (cannot be edited in the current Tiled editor)
500 name - the name of the object group.
501 color - the color used to display the objects in this group.
502 opacity - the opacity of the layer as a value from 0 to 1.
503 visible - whether the layer is shown (1) or hidden (0).
504 objects - the objects in this Layer (Object instances)
505 '''
506 def __init__(self, name, color, objects, opacity=1,
507 visible=1, position=(0, 0)):
508 self.name = name
509 self.color = color
510 self.objects = objects
511 self.opacity = opacity
512 self.visible = visible
513 self.position = position
514 self.properties = {}
515
516 def __repr__(self):
517 return '<ObjectLayer "%s" at 0x%x>' % (self.name, id(self))
518
519 @classmethod
520 def fromxml(cls, tag, map):
521 layer = cls(tag.attrib['name'], tag.attrib.get('color'), [],
522 float(tag.attrib.get('opacity', 1)),
523 int(tag.attrib.get('visible', 1)))
524 for object in tag.findall('object'):
525 layer.objects.append(Object.fromxml(object, map))
526 for c in tag.findall('property'):
527 # store additional properties.
528 name = c.attrib['name']
529 value = c.attrib['value']
530
531 # TODO hax
532 if value.isdigit():
533 value = int(value)
534 layer.properties[name] = value
535 return layer
536
537 def update(self, dt, *args):
538 pass
539
540 def set_view(self, x, y, w, h, viewport_ox=0, viewport_oy=0):
541 self.view_x, self.view_y = x, y
542 self.view_w, self.view_h = w, h
543 x -= viewport_ox
544 y -= viewport_oy
545 self.position = (x, y)
546
547 def draw(self, surface):
548 '''Draw this layer, limited to the current viewport, to the Surface.
549 '''
550 if not self.visible:
551 return
552 ox, oy = self.position
553 w, h = self.view_w, self.view_h
554 for object in self.objects:
555 object.draw(surface, self.view_x, self.view_y)
556
557 def find(self, *properties):
558 '''Find all cells with the given properties set.
559 '''
560 r = []
561 for propname in properties:
562 for object in self.objects:
563 if object and propname in object or propname in self.properties:
564 r.append(object)
565 return r
566
567 def match(self, **properties):
568 '''Find all objects with the given properties set to the given values.
569 '''
570 r = []
571 for propname in properties:
572 for object in self.objects:
573 if propname in object:
574 val = object[propname]
575 elif propname in self.properties:
576 val = self.properties[propname]
577 else:
578 continue
579 if properties[propname] == val:
580 r.append(object)
581 return r
582
583 def collide(self, rect, propname):
584 '''Find all objects the rect is touching that have the indicated
585 property name set.
586 '''
587 r = []
588 for object in self.get_in_region(rect.left, rect.top, rect.right,
589 rect.bottom):
590 if propname in object or propname in self.properties:
591 r.append(object)
592 return r
593
594 def get_in_region(self, x1, y1, x2, y2):
595 '''Return objects that are within the map-space
596 pixel bounds specified by the bottom-left (x1, y1) and top-right
597 (x2, y2) corners.
598
599 Return a list of Object instances.
600 '''
601 return [obj for obj in self.objects if obj.intersects(x1, y1, x2, y2)]
602
603 def get_at(self, x, y):
604 '''Return the first object found at the nominated (x, y) coordinate.
605
606 Return an Object instance or None.
607 '''
608 for object in self.objects:
609 if object.contains(x,y):
610 return object
611
612
613class SpriteLayer(pygame.sprite.AbstractGroup):
614 def __init__(self):
615 super(SpriteLayer, self).__init__()
616 self.visible = True
617
618 def set_view(self, x, y, w, h, viewport_ox=0, viewport_oy=0):
619 self.view_x, self.view_y = x, y
620 self.view_w, self.view_h = w, h
621 x -= viewport_ox
622 y -= viewport_oy
623 self.position = (x, y)
624
625 def draw(self, screen):
626 ox, oy = self.position
627 w, h = self.view_w, self.view_h
628 for sprite in self.sprites():
629 sx, sy = sprite.rect.topleft
630 screen.blit(sprite.image, (sx-ox, sy-oy))
631
632class Layers(list):
633 def __init__(self):
634 self.by_name = {}
635
636 def add_named(self, layer, name):
637 self.append(layer)
638 self.by_name[name] = layer
639
640 def __getitem__(self, item):
641 if isinstance(item, int):
642 return self[item]
643 return self.by_name[item]
644
645class TileMap(object):
646 '''A TileMap is a collection of Layers which contain gridded maps or sprites
647 which are drawn constrained by a viewport.
648
649 And breathe.
650
651 TileMaps are loaded from TMX files which sets the .layers and .tilesets
652 properties. After loading additional SpriteLayers may be added.
653
654 A TileMap's rendering is restricted by a viewport which is defined by the
655 size passed in at construction time and the focus set by set_focus() or
656 force_focus().
657
658 TileMaps have a number of properties:
659
660 width, height - the dimensions of the tilemap in cells
661 tile_width, tile_height - the dimensions of the cells in the map
662 px_width, px_height - the dimensions of the tilemap in pixels
663 properties - any properties set on the tilemap in the TMX file
664 layers - all layers of this tilemap as a Layers instance
665 tilesets - all tilesets of this tilemap as a Tilesets instance
666 fx, fy - viewport focus point
667 view_w, view_h - viewport size
668 view_x, view_y - viewport offset (origin)
669 viewport - a Rect instance giving the current viewport specification
670
671 '''
672 def __init__(self, size, origin=(0,0)):
673 self.px_width = 0
674 self.px_height = 0
675 self.tile_width = 0
676 self.tile_height = 0
677 self.width = 0
678 self.height = 0
679 self.properties = {}
680 self.layers = Layers()
681 self.tilesets = Tilesets()
682 self.fx, self.fy = 0, 0 # viewport focus point
683 self.view_w, self.view_h = size # viewport size
684 self.view_x, self.view_y = origin # viewport offset
685 self.viewport = Rect(origin, size)
686
687 def update(self, dt, *args):
688 for layer in self.layers:
689 layer.update(dt, *args)
690
691 def draw(self, screen):
692 for layer in self.layers:
693 if layer.visible:
694 layer.draw(screen)
695
696 @classmethod
697 def load(cls, filename, viewport):
698 with open(filename) as f:
699 map = ElementTree.fromstring(f.read())
700
701 # get most general map informations and create a surface
702 tilemap = TileMap(viewport)
703 tilemap.width = int(map.attrib['width'])
704 tilemap.height = int(map.attrib['height'])
705 tilemap.tile_width = int(map.attrib['tilewidth'])
706 tilemap.tile_height = int(map.attrib['tileheight'])
707 tilemap.px_width = tilemap.width * tilemap.tile_width
708 tilemap.px_height = tilemap.height * tilemap.tile_height
709
710 for tag in map.findall('tileset'):
711 tilemap.tilesets.add(Tileset.fromxml(tag))
712
713 for tag in map.findall('layer'):
714 layer = Layer.fromxml(tag, tilemap)
715 tilemap.layers.add_named(layer, layer.name)
716
717 for tag in map.findall('objectgroup'):
718 layer = ObjectLayer.fromxml(tag, tilemap)
719 tilemap.layers.add_named(layer, layer.name)
720
721 return tilemap
722
723 _old_focus = None
724 def set_focus(self, fx, fy, force=False):
725 '''Determine the viewport based on a desired focus pixel in the
726 Layer space (fx, fy) and honoring any bounding restrictions of
727 child layers.
728
729 The focus will always be shifted to ensure no child layers display
730 out-of-bounds data, as defined by their dimensions px_width and px_height.
731 '''
732 # The result is that all chilren will have their viewport set, defining
733 # which of their pixels should be visible.
734 fx, fy = int(fx), int(fy)
735 self.fx, self.fy = fx, fy
736
737 a = (fx, fy)
738
739 # check for NOOP (same arg passed in)
740 if not force and self._old_focus == a:
741 return
742 self._old_focus = a
743
744 # get our viewport information, scaled as appropriate
745 w = int(self.view_w)
746 h = int(self.view_h)
747 w2, h2 = w//2, h//2
748
749 if self.px_width <= w:
750 # this branch for centered view and no view jump when
751 # crossing the center; both when world width <= view width
752 restricted_fx = self.px_width / 2
753 else:
754 if (fx - w2) < 0:
755 restricted_fx = w2 # hit minimum X extent
756 elif (fx + w2) > self.px_width:
757 restricted_fx = self.px_width - w2 # hit maximum X extent
758 else:
759 restricted_fx = fx
760 if self.px_height <= h:
761 # this branch for centered view and no view jump when
762 # crossing the center; both when world height <= view height
763 restricted_fy = self.px_height / 2
764 else:
765 if (fy - h2) < 0:
766 restricted_fy = h2 # hit minimum Y extent
767 elif (fy + h2) > self.px_height:
768 restricted_fy = self.px_height - h2 # hit maximum Y extent
769 else:
770 restricted_fy = fy
771
772 # ... and this is our focus point, center of screen
773 self.restricted_fx = int(restricted_fx)
774 self.restricted_fy = int(restricted_fy)
775
776 # determine child view bounds to match that focus point
777 x, y = int(restricted_fx - w2), int(restricted_fy - h2)
778 self.viewport.x = x
779 self.viewport.y = y
780
781 self.childs_ox = x - self.view_x
782 self.childs_oy = y - self.view_y
783
784 for layer in self.layers:
785 layer.set_view(x, y, w, h, self.view_x, self.view_y)
786
787 def force_focus(self, fx, fy):
788 '''Force the manager to focus on a point, regardless of any managed layer
789 visible boundaries.
790
791 '''
792 # This calculation takes into account the scaling of this Layer (and
793 # therefore also its children).
794 # The result is that all chilren will have their viewport set, defining
795 # which of their pixels should be visible.
796 self.fx, self.fy = map(int, (fx, fy))
797 self.fx, self.fy = fx, fy
798
799 # get our view size
800 w = int(self.view_w)
801 h = int(self.view_h)
802 w2, h2 = w//2, h//2
803
804 # bottom-left corner of the viewport
805 x, y = fx - w2, fy - h2
806 self.viewport.x = x
807 self.viewport.y = y
808
809 self.childs_ox = x - self.view_x
810 self.childs_oy = y - self.view_y
811
812 for layer in self.layers:
813 layer.set_view(x, y, w, h, self.view_x, self.view_y)
814
815 def pixel_from_screen(self, x, y):
816 '''Look up the Layer-space pixel matching the screen-space pixel.
817 '''
818 vx, vy = self.childs_ox, self.childs_oy
819 return int(vx + x), int(vy + y)
820
821 def pixel_to_screen(self, x, y):
822 '''Look up the screen-space pixel matching the Layer-space pixel.
823 '''
824 screen_x = x-self.childs_ox
825 screen_y = y-self.childs_oy
826 return int(screen_x), int(screen_y)
827
828 def index_at(self, x, y):
829 '''Return the map index at the (screen-space) pixel position.
830 '''
831 sx, sy = self.pixel_from_screen(x, y)
832 return int(sx//self.tile_width), int(sy//self.tile_height)
833
834def load(filename, viewport):
835 return TileMap.load(filename, viewport)
836
837if __name__ == '__main__':
838 # allow image load to work
839 pygame.init()
840 pygame.display.set_mode((640, 480))
841 t = load(sys.argv[1], (0, 0))