2 package com.badlogic.gdx.maps.tiled;
4 import java.io.ByteArrayInputStream;
5 import java.io.IOException;
6 import java.util.StringTokenizer;
7 import java.util.zip.DataFormatException;
8 import java.util.zip.GZIPInputStream;
9 import java.util.zip.Inflater;
11 import com.badlogic.gdx.assets.AssetDescriptor;
12 import com.badlogic.gdx.assets.AssetLoaderParameters;
13 import com.badlogic.gdx.assets.AssetManager;
14 import com.badlogic.gdx.assets.loaders.AsynchronousAssetLoader;
15 import com.badlogic.gdx.assets.loaders.FileHandleResolver;
16 import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
17 import com.badlogic.gdx.files.FileHandle;
18 import com.badlogic.gdx.graphics.Texture;
19 import com.badlogic.gdx.graphics.Texture.TextureFilter;
20 import com.badlogic.gdx.graphics.g2d.TextureAtlas;
21 import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
22 import com.badlogic.gdx.maps.MapLayer;
23 import com.badlogic.gdx.maps.MapObject;
24 import com.badlogic.gdx.maps.MapProperties;
25 import com.badlogic.gdx.maps.objects.EllipseMapObject;
26 import com.badlogic.gdx.maps.objects.PolygonMapObject;
27 import com.badlogic.gdx.maps.objects.PolylineMapObject;
28 import com.badlogic.gdx.maps.objects.RectangleMapObject;
29 import com.badlogic.gdx.maps.tiled.TiledMapTileLayer.Cell;
30 import com.badlogic.gdx.maps.tiled.tiles.StaticTiledMapTile;
31 import com.badlogic.gdx.math.Polygon;
32 import com.badlogic.gdx.math.Polyline;
33 import com.badlogic.gdx.utils.Array;
34 import com.badlogic.gdx.utils.Base64Coder;
35 import com.badlogic.gdx.utils.GdxRuntimeException;
36 import com.badlogic.gdx.utils.ObjectMap;
37 import com.badlogic.gdx.utils.XmlReader;
38 import com.badlogic.gdx.utils.XmlReader.Element;
40 /** A TiledMap Loader which loads tiles from a TextureAtlas instead of separate images.
42 * It requires a map-level property called 'atlas' with its value being the relative path to the TextureAtlas. The atlas must have
43 * in it indexed regions named after the tilesets used in the map. The indexes shall be local to the tileset (not the global id).
44 * Strip whitespace and rotation should not be used when creating the atlas.
46 * @author Justin Shapcott
47 * @author Manuel Bua */
48 public class AtlasTmxMapLoader extends AsynchronousAssetLoader<TiledMap, AtlasTmxMapLoader.AtlasTiledMapLoaderParameters> {
50 public static class AtlasTiledMapLoaderParameters extends AssetLoaderParameters<TiledMap> {
51 /** Whether to load the map for a y-up coordinate system */
52 public boolean yUp = true;
54 /** force texture filters? **/
55 public boolean forceTextureFilters = false;
57 /** The TextureFilter to use for minification, if forceTextureFilter is enabled **/
58 public TextureFilter textureMinFilter = TextureFilter.Nearest;
60 /** The TextureFilter to use for magnification, if forceTextureFilter is enabled **/
61 public TextureFilter textureMagFilter = TextureFilter.Nearest;
64 protected static final int FLAG_FLIP_HORIZONTALLY = 0x80000000;
65 protected static final int FLAG_FLIP_VERTICALLY = 0x40000000;
66 protected static final int FLAG_FLIP_DIAGONALLY = 0x20000000;
67 protected static final int MASK_CLEAR = 0xE0000000;
69 protected XmlReader xml = new XmlReader();
70 protected Element root;
71 protected boolean yUp;
73 protected int mapWidthInPixels;
74 protected int mapHeightInPixels;
76 protected TiledMap map;
77 protected Array<Texture> trackedTextures = new Array<Texture>();
79 private interface AtlasResolver {
81 public TextureAtlas getAtlas (String name);
83 public static class DirectAtlasResolver implements AtlasResolver {
85 private final ObjectMap<String, TextureAtlas> atlases;
87 public DirectAtlasResolver (ObjectMap<String, TextureAtlas> atlases) {
88 this.atlases = atlases;
92 public TextureAtlas getAtlas (String name) {
93 return atlases.get(name);
98 public static class AssetManagerAtlasResolver implements AtlasResolver {
99 private final AssetManager assetManager;
101 public AssetManagerAtlasResolver (AssetManager assetManager) {
102 this.assetManager = assetManager;
106 public TextureAtlas getAtlas (String name) {
107 return assetManager.get(name, TextureAtlas.class);
112 public AtlasTmxMapLoader () {
113 super(new InternalFileHandleResolver());
116 public AtlasTmxMapLoader (FileHandleResolver resolver) {
120 public TiledMap load (String fileName) {
121 return load(fileName, new AtlasTiledMapLoaderParameters());
125 public Array<AssetDescriptor> getDependencies (String fileName, FileHandle tmxFile, AtlasTiledMapLoaderParameters parameter) {
126 Array<AssetDescriptor> dependencies = new Array<AssetDescriptor>();
128 root = xml.parse(tmxFile);
130 Element properties = root.getChildByName("properties");
131 if (properties != null) {
132 for (Element property : properties.getChildrenByName("property")) {
133 String name = property.getAttribute("name");
134 String value = property.getAttribute("value");
135 if (name.startsWith("atlas")) {
136 FileHandle atlasHandle = getRelativeFileHandle(tmxFile, value);
137 dependencies.add(new AssetDescriptor(atlasHandle, TextureAtlas.class));
141 } catch (IOException e) {
142 throw new GdxRuntimeException("Unable to parse .tmx file.");
147 public TiledMap load (String fileName, AtlasTiledMapLoaderParameters parameter) {
149 if (parameter != null) {
155 FileHandle tmxFile = resolve(fileName);
156 root = xml.parse(tmxFile);
157 ObjectMap<String, TextureAtlas> atlases = new ObjectMap<String, TextureAtlas>();
158 FileHandle atlasFile = loadAtlas(root, tmxFile);
159 if (atlasFile == null) {
160 throw new GdxRuntimeException("Couldn't load atlas");
163 TextureAtlas atlas = new TextureAtlas(atlasFile);
164 atlases.put(atlasFile.path(), atlas);
166 AtlasResolver.DirectAtlasResolver atlasResolver = new AtlasResolver.DirectAtlasResolver(atlases);
167 TiledMap map = loadMap(root, tmxFile, atlasResolver, parameter);
168 map.setOwnedResources(atlases.values().toArray());
169 setTextureFilters(parameter.textureMinFilter, parameter.textureMagFilter);
172 } catch (IOException e) {
173 throw new GdxRuntimeException("Couldn't load tilemap '" + fileName + "'", e);
177 protected FileHandle loadAtlas (Element root, FileHandle tmxFile) throws IOException {
178 Element e = root.getChildByName("properties");
181 for (Element property : e.getChildrenByName("property")) {
182 String name = property.getAttribute("name", null);
183 String value = property.getAttribute("value", null);
184 if (name.equals("atlas")) {
186 value = property.getText();
189 if (value == null || value.length() == 0) {
190 // keep trying until there are no more atlas properties
194 return getRelativeFileHandle(tmxFile, value);
202 private void setTextureFilters (TextureFilter min, TextureFilter mag) {
203 for (Texture texture : trackedTextures) {
204 texture.setFilter(min, mag);
209 public void loadAsync (AssetManager manager, String fileName, FileHandle tmxFile, AtlasTiledMapLoaderParameters parameter) {
212 if (parameter != null) {
219 map = loadMap(root, tmxFile, new AtlasResolver.AssetManagerAtlasResolver(manager), parameter);
220 } catch (Exception e) {
221 throw new GdxRuntimeException("Couldn't load tilemap '" + fileName + "'", e);
226 public TiledMap loadSync (AssetManager manager, String fileName, FileHandle file, AtlasTiledMapLoaderParameters parameter) {
227 if (parameter != null) {
228 setTextureFilters(parameter.textureMinFilter, parameter.textureMagFilter);
234 protected TiledMap loadMap (Element root, FileHandle tmxFile, AtlasResolver resolver, AtlasTiledMapLoaderParameters parameter) {
235 TiledMap map = new TiledMap();
237 String mapOrientation = root.getAttribute("orientation", null);
238 int mapWidth = root.getIntAttribute("width", 0);
239 int mapHeight = root.getIntAttribute("height", 0);
240 int tileWidth = root.getIntAttribute("tilewidth", 0);
241 int tileHeight = root.getIntAttribute("tileheight", 0);
242 String mapBackgroundColor = root.getAttribute("backgroundcolor", null);
244 MapProperties mapProperties = map.getProperties();
245 if (mapOrientation != null) {
246 mapProperties.put("orientation", mapOrientation);
248 mapProperties.put("width", mapWidth);
249 mapProperties.put("height", mapHeight);
250 mapProperties.put("tilewidth", tileWidth);
251 mapProperties.put("tileheight", tileHeight);
252 if (mapBackgroundColor != null) {
253 mapProperties.put("backgroundcolor", mapBackgroundColor);
255 mapWidthInPixels = mapWidth * tileWidth;
256 mapHeightInPixels = mapHeight * tileHeight;
258 for (int i = 0, j = root.getChildCount(); i < j; i++) {
259 Element element = root.getChild(i);
260 String elementName = element.getName();
261 if (elementName.equals("properties")) {
262 loadProperties(map.getProperties(), element);
263 } else if (elementName.equals("tileset")) {
264 loadTileset(map, element, tmxFile, resolver, parameter);
265 } else if (elementName.equals("layer")) {
266 loadTileLayer(map, element);
267 } else if (elementName.equals("objectgroup")) {
268 loadObjectGroup(map, element);
274 protected void loadTileset (TiledMap map, Element element, FileHandle tmxFile, AtlasResolver resolver,
275 AtlasTiledMapLoaderParameters parameter) {
276 if (element.getName().equals("tileset")) {
277 String name = element.get("name", null);
278 int firstgid = element.getIntAttribute("firstgid", 1);
279 int tilewidth = element.getIntAttribute("tilewidth", 0);
280 int tileheight = element.getIntAttribute("tileheight", 0);
281 int spacing = element.getIntAttribute("spacing", 0);
282 int margin = element.getIntAttribute("margin", 0);
283 String source = element.getAttribute("source", null);
285 String imageSource = "";
286 int imageWidth = 0, imageHeight = 0;
288 FileHandle image = null;
289 if (source != null) {
290 FileHandle tsx = getRelativeFileHandle(tmxFile, source);
292 element = xml.parse(tsx);
293 name = element.get("name", null);
294 tilewidth = element.getIntAttribute("tilewidth", 0);
295 tileheight = element.getIntAttribute("tileheight", 0);
296 spacing = element.getIntAttribute("spacing", 0);
297 margin = element.getIntAttribute("margin", 0);
298 imageSource = element.getChildByName("image").getAttribute("source");
299 imageWidth = element.getChildByName("image").getIntAttribute("width", 0);
300 imageHeight = element.getChildByName("image").getIntAttribute("height", 0);
301 } catch (IOException e) {
302 throw new GdxRuntimeException("Error parsing external tileset.");
305 imageSource = element.getChildByName("image").getAttribute("source");
306 imageWidth = element.getChildByName("image").getIntAttribute("width", 0);
307 imageHeight = element.getChildByName("image").getIntAttribute("height", 0);
310 if (!map.getProperties().containsKey("atlas")) {
311 throw new GdxRuntimeException("The map is missing the 'atlas' property");
314 // get the TextureAtlas for this tileset
315 FileHandle atlasHandle = getRelativeFileHandle(tmxFile, map.getProperties().get("atlas", String.class));
316 atlasHandle = resolve(atlasHandle.path());
317 TextureAtlas atlas = resolver.getAtlas(atlasHandle.path());
318 String regionsName = atlasHandle.nameWithoutExtension();
320 if (parameter != null && parameter.forceTextureFilters) {
321 for (Texture texture : atlas.getTextures()) {
322 trackedTextures.add(texture);
326 TiledMapTileSet tileset = new TiledMapTileSet();
327 MapProperties props = tileset.getProperties();
328 tileset.setName(name);
329 props.put("firstgid", firstgid);
330 props.put("imagesource", imageSource);
331 props.put("imagewidth", imageWidth);
332 props.put("imageheight", imageHeight);
333 props.put("tilewidth", tilewidth);
334 props.put("tileheight", tileheight);
335 props.put("margin", margin);
336 props.put("spacing", spacing);
338 Array<AtlasRegion> regions = atlas.findRegions(regionsName);
339 for (AtlasRegion region : regions) {
340 // handle unused tile ids
341 if (region != null) {
342 StaticTiledMapTile tile = new StaticTiledMapTile(region);
345 region.flip(false, true);
348 int tileid = firstgid + region.index;
350 tileset.putTile(tileid, tile);
354 Array<Element> tileElements = element.getChildrenByName("tile");
356 for (Element tileElement : tileElements) {
357 int localtid = tileElement.getIntAttribute("id", 0);
358 TiledMapTile tile = tileset.getTile(firstgid + localtid);
360 String terrain = tileElement.getAttribute("terrain", null);
361 if (terrain != null) {
362 tile.getProperties().put("terrain", terrain);
364 String probability = tileElement.getAttribute("probability", null);
365 if (probability != null) {
366 tile.getProperties().put("probability", probability);
368 Element properties = tileElement.getChildByName("properties");
369 if (properties != null) {
370 loadProperties(tile.getProperties(), properties);
375 Element properties = element.getChildByName("properties");
376 if (properties != null) {
377 loadProperties(tileset.getProperties(), properties);
379 map.getTileSets().addTileSet(tileset);
383 protected void loadTileLayer (TiledMap map, Element element) {
384 if (element.getName().equals("layer")) {
385 String name = element.getAttribute("name", null);
386 int width = element.getIntAttribute("width", 0);
387 int height = element.getIntAttribute("height", 0);
388 int tileWidth = element.getParent().getIntAttribute("tilewidth", 0);
389 int tileHeight = element.getParent().getIntAttribute("tileheight", 0);
390 boolean visible = element.getIntAttribute("visible", 1) == 1;
391 float opacity = element.getFloatAttribute("opacity", 1.0f);
392 TiledMapTileLayer layer = new TiledMapTileLayer(width, height, tileWidth, tileHeight);
393 layer.setVisible(visible);
394 layer.setOpacity(opacity);
397 TiledMapTileSets tilesets = map.getTileSets();
399 Element data = element.getChildByName("data");
400 String encoding = data.getAttribute("encoding", null);
401 String compression = data.getAttribute("compression", null);
402 if (encoding == null) { // no 'encoding' attribute means that the encoding is XML
403 throw new GdxRuntimeException("Unsupported encoding (XML) for TMX Layer Data");
405 if (encoding.equals("csv")) {
406 String[] array = data.getText().split(",");
407 for (int y = 0; y < height; y++) {
408 for (int x = 0; x < width; x++) {
409 int id = (int)Long.parseLong(array[y * width + x].trim());
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);
427 if (encoding.equals("base64")) {
428 byte[] bytes = Base64Coder.decode(data.getText());
429 if (compression == null) {
431 for (int y = 0; y < height; y++) {
432 for (int x = 0; x < width; x++) {
434 int id = unsignedByteToInt(bytes[read++]) | unsignedByteToInt(bytes[read++]) << 8
435 | unsignedByteToInt(bytes[read++]) << 16 | unsignedByteToInt(bytes[read++]) << 24;
437 final boolean flipHorizontally = ((id & FLAG_FLIP_HORIZONTALLY) != 0);
438 final boolean flipVertically = ((id & FLAG_FLIP_VERTICALLY) != 0);
439 final boolean flipDiagonally = ((id & FLAG_FLIP_DIAGONALLY) != 0);
441 id = id & ~MASK_CLEAR;
443 tilesets.getTile(id);
444 TiledMapTile tile = tilesets.getTile(id);
446 Cell cell = createTileLayerCell(flipHorizontally, flipVertically, flipDiagonally);
448 layer.setCell(x, yUp ? height - 1 - y : y, cell);
452 } else if (compression.equals("gzip")) {
453 GZIPInputStream GZIS = null;
455 GZIS = new GZIPInputStream(new ByteArrayInputStream(bytes), bytes.length);
456 } catch (IOException e) {
457 throw new GdxRuntimeException("Error Reading TMX Layer Data - IOException: " + e.getMessage());
460 byte[] temp = new byte[4];
461 for (int y = 0; y < height; y++) {
462 for (int x = 0; x < width; x++) {
464 GZIS.read(temp, 0, 4);
465 int id = unsignedByteToInt(temp[0]) | unsignedByteToInt(temp[1]) << 8
466 | unsignedByteToInt(temp[2]) << 16 | unsignedByteToInt(temp[3]) << 24;
468 final boolean flipHorizontally = ((id & FLAG_FLIP_HORIZONTALLY) != 0);
469 final boolean flipVertically = ((id & FLAG_FLIP_VERTICALLY) != 0);
470 final boolean flipDiagonally = ((id & FLAG_FLIP_DIAGONALLY) != 0);
472 id = id & ~MASK_CLEAR;
474 tilesets.getTile(id);
475 TiledMapTile tile = tilesets.getTile(id);
477 Cell cell = createTileLayerCell(flipHorizontally, flipVertically, flipDiagonally);
479 layer.setCell(x, yUp ? height - 1 - y : y, cell);
481 } catch (IOException e) {
482 throw new GdxRuntimeException("Error Reading TMX Layer Data.", e);
486 } else if (compression.equals("zlib")) {
487 Inflater zlib = new Inflater();
489 byte[] temp = new byte[4];
491 zlib.setInput(bytes, 0, bytes.length);
493 for (int y = 0; y < height; y++) {
494 for (int x = 0; x < width; x++) {
496 zlib.inflate(temp, 0, 4);
497 int id = unsignedByteToInt(temp[0]) | unsignedByteToInt(temp[1]) << 8
498 | unsignedByteToInt(temp[2]) << 16 | unsignedByteToInt(temp[3]) << 24;
500 final boolean flipHorizontally = ((id & FLAG_FLIP_HORIZONTALLY) != 0);
501 final boolean flipVertically = ((id & FLAG_FLIP_VERTICALLY) != 0);
502 final boolean flipDiagonally = ((id & FLAG_FLIP_DIAGONALLY) != 0);
504 id = id & ~MASK_CLEAR;
506 tilesets.getTile(id);
507 TiledMapTile tile = tilesets.getTile(id);
509 Cell cell = createTileLayerCell(flipHorizontally, flipVertically, flipDiagonally);
511 layer.setCell(x, yUp ? height - 1 - y : y, cell);
514 } catch (DataFormatException e) {
515 throw new GdxRuntimeException("Error Reading TMX Layer Data.", e);
521 // any other value of 'encoding' is one we're not aware of, probably a feature of a future version of Tiled
523 throw new GdxRuntimeException("Unrecognised encoding (" + encoding + ") for TMX Layer Data");
526 Element properties = element.getChildByName("properties");
527 if (properties != null) {
528 loadProperties(layer.getProperties(), properties);
530 map.getLayers().add(layer);
534 protected void loadObjectGroup (TiledMap map, Element element) {
535 if (element.getName().equals("objectgroup")) {
536 String name = element.getAttribute("name", null);
537 MapLayer layer = new MapLayer();
539 Element properties = element.getChildByName("properties");
540 if (properties != null) {
541 loadProperties(layer.getProperties(), properties);
544 for (Element objectElement : element.getChildrenByName("object")) {
545 loadObject(layer, objectElement);
548 map.getLayers().add(layer);
552 protected void loadObject (MapLayer layer, Element element) {
553 if (element.getName().equals("object")) {
554 MapObject object = null;
556 int x = element.getIntAttribute("x", 0);
557 int y = (yUp ? mapHeightInPixels - element.getIntAttribute("y", 0) : element.getIntAttribute("y", 0));
559 int width = element.getIntAttribute("width", 0);
560 int height = element.getIntAttribute("height", 0);
562 if (element.getChildCount() > 0) {
563 Element child = null;
564 if ((child = element.getChildByName("polygon")) != null) {
565 String[] points = child.getAttribute("points").split(" ");
566 float[] vertices = new float[points.length * 2];
567 for (int i = 0; i < points.length; i++) {
568 String[] point = points[i].split(",");
569 vertices[i * 2] = Integer.parseInt(point[0]);
570 vertices[i * 2 + 1] = Integer.parseInt(point[1]);
572 vertices[i * 2 + 1] *= -1;
575 Polygon polygon = new Polygon(vertices);
576 polygon.setPosition(x, y);
577 object = new PolygonMapObject(polygon);
578 } else if ((child = element.getChildByName("polyline")) != null) {
579 String[] points = child.getAttribute("points").split(" ");
580 float[] vertices = new float[points.length * 2];
581 for (int i = 0; i < points.length; i++) {
582 String[] point = points[i].split(",");
583 vertices[i * 2] = Integer.parseInt(point[0]);
584 vertices[i * 2 + 1] = Integer.parseInt(point[1]);
586 vertices[i * 2 + 1] *= -1;
589 Polyline polyline = new Polyline(vertices);
590 polyline.setPosition(x, y);
591 object = new PolylineMapObject(polyline);
592 } else if ((child = element.getChildByName("ellipse")) != null) {
593 object = new EllipseMapObject(x, yUp ? y - height : y, width, height);
596 if (object == null) {
597 object = new RectangleMapObject(x, yUp ? y - height : y, width, height);
599 object.setName(element.getAttribute("name", null));
600 String type = element.getAttribute("type", null);
602 object.getProperties().put("type", type);
604 int gid = element.getIntAttribute("gid", -1);
606 object.getProperties().put("gid", gid);
608 object.getProperties().put("x", x);
609 object.getProperties().put("y", yUp ? y - height : y);
610 object.setVisible(element.getIntAttribute("visible", 1) == 1);
611 Element properties = element.getChildByName("properties");
612 if (properties != null) {
613 loadProperties(object.getProperties(), properties);
615 layer.getObjects().add(object);
619 protected void loadProperties (MapProperties properties, Element element) {
620 if (element.getName().equals("properties")) {
621 for (Element property : element.getChildrenByName("property")) {
622 String name = property.getAttribute("name", null);
623 String value = property.getAttribute("value", null);
625 value = property.getText();
627 properties.put(name, value);
632 protected Cell createTileLayerCell (boolean flipHorizontally, boolean flipVertically, boolean flipDiagonally) {
633 Cell cell = new Cell();
634 if (flipDiagonally) {
635 if (flipHorizontally && flipVertically) {
636 cell.setFlipHorizontally(true);
637 cell.setRotation(yUp ? Cell.ROTATE_270 : Cell.ROTATE_90);
638 } else if (flipHorizontally) {
639 cell.setRotation(yUp ? Cell.ROTATE_270 : Cell.ROTATE_90);
640 } else if (flipVertically) {
641 cell.setRotation(yUp ? Cell.ROTATE_90 : Cell.ROTATE_270);
643 cell.setFlipVertically(true);
644 cell.setRotation(yUp ? Cell.ROTATE_270 : Cell.ROTATE_90);
647 cell.setFlipHorizontally(flipHorizontally);
648 cell.setFlipVertically(flipVertically);
653 public static FileHandle getRelativeFileHandle (FileHandle file, String path) {
654 StringTokenizer tokenizer = new StringTokenizer(path, "\\/");
655 FileHandle result = file.parent();
656 while (tokenizer.hasMoreElements()) {
657 String token = tokenizer.nextToken();
658 if (token.equals(".."))
659 result = result.parent();
661 result = result.child(token);
667 protected static int unsignedByteToInt (byte b) {
668 return (int)b & 0xFF;