1 # -*- coding: utf-8 -*-
5 This program contains a sample implementation for loading a map produced
6 by Tiled in pyglet. The script can be run on its own to demonstrate its
7 capabilities, or the script can be imported to use its functionality. Users
8 will hopefully use the ResourceLoaderPyglet already provided in this.
9 Tiled may be found at http://mapeditor.org/
12 Holding the arrow keys scrolls the map.
13 Holding the left shift key makes you scroll faster.
14 Pressing the Esc key closes the program.
17 The map is fully viewable by scrolling.
18 You can scroll outside of the bounds of the map.
19 All visible layers are loaded and displayed.
20 Transparency is supported. (Nothing needed to be done for this.)
21 Minimal OpenGL used. (Less of a learning curve.)
25 # Versioning scheme based on: http://en.wikipedia.org/wiki/Versioning#Designating_development_stage
27 # +-- api change, probably incompatible with older versions
28 # | +-- enhancements but no api change
30 # major.minor[.build[.revision]]
32 # +-|* 0 for alpha (status)
33 # |* 1 for beta (status)
34 # |* 2 for release candidate
35 # |* 3 for (public) release
38 # * 1.2.0.1 instead of 1.2-a
39 # * 1.2.1.2 instead of 1.2-b2 (beta with some bug fixes)
40 # * 1.2.2.3 instead of 1.2-rc (release candidate)
41 # * 1.2.3.0 instead of 1.2-r (commercial distribution)
42 # * 1.2.3.5 instead of 1.2-r5 (commercial distribution with many bug fixes)
44 __revision__
= "$Rev$"
45 __version__
= "3.0.0." + __revision__
[6:-2]
46 __author__
= 'DR0ID @ 2009-2011'
49 # -----------------------------------------------------------------------------
53 from xml
.dom
import minidom
, Node
60 from . import tmxreader
62 # -----------------------------------------------------------------------------
64 # [20:31] bjorn: Of course, for fastest rendering, you would combine the used
65 # tiles into a single texture and set up arrays of vertex and texture coordinates.
66 # .. so that the video card can dump the map to the screen without having to
67 # analyze the tile data again and again.
69 class ResourceLoaderPyglet(tmxreader
.AbstractResourceLoader
):
70 """Loads all tile images and lays them out on a grid.
72 Unlike the AbstractResourceLoader this class derives from, no overridden
73 methods use a colorkey parameter. A colorkey is only useful for pygame.
74 This loader adds its own pyglet-specific parameter to deal with
75 pyglet.image.load's capability to work with file-like objects.
79 def load(self
, tile_map
):
80 tmxreader
.AbstractResourceLoader
.load(self
, tile_map
)
81 # ISSUE 17: flipped tiles
82 for layer
in self
.world_map
.layers
:
83 for gid
in layer
.decoded_content
:
84 if gid
not in self
.indexed_tiles
:
85 if gid
& self
.FLIP_X
or gid
& self
.FLIP_Y
:
86 image_gid
= gid
& ~
(self
.FLIP_X | self
.FLIP_Y
)
87 offx
, offy
, img
= self
.indexed_tiles
[image_gid
]
88 # TODO: how to flip it? this does mix textures and image classes
89 img
= copy
.deepcopy(img
)
90 tex
= img
.get_texture()
91 tex
.anchor_x
= tex
.width
// 2
92 tex
.anchor_y
= tex
.height
// 2
93 tex2
= tex
.get_transform(bool(gid
& self
.FLIP_X
), bool(gid
& self
.FLIP_Y
))
94 # img2 = pyglet.image.ImageDataRegion(img.x, img.y, tex2.width, tex2.height, tex2.image_data))
97 self
.indexed_tiles
[gid
] = (offx
, offy
, tex2
)
99 def _load_image(self
, filename
, fileobj
=None):
100 """Load a single image.
102 Images are loaded only once. Subsequence loads call upon a cache.
106 Path to the file to be loaded.
108 A file-like object which pyglet can decode.
110 :rtype: A subclass of AbstractImage.
113 img
= self
._img
_cache
.get(filename
, None)
116 img
= pyglet
.image
.load(filename
, fileobj
,
117 pyglet
.image
.codecs
.get_decoders("*.png")[0])
119 img
= pyglet
.image
.load(filename
)
120 self
._img
_cache
[filename
] = img
123 def _load_image_part(self
, filename
, x
, y
, w
, h
):
124 """Load a section of an image and returns its ImageDataRegion."""
125 return self
._load
_image
(filename
).get_region(x
, y
, w
, h
)
127 def _load_image_parts(self
, filename
, margin
, spacing
, tile_width
, tile_height
, colorkey
=None):
128 """Load different tile images from one source image.
132 Path to image to be loaded.
134 The margin around the image.
136 The space between the tile images.
138 The width of a single tile.
140 The height of a single tile.
142 Unused. (Intended for pygame.)
144 :rtype: A list of images.
147 source_img
= self
._load
_image
(filename
)
148 # ISSUE 16 fixed wrong sized tilesets
149 height
= (source_img
.height
// tile_height
) * tile_height
150 width
= (source_img
.width
// tile_width
) * tile_width
152 # Reverse the map column reading to compensate for pyglet's y-origin.
153 for y
in range(height
- tile_height
, margin
- tile_height
, -tile_height
- spacing
):
154 for x
in range(margin
, width
, tile_width
+ spacing
):
155 img_part
= self
._load
_image
_part
(filename
, x
, y
- spacing
, tile_width
, tile_height
)
156 images
.append(img_part
)
159 def _load_image_file_like(self
, file_like_obj
):
160 """Loads a file-like object and returns its subclassed AbstractImage."""
161 # TODO: Ask myself why this extra indirection is necessary.
162 return self
._load
_image
(file_like_obj
, file_like_obj
)
165 # -----------------------------------------------------------------------------
168 def demo_pyglet(file_name
):
169 """Demonstrates loading, rendering, and traversing a Tiled map in pyglet.
172 Maybe use this to put topleft as origin:
173 glMatrixMode(GL_PROJECTION);
175 glOrtho(0.0, (double)mTarget->w, (double)mTarget->h, 0.0, -1.0, 1.0);
180 from pyglet
.gl
import glTranslatef
, glLoadIdentity
182 world_map
= tmxreader
.TileMapParser().parse_decode(file_name
)
183 # delta is the x/y position of the map view.
184 # delta is a list so that it can be accessed from the on_draw method of
185 # window and the update function. Note that the position is in integers to
186 # match Pyglet Sprites. Using floating-point numbers causes graphical
187 # problems. See http://groups.google.com/group/pyglet-users/browse_thread/thread/52f9ae1ef7b0c8fa?pli=1
188 delta
= [200, -world_map
.pixel_height
+150]
189 frames_per_sec
= 1.0 / 30.0
190 window
= pyglet
.window
.Window()
195 # Reset the "eye" back to the default location.
197 # Move the "eye" to the current location on the map.
198 glTranslatef(delta
[0], delta
[1], 0.0)
199 # TODO: [21:03] thorbjorn: DR0ID_: You can generally determine the range of tiles that are visible before your drawing loop, which is much faster than looping over all tiles and checking whether it is visible for each of them.
200 # [21:06] DR0ID_: probably would have to rewrite the pyglet demo to use a similar render loop as you mentioned
201 # [21:06] thorbjorn: Yeah.
202 # [21:06] DR0ID_: I'll keep your suggestion in mind, thanks
203 # [21:06] thorbjorn: I haven't written a specific OpenGL renderer yet, so not sure what's the best approach for a tile map.
204 # [21:07] thorbjorn: Best to create a single texture with all your tiles, bind it, set up your vertex arrays and fill it with the coordinates of the tiles currently on the screen, and then let OpenGL draw the bunch.
205 # [21:08] DR0ID_: for each layer?
206 # [21:08] DR0ID_: yeah, probably a good approach
207 # [21:09] thorbjorn: Ideally for all layers at the same time, if you don't have to draw anything in between.
208 # [21:09] DR0ID_: well, the NPC and other dynamic things need to be drawn in between, right?
209 # [21:09] thorbjorn: Right, so maybe once for the bottom layers, then your complicated stuff, and then another time for the layers on top.
213 keys
= pyglet
.window
.key
.KeyStateHandler()
214 window
.push_handlers(keys
)
215 resources
= ResourceLoaderPyglet()
216 resources
.load(world_map
)
219 # The speed is 3 by default.
220 # When left Shift is held, the speed increases.
221 # The speed interpolates based on time passed, so the demo navigates
222 # at a reasonable pace even on huge maps.
223 speed
= (3 + keys
[pyglet
.window
.key
.LSHIFT
] * 6) * \
224 int(dt
/ frames_per_sec
)
225 if keys
[pyglet
.window
.key
.LEFT
]:
227 if keys
[pyglet
.window
.key
.RIGHT
]:
229 if keys
[pyglet
.window
.key
.UP
]:
231 if keys
[pyglet
.window
.key
.DOWN
]:
234 # Generate the graphics for every visible tile.
235 batch
= pyglet
.graphics
.Batch()
237 for group_num
, layer
in enumerate(world_map
.layers
):
238 if not layer
.visible
:
240 if layer
.is_object_group
:
241 # This is unimplemented in this minimal-case example code.
242 # Should you as a user of tmxreader need this layer,
243 # I hope to have a separate demo using objects as well.
245 group
= pyglet
.graphics
.OrderedGroup(group_num
)
246 for ytile
in range(layer
.height
):
247 for xtile
in range(layer
.width
):
248 image_id
= layer
.content2D
[xtile
][ytile
]
250 image_file
= resources
.indexed_tiles
[image_id
][2]
251 # The loader needed to load the images upside-down to match
252 # the tiles to their correct images. This reversal must be
253 # done again to render the rows in the correct order.
254 sprites
.append(pyglet
.sprite
.Sprite(image_file
,
255 world_map
.tilewidth
* xtile
,
256 world_map
.tileheight
* (layer
.height
- ytile
),
257 batch
=batch
, group
=group
))
259 pyglet
.clock
.schedule_interval(update
, frames_per_sec
)
263 # -----------------------------------------------------------------------------
265 if __name__
== '__main__':
267 # cProfile.run('main()', "stats.profile")
269 # p = pstats.Stats("stats.profile")
271 # p.sort_stats('time')
273 if len(sys
.argv
) == 2:
274 demo_pyglet(sys
.argv
[1])
276 print(('Usage: python %s your_map.tmx' % os
.path
.basename(__file__
)))