1 package com.badlogic.gdx.maps.tiled;
3 import java.io.IOException;
4 import java.util.StringTokenizer;
6 import com.badlogic.gdx.assets.AssetDescriptor;
7 import com.badlogic.gdx.assets.AssetLoaderParameters;
8 import com.badlogic.gdx.assets.AssetManager;
9 import com.badlogic.gdx.assets.loaders.AsynchronousAssetLoader;
10 import com.badlogic.gdx.assets.loaders.FileHandleResolver;
11 import com.badlogic.gdx.assets.loaders.TextureLoader;
12 import com.badlogic.gdx.assets.loaders.TextureLoader.TextureParameter;
13 import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
14 import com.badlogic.gdx.files.FileHandle;
15 import com.badlogic.gdx.graphics.Texture;
16 import com.badlogic.gdx.graphics.Texture.TextureFilter;
17 import com.badlogic.gdx.graphics.g2d.TextureRegion;
18 import com.badlogic.gdx.maps.ImageResolver;
19 import com.badlogic.gdx.maps.ImageResolver.AssetManagerImageResolver;
20 import com.badlogic.gdx.maps.ImageResolver.DirectImageResolver;
21 import com.badlogic.gdx.maps.MapLayer;
22 import com.badlogic.gdx.maps.MapObject;
23 import com.badlogic.gdx.maps.MapProperties;
24 import com.badlogic.gdx.maps.objects.EllipseMapObject;
25 import com.badlogic.gdx.maps.objects.PolygonMapObject;
26 import com.badlogic.gdx.maps.objects.PolylineMapObject;
27 import com.badlogic.gdx.maps.objects.RectangleMapObject;
28 import com.badlogic.gdx.maps.tiled.TiledMapTileLayer.Cell;
29 import com.badlogic.gdx.maps.tiled.tiles.StaticTiledMapTile;
30 import com.badlogic.gdx.math.Polygon;
31 import com.badlogic.gdx.math.Polyline;
32 import com.badlogic.gdx.utils.Array;
33 import com.badlogic.gdx.utils.Base64Coder;
34 import com.badlogic.gdx.utils.GdxRuntimeException;
35 import com.badlogic.gdx.utils.ObjectMap;
36 import com.badlogic.gdx.utils.XmlReader;
37 import com.badlogic.gdx.utils.XmlReader.Element;
39 /** @brief synchronous loader for TMX maps created with the Tiled tool */
40 public class TmxMapLoader extends AsynchronousAssetLoader<TiledMap, TmxMapLoader.Parameters> {
42 public static class Parameters extends AssetLoaderParameters<TiledMap> {
43 /** Whether to load the map for a y-up coordinate system */
44 public boolean yUp = true;
45 /** generate mipmaps? **/
46 public boolean generateMipMaps = false;
47 /** The TextureFilter to use for minification **/
48 public TextureFilter textureMinFilter = TextureFilter.Nearest;
49 /** The TextureFilter to use for magnification **/
50 public TextureFilter textureMagFilter = TextureFilter.Nearest;
53 protected static final int FLAG_FLIP_HORIZONTALLY = 0x80000000;
54 protected static final int FLAG_FLIP_VERTICALLY = 0x40000000;
55 protected static final int FLAG_FLIP_DIAGONALLY = 0x20000000;
56 protected static final int MASK_CLEAR = 0xE0000000;
58 protected XmlReader xml = new XmlReader();
59 protected Element root;
60 protected boolean yUp;
62 protected int mapWidthInPixels;
63 protected int mapHeightInPixels;
65 protected TiledMap map;
67 public TmxMapLoader () {
68 super(new InternalFileHandleResolver());
74 public TmxMapLoader (FileHandleResolver resolver) {
78 /** Loads the {@link TiledMap} from the given file. The file is resolved via the {@link FileHandleResolver} set in the
79 * constructor of this class. By default it will resolve to an internal file. The map will be loaded for a y-up coordinate
81 * @param fileName the filename
82 * @return the TiledMap */
83 public TiledMap load (String fileName) {
84 return load(fileName, new TmxMapLoader.Parameters());
87 /** Loads the {@link TiledMap} from the given file. The file is resolved via the {@link FileHandleResolver} set in the
88 * constructor of this class. By default it will resolve to an internal file.
89 * @param fileName the filename
90 * @param parameters specifies whether to use y-up, generate mip maps etc.
91 * @return the TiledMap */
92 public TiledMap load (String fileName, TmxMapLoader.Parameters parameters) {
94 this.yUp = parameters.yUp;
95 FileHandle tmxFile = resolve(fileName);
96 root = xml.parse(tmxFile);
97 ObjectMap<String, Texture> textures = new ObjectMap<String, Texture>();
98 for (FileHandle textureFile : loadTilesets(root, tmxFile)) {
99 Texture texture = new Texture(textureFile, parameters.generateMipMaps);
100 texture.setFilter(parameters.textureMinFilter, parameters.textureMagFilter);
101 textures.put(textureFile.path(), texture);
103 DirectImageResolver imageResolver = new DirectImageResolver(textures);
104 TiledMap map = loadTilemap(root, tmxFile, imageResolver);
105 map.setOwnedResources(textures.values().toArray());
107 } catch (IOException e) {
108 throw new GdxRuntimeException("Couldn't load tilemap '" + fileName + "'", e);
113 public void loadAsync (AssetManager manager, String fileName, FileHandle tmxFile, TmxMapLoader.Parameters parameter) {
116 if (parameter != null) {
122 map = loadTilemap(root, tmxFile, new AssetManagerImageResolver(manager));
123 } catch (Exception e) {
124 throw new GdxRuntimeException("Couldn't load tilemap '" + fileName + "'", e);
129 public TiledMap loadSync (AssetManager manager, String fileName, FileHandle fileHandle, TmxMapLoader.Parameters parameter) {
133 /** Retrieves TiledMap resource dependencies
136 * @param parameter not used for now
137 * @return dependencies for the given .tmx file */
139 public Array<AssetDescriptor> getDependencies (String fileName, FileHandle tmxFile, Parameters parameter) {
140 Array<AssetDescriptor> dependencies = new Array<AssetDescriptor>();
142 root = xml.parse(tmxFile);
143 boolean generateMipMaps = (parameter != null ? parameter.generateMipMaps : false);
144 TextureLoader.TextureParameter texParams = new TextureParameter();
145 texParams.genMipMaps = generateMipMaps;
146 if (parameter != null) {
147 texParams.minFilter = parameter.textureMinFilter;
148 texParams.magFilter = parameter.textureMagFilter;
150 for (FileHandle image : loadTilesets(root, tmxFile)) {
151 dependencies.add(new AssetDescriptor(image, Texture.class, texParams));
154 } catch (IOException e) {
155 throw new GdxRuntimeException("Couldn't load tilemap '" + fileName + "'", e);
159 /** Loads the map data, given the XML root element and an {@link ImageResolver} used to return the tileset Textures
160 * @param root the XML root element
161 * @param tmxFile the Filehandle of the tmx file
162 * @param imageResolver the {@link ImageResolver}
163 * @return the {@link TiledMap} */
164 protected TiledMap loadTilemap (Element root, FileHandle tmxFile, ImageResolver imageResolver) {
165 TiledMap map = new TiledMap();
167 String mapOrientation = root.getAttribute("orientation", null);
168 int mapWidth = root.getIntAttribute("width", 0);
169 int mapHeight = root.getIntAttribute("height", 0);
170 int tileWidth = root.getIntAttribute("tilewidth", 0);
171 int tileHeight = root.getIntAttribute("tileheight", 0);
172 String mapBackgroundColor = root.getAttribute("backgroundcolor", null);
174 MapProperties mapProperties = map.getProperties();
175 if (mapOrientation != null) {
176 mapProperties.put("orientation", mapOrientation);
178 mapProperties.put("width", mapWidth);
179 mapProperties.put("height", mapHeight);
180 mapProperties.put("tilewidth", tileWidth);
181 mapProperties.put("tileheight", tileHeight);
182 if (mapBackgroundColor != null) {
183 mapProperties.put("backgroundcolor", mapBackgroundColor);
185 mapWidthInPixels = mapWidth * tileWidth;
186 mapHeightInPixels = mapHeight * tileHeight;
188 Element properties = root.getChildByName("properties");
189 if (properties != null) {
190 loadProperties(map.getProperties(), properties);
192 Array<Element> tilesets = root.getChildrenByName("tileset");
193 for (Element element : tilesets) {
194 loadTileSet(map, element, tmxFile, imageResolver);
195 root.removeChild(element);
197 for (int i = 0, j = root.getChildCount(); i < j; i++) {
198 Element element = root.getChild(i);
199 String name = element.getName();
200 if (name.equals("layer")) {
201 loadTileLayer(map, element);
202 } else if (name.equals("objectgroup")) {
203 loadObjectGroup(map, element);
209 /** Loads the tilesets
210 * @param root the root XML element
211 * @return a list of filenames for images containing tiles
212 * @throws IOException */
213 protected Array<FileHandle> loadTilesets (Element root, FileHandle tmxFile) throws IOException {
214 Array<FileHandle> images = new Array<FileHandle>();
215 for (Element tileset : root.getChildrenByName("tileset")) {
216 String source = tileset.getAttribute("source", null);
217 FileHandle image = null;
218 if (source != null) {
219 FileHandle tsx = getRelativeFileHandle(tmxFile, source);
220 tileset = xml.parse(tsx);
221 String imageSource = tileset.getChildByName("image").getAttribute("source");
222 image = getRelativeFileHandle(tsx, imageSource);
224 String imageSource = tileset.getChildByName("image").getAttribute("source");
225 image = getRelativeFileHandle(tmxFile, imageSource);
232 /** Loads the specified tileset data, adding it to the collection of the specified map, given the XML element, the tmxFile and
233 * an {@link ImageResolver} used to retrieve the tileset Textures.
236 * Default tileset's property keys that are loaded by default are:
240 * <li><em>firstgid</em>, (int, defaults to 1) the first valid global id used for tile numbering</li>
241 * <li><em>imagesource</em>, (String, defaults to empty string) the tileset source image filename</li>
242 * <li><em>imagewidth</em>, (int, defaults to 0) the tileset source image width</li>
243 * <li><em>imageheight</em>, (int, defaults to 0) the tileset source image height</li>
244 * <li><em>tilewidth</em>, (int, defaults to 0) the tile width</li>
245 * <li><em>tileheight</em>, (int, defaults to 0) the tile height</li>
246 * <li><em>margin</em>, (int, defaults to 0) the tileset margin</li>
247 * <li><em>spacing</em>, (int, defaults to 0) the tileset spacing</li>
251 * The values are extracted from the specified Tmx file, if a value can't be found then the default is used.
253 * @param map the Map whose tilesets collection will be populated
254 * @param element the XML element identifying the tileset to load
255 * @param tmxFile the Filehandle of the tmx file
256 * @param imageResolver the {@link ImageResolver} */
257 protected void loadTileSet (TiledMap map, Element element, FileHandle tmxFile, ImageResolver imageResolver) {
258 if (element.getName().equals("tileset")) {
259 String name = element.get("name", null);
260 int firstgid = element.getIntAttribute("firstgid", 1);
261 int tilewidth = element.getIntAttribute("tilewidth", 0);
262 int tileheight = element.getIntAttribute("tileheight", 0);
263 int spacing = element.getIntAttribute("spacing", 0);
264 int margin = element.getIntAttribute("margin", 0);
265 String source = element.getAttribute("source", null);
267 String imageSource = "";
268 int imageWidth = 0, imageHeight = 0;
270 FileHandle image = null;
271 if (source != null) {
272 FileHandle tsx = getRelativeFileHandle(tmxFile, source);
274 element = xml.parse(tsx);
275 name = element.get("name", null);
276 tilewidth = element.getIntAttribute("tilewidth", 0);
277 tileheight = element.getIntAttribute("tileheight", 0);
278 spacing = element.getIntAttribute("spacing", 0);
279 margin = element.getIntAttribute("margin", 0);
280 imageSource = element.getChildByName("image").getAttribute("source");
281 imageWidth = element.getChildByName("image").getIntAttribute("width", 0);
282 imageHeight = element.getChildByName("image").getIntAttribute("height", 0);
283 image = getRelativeFileHandle(tsx, imageSource);
284 } catch (IOException e) {
285 throw new GdxRuntimeException("Error parsing external tileset.");
288 imageSource = element.getChildByName("image").getAttribute("source");
289 imageWidth = element.getChildByName("image").getIntAttribute("width", 0);
290 imageHeight = element.getChildByName("image").getIntAttribute("height", 0);
291 image = getRelativeFileHandle(tmxFile, imageSource);
294 TextureRegion texture = imageResolver.getImage(image.path());
296 TiledMapTileSet tileset = new TiledMapTileSet();
297 MapProperties props = tileset.getProperties();
298 tileset.setName(name);
299 props.put("firstgid", firstgid);
300 props.put("imagesource", imageSource);
301 props.put("imagewidth", imageWidth);
302 props.put("imageheight", imageHeight);
303 props.put("tilewidth", tilewidth);
304 props.put("tileheight", tileheight);
305 props.put("margin", margin);
306 props.put("spacing", spacing);
308 int stopWidth = texture.getRegionWidth() - tilewidth;
309 int stopHeight = texture.getRegionHeight() - tileheight;
313 for (int y = margin; y <= stopHeight; y += tileheight + spacing) {
314 for (int x = margin; x <= stopWidth; x += tilewidth + spacing) {
315 TextureRegion tileRegion = new TextureRegion(texture, x, y, tilewidth, tileheight);
317 tileRegion.flip(false, true);
319 TiledMapTile tile = new StaticTiledMapTile(tileRegion);
321 tileset.putTile(id++, tile);
325 Array<Element> tileElements = element.getChildrenByName("tile");
327 for (Element tileElement : tileElements) {
328 int localtid = tileElement.getIntAttribute("id", 0);
329 TiledMapTile tile = tileset.getTile(firstgid + localtid);
331 String terrain = tileElement.getAttribute("terrain", null);
332 if (terrain != null) {
333 tile.getProperties().put("terrain", terrain);
335 String probability = tileElement.getAttribute("probability", null);
336 if (probability != null) {
337 tile.getProperties().put("probability", probability);
339 Element properties = tileElement.getChildByName("properties");
340 if (properties != null) {
341 loadProperties(tile.getProperties(), properties);
346 Element properties = element.getChildByName("properties");
347 if (properties != null) {
348 loadProperties(tileset.getProperties(), properties);
350 map.getTileSets().addTileSet(tileset);
354 /** Load one layer (a 'layer' tag).
357 protected void loadTileLayer (TiledMap map, Element element) {
358 if (element.getName().equals("layer")) {
359 String name = element.getAttribute("name", null);
360 int width = element.getIntAttribute("width", 0);
361 int height = element.getIntAttribute("height", 0);
362 int tileWidth = element.getParent().getIntAttribute("tilewidth", 0);
363 int tileHeight = element.getParent().getIntAttribute("tileheight", 0);
364 boolean visible = element.getIntAttribute("visible", 1) == 1;
365 float opacity = element.getFloatAttribute("opacity", 1.0f);
366 TiledMapTileLayer layer = new TiledMapTileLayer(width, height, tileWidth, tileHeight);
367 layer.setVisible(visible);
368 layer.setOpacity(opacity);
371 TiledMapTileSets tilesets = map.getTileSets();
373 Element data = element.getChildByName("data");
374 String encoding = data.getAttribute("encoding", null);
375 String compression = data.getAttribute("compression", null);
376 if (encoding == null) { // no 'encoding' attribute means that the encoding is XML
377 throw new GdxRuntimeException("Unsupported encoding (XML) for TMX Layer Data");
379 if (encoding.equals("csv")) {
380 String[] array = data.getText().split(",");
381 for (int y = 0; y < height; y++) {
382 for (int x = 0; x < width; x++) {
383 int id = (int)Long.parseLong(array[y * width + x].trim());
385 final boolean flipHorizontally = ((id & FLAG_FLIP_HORIZONTALLY) != 0);
386 final boolean flipVertically = ((id & FLAG_FLIP_VERTICALLY) != 0);
387 final boolean flipDiagonally = ((id & FLAG_FLIP_DIAGONALLY) != 0);
389 id = id & ~MASK_CLEAR;
391 tilesets.getTile(id);
392 TiledMapTile tile = tilesets.getTile(id);
394 Cell cell = createTileLayerCell(flipHorizontally, flipVertically, flipDiagonally);
396 layer.setCell(x, yUp ? height - 1 - y : y, cell);
401 if (encoding.equals("base64")) {
402 byte[] bytes = Base64Coder.decode(data.getText());
403 if (compression == null) {
405 for (int y = 0; y < height; y++) {
406 for (int x = 0; x < width; x++) {
408 int id = unsignedByteToInt(bytes[read++]) | unsignedByteToInt(bytes[read++]) << 8
409 | unsignedByteToInt(bytes[read++]) << 16 | unsignedByteToInt(bytes[read++]) << 24;
411 final boolean flipHorizontally = ((id & FLAG_FLIP_HORIZONTALLY) != 0);
412 final boolean flipVertically = ((id & FLAG_FLIP_VERTICALLY) != 0);
413 final boolean flipDiagonally = ((id & FLAG_FLIP_DIAGONALLY) != 0);
415 id = id & ~MASK_CLEAR;
417 tilesets.getTile(id);
418 TiledMapTile tile = tilesets.getTile(id);
420 Cell cell = createTileLayerCell(flipHorizontally, flipVertically, flipDiagonally);
422 layer.setCell(x, yUp ? height - 1 - y : y, cell);
426 } else if (compression.equals("gzip")) {
427 throw new GdxRuntimeException("GZIP compression not supported in GWT backend");
428 } else if (compression.equals("zlib")) {
429 throw new GdxRuntimeException("ZLIB compression not supported in GWT backend");
432 // any other value of 'encoding' is one we're not aware of, probably a feature of a future version of Tiled
433 throw new GdxRuntimeException("Unrecognised encoding (" + encoding + ") for TMX Layer Data");
436 Element properties = element.getChildByName("properties");
437 if (properties != null) {
438 loadProperties(layer.getProperties(), properties);
440 map.getLayers().add(layer);
444 protected void loadObjectGroup (TiledMap map, Element element) {
445 if (element.getName().equals("objectgroup")) {
446 String name = element.getAttribute("name", null);
447 MapLayer layer = new MapLayer();
449 Element properties = element.getChildByName("properties");
450 if (properties != null) {
451 loadProperties(layer.getProperties(), properties);
454 for (Element objectElement : element.getChildrenByName("object")) {
455 loadObject(layer, objectElement);
458 map.getLayers().add(layer);
462 protected void loadObject (MapLayer layer, Element element) {
463 if (element.getName().equals("object")) {
464 MapObject object = null;
466 int x = element.getIntAttribute("x", 0);
467 int y = (yUp ? mapHeightInPixels - element.getIntAttribute("y", 0) : element.getIntAttribute("y", 0));
469 int width = element.getIntAttribute("width", 0);
470 int height = element.getIntAttribute("height", 0);
472 if (element.getChildCount() > 0) {
473 Element child = null;
474 if ((child = element.getChildByName("polygon")) != null) {
475 String[] points = child.getAttribute("points").split(" ");
476 float[] vertices = new float[points.length * 2];
477 for (int i = 0; i < points.length; i++) {
478 String[] point = points[i].split(",");
479 vertices[i * 2] = Integer.parseInt(point[0]);
480 vertices[i * 2 + 1] = Integer.parseInt(point[1]);
482 vertices[i * 2 + 1] *= -1;
485 Polygon polygon = new Polygon(vertices);
486 polygon.setPosition(x, y);
487 object = new PolygonMapObject(polygon);
488 } else if ((child = element.getChildByName("polyline")) != null) {
489 String[] points = child.getAttribute("points").split(" ");
490 float[] vertices = new float[points.length * 2];
491 for (int i = 0; i < points.length; i++) {
492 String[] point = points[i].split(",");
493 vertices[i * 2] = Integer.parseInt(point[0]);
494 vertices[i * 2 + 1] = Integer.parseInt(point[1]);
496 vertices[i * 2 + 1] *= -1;
499 Polyline polyline = new Polyline(vertices);
500 polyline.setPosition(x, y);
501 object = new PolylineMapObject(polyline);
502 } else if ((child = element.getChildByName("ellipse")) != null) {
503 object = new EllipseMapObject(x, yUp ? y - height : y, width, height);
506 if (object == null) {
507 object = new RectangleMapObject(x, yUp ? y - height : y, width, height);
509 object.setName(element.getAttribute("name", null));
510 String type = element.getAttribute("type", null);
512 object.getProperties().put("type", type);
514 int gid = element.getIntAttribute("gid", -1);
516 object.getProperties().put("gid", gid);
518 object.getProperties().put("x", x);
519 object.getProperties().put("y", yUp ? y - height : y);
520 object.setVisible(element.getIntAttribute("visible", 1) == 1);
521 Element properties = element.getChildByName("properties");
522 if (properties != null) {
523 loadProperties(object.getProperties(), properties);
525 layer.getObjects().add(object);
529 protected void loadProperties (MapProperties properties, Element element) {
530 if (element.getName().equals("properties")) {
531 for (Element property : element.getChildrenByName("property")) {
532 String name = property.getAttribute("name", null);
533 String value = property.getAttribute("value", null);
535 value = property.getText();
537 properties.put(name, value);
542 protected Cell createTileLayerCell (boolean flipHorizontally, boolean flipVertically, boolean flipDiagonally) {
543 Cell cell = new Cell();
544 if (flipDiagonally) {
545 if (flipHorizontally && flipVertically) {
546 cell.setFlipHorizontally(true);
547 cell.setRotation(yUp ? Cell.ROTATE_270 : Cell.ROTATE_90);
548 } else if (flipHorizontally) {
549 cell.setRotation(yUp ? Cell.ROTATE_270 : Cell.ROTATE_90);
550 } else if (flipVertically) {
551 cell.setRotation(yUp ? Cell.ROTATE_90 : Cell.ROTATE_270);
553 cell.setFlipVertically(true);
554 cell.setRotation(yUp ? Cell.ROTATE_270 : Cell.ROTATE_90);
557 cell.setFlipHorizontally(flipHorizontally);
558 cell.setFlipVertically(flipVertically);
563 protected static FileHandle getRelativeFileHandle (FileHandle file, String path) {
564 StringTokenizer tokenizer = new StringTokenizer(path, "\\/");
565 FileHandle result = file.parent();
566 while (tokenizer.hasMoreElements()) {
567 String token = tokenizer.nextToken();
568 if (token.equals(".."))
569 result = result.parent();
571 result = result.child(token);
577 protected static int unsignedByteToInt (byte b) {
578 return (int)b & 0xFF;