Initial version of the engine
[2dworld.git] / tiledtmxloader / helperspyglet.py
blob455c2f90e550779ed5616e9cc30f9e9c766441a6
1 # -*- coding: utf-8 -*-
3 """
4 > Overview
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/
11 > Demo Controls
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.
16 > Demo Features
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.)
23 """
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
29 # | |
30 # major.minor[.build[.revision]]
31 # |
32 # +-|* 0 for alpha (status)
33 # |* 1 for beta (status)
34 # |* 2 for release candidate
35 # |* 3 for (public) release
37 # For instance:
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 # -----------------------------------------------------------------------------
52 import sys
53 from xml.dom import minidom, Node
54 import io
55 import os.path
57 import pyglet
59 import copy
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.
77 """
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))
95 tex.anchor_x = 0
96 tex.anchor_y = 0
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.
104 :Parameters:
105 filename : string
106 Path to the file to be loaded.
107 fileobj : file
108 A file-like object which pyglet can decode.
110 :rtype: A subclass of AbstractImage.
113 img = self._img_cache.get(filename, None)
114 if img is None:
115 if fileobj:
116 img = pyglet.image.load(filename, fileobj,
117 pyglet.image.codecs.get_decoders("*.png")[0])
118 else:
119 img = pyglet.image.load(filename)
120 self._img_cache[filename] = img
121 return 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.
130 :Parameters:
131 filename : string
132 Path to image to be loaded.
133 margin : int
134 The margin around the image.
135 spacing : int
136 The space between the tile images.
137 tilewidth : int
138 The width of a single tile.
139 tileheight : int
140 The height of a single tile.
141 colorkey : ???
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
151 images = []
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)
157 return images
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.
171 TODO:
172 Maybe use this to put topleft as origin:
173 glMatrixMode(GL_PROJECTION);
174 glLoadIdentity();
175 glOrtho(0.0, (double)mTarget->w, (double)mTarget->h, 0.0, -1.0, 1.0);
179 import pyglet
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()
192 @window.event
193 def on_draw():
194 window.clear()
195 # Reset the "eye" back to the default location.
196 glLoadIdentity()
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.
211 batch.draw()
213 keys = pyglet.window.key.KeyStateHandler()
214 window.push_handlers(keys)
215 resources = ResourceLoaderPyglet()
216 resources.load(world_map)
218 def update(dt):
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]:
226 delta[0] += speed
227 if keys[pyglet.window.key.RIGHT]:
228 delta[0] -= speed
229 if keys[pyglet.window.key.UP]:
230 delta[1] -= speed
231 if keys[pyglet.window.key.DOWN]:
232 delta[1] += speed
234 # Generate the graphics for every visible tile.
235 batch = pyglet.graphics.Batch()
236 sprites = []
237 for group_num, layer in enumerate(world_map.layers):
238 if not layer.visible:
239 continue
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.
244 continue
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]
249 if image_id:
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)
260 pyglet.app.run()
263 # -----------------------------------------------------------------------------
265 if __name__ == '__main__':
266 # import cProfile
267 # cProfile.run('main()', "stats.profile")
268 # import pstats
269 # p = pstats.Stats("stats.profile")
270 # p.strip_dirs()
271 # p.sort_stats('time')
272 # p.print_stats()
273 if len(sys.argv) == 2:
274 demo_pyglet(sys.argv[1])
275 else:
276 print(('Usage: python %s your_map.tmx' % os.path.basename(__file__)))