1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.maps.tiled; 18 19 import java.io.IOException; 20 import java.util.StringTokenizer; 21 22 import com.badlogic.gdx.assets.AssetDescriptor; 23 import com.badlogic.gdx.assets.AssetLoaderParameters; 24 import com.badlogic.gdx.assets.AssetManager; 25 import com.badlogic.gdx.assets.loaders.AsynchronousAssetLoader; 26 import com.badlogic.gdx.assets.loaders.FileHandleResolver; 27 import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver; 28 import com.badlogic.gdx.files.FileHandle; 29 import com.badlogic.gdx.graphics.Texture; 30 import com.badlogic.gdx.graphics.Texture.TextureFilter; 31 import com.badlogic.gdx.graphics.g2d.TextureAtlas; 32 import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; 33 import com.badlogic.gdx.maps.MapLayer; 34 import com.badlogic.gdx.maps.MapObject; 35 import com.badlogic.gdx.maps.MapProperties; 36 import com.badlogic.gdx.maps.objects.EllipseMapObject; 37 import com.badlogic.gdx.maps.objects.PolygonMapObject; 38 import com.badlogic.gdx.maps.objects.PolylineMapObject; 39 import com.badlogic.gdx.maps.objects.RectangleMapObject; 40 import com.badlogic.gdx.maps.tiled.TiledMapTileLayer.Cell; 41 import com.badlogic.gdx.maps.tiled.tiles.AnimatedTiledMapTile; 42 import com.badlogic.gdx.maps.tiled.tiles.StaticTiledMapTile; 43 import com.badlogic.gdx.math.Polygon; 44 import com.badlogic.gdx.math.Polyline; 45 import com.badlogic.gdx.utils.Array; 46 import com.badlogic.gdx.utils.GdxRuntimeException; 47 import com.badlogic.gdx.utils.IntArray; 48 import com.badlogic.gdx.utils.LongArray; 49 import com.badlogic.gdx.utils.ObjectMap; 50 import com.badlogic.gdx.utils.XmlReader; 51 import com.badlogic.gdx.utils.XmlReader.Element; 52 53 /** A TiledMap Loader which loads tiles from a TextureAtlas instead of separate images. 54 * 55 * It requires a map-level property called 'atlas' with its value being the relative path to the TextureAtlas. The atlas must have 56 * in it indexed regions named after the tilesets used in the map. The indexes shall be local to the tileset (not the global id). 57 * Strip whitespace and rotation should not be used when creating the atlas. 58 * 59 * @author Justin Shapcott 60 * @author Manuel Bua */ 61 public class AtlasTmxMapLoader extends BaseTmxMapLoader<AtlasTmxMapLoader.AtlasTiledMapLoaderParameters> { 62 63 public static class AtlasTiledMapLoaderParameters extends BaseTmxMapLoader.Parameters { 64 /** force texture filters? **/ 65 public boolean forceTextureFilters = false; 66 } 67 68 protected Array<Texture> trackedTextures = new Array<Texture>(); 69 70 private interface AtlasResolver { 71 getAtlas(String name)72 public TextureAtlas getAtlas (String name); 73 74 public static class DirectAtlasResolver implements AtlasResolver { 75 76 private final ObjectMap<String, TextureAtlas> atlases; 77 DirectAtlasResolver(ObjectMap<String, TextureAtlas> atlases)78 public DirectAtlasResolver (ObjectMap<String, TextureAtlas> atlases) { 79 this.atlases = atlases; 80 } 81 82 @Override getAtlas(String name)83 public TextureAtlas getAtlas (String name) { 84 return atlases.get(name); 85 } 86 87 } 88 89 public static class AssetManagerAtlasResolver implements AtlasResolver { 90 private final AssetManager assetManager; 91 AssetManagerAtlasResolver(AssetManager assetManager)92 public AssetManagerAtlasResolver (AssetManager assetManager) { 93 this.assetManager = assetManager; 94 } 95 96 @Override getAtlas(String name)97 public TextureAtlas getAtlas (String name) { 98 return assetManager.get(name, TextureAtlas.class); 99 } 100 } 101 } 102 AtlasTmxMapLoader()103 public AtlasTmxMapLoader () { 104 super(new InternalFileHandleResolver()); 105 } 106 AtlasTmxMapLoader(FileHandleResolver resolver)107 public AtlasTmxMapLoader (FileHandleResolver resolver) { 108 super(resolver); 109 } 110 load(String fileName)111 public TiledMap load (String fileName) { 112 return load(fileName, new AtlasTiledMapLoaderParameters()); 113 } 114 115 @Override getDependencies(String fileName, FileHandle tmxFile, AtlasTiledMapLoaderParameters parameter)116 public Array<AssetDescriptor> getDependencies (String fileName, FileHandle tmxFile, AtlasTiledMapLoaderParameters parameter) { 117 Array<AssetDescriptor> dependencies = new Array<AssetDescriptor>(); 118 try { 119 root = xml.parse(tmxFile); 120 121 Element properties = root.getChildByName("properties"); 122 if (properties != null) { 123 for (Element property : properties.getChildrenByName("property")) { 124 String name = property.getAttribute("name"); 125 String value = property.getAttribute("value"); 126 if (name.startsWith("atlas")) { 127 FileHandle atlasHandle = getRelativeFileHandle(tmxFile, value); 128 dependencies.add(new AssetDescriptor(atlasHandle, TextureAtlas.class)); 129 } 130 } 131 } 132 } catch (IOException e) { 133 throw new GdxRuntimeException("Unable to parse .tmx file."); 134 } 135 return dependencies; 136 } 137 load(String fileName, AtlasTiledMapLoaderParameters parameter)138 public TiledMap load (String fileName, AtlasTiledMapLoaderParameters parameter) { 139 try { 140 if (parameter != null) { 141 convertObjectToTileSpace = parameter.convertObjectToTileSpace; 142 flipY = parameter.flipY; 143 } else { 144 convertObjectToTileSpace = false; 145 flipY = true; 146 } 147 148 FileHandle tmxFile = resolve(fileName); 149 root = xml.parse(tmxFile); 150 ObjectMap<String, TextureAtlas> atlases = new ObjectMap<String, TextureAtlas>(); 151 FileHandle atlasFile = loadAtlas(root, tmxFile); 152 if (atlasFile == null) { 153 throw new GdxRuntimeException("Couldn't load atlas"); 154 } 155 156 TextureAtlas atlas = new TextureAtlas(atlasFile); 157 atlases.put(atlasFile.path(), atlas); 158 159 AtlasResolver.DirectAtlasResolver atlasResolver = new AtlasResolver.DirectAtlasResolver(atlases); 160 TiledMap map = loadMap(root, tmxFile, atlasResolver); 161 map.setOwnedResources(atlases.values().toArray()); 162 setTextureFilters(parameter.textureMinFilter, parameter.textureMagFilter); 163 return map; 164 } catch (IOException e) { 165 throw new GdxRuntimeException("Couldn't load tilemap '" + fileName + "'", e); 166 } 167 } 168 169 /** May return null. */ loadAtlas(Element root, FileHandle tmxFile)170 protected FileHandle loadAtlas (Element root, FileHandle tmxFile) throws IOException { 171 Element e = root.getChildByName("properties"); 172 173 if (e != null) { 174 for (Element property : e.getChildrenByName("property")) { 175 String name = property.getAttribute("name", null); 176 String value = property.getAttribute("value", null); 177 if (name.equals("atlas")) { 178 if (value == null) { 179 value = property.getText(); 180 } 181 182 if (value == null || value.length() == 0) { 183 // keep trying until there are no more atlas properties 184 continue; 185 } 186 187 return getRelativeFileHandle(tmxFile, value); 188 } 189 } 190 } 191 FileHandle atlasFile = tmxFile.sibling(tmxFile.nameWithoutExtension() + ".atlas"); 192 return atlasFile.exists() ? atlasFile : null; 193 } 194 setTextureFilters(TextureFilter min, TextureFilter mag)195 private void setTextureFilters (TextureFilter min, TextureFilter mag) { 196 for (Texture texture : trackedTextures) { 197 texture.setFilter(min, mag); 198 } 199 trackedTextures.clear(); 200 } 201 202 @Override loadAsync(AssetManager manager, String fileName, FileHandle tmxFile, AtlasTiledMapLoaderParameters parameter)203 public void loadAsync (AssetManager manager, String fileName, FileHandle tmxFile, AtlasTiledMapLoaderParameters parameter) { 204 map = null; 205 206 if (parameter != null) { 207 convertObjectToTileSpace = parameter.convertObjectToTileSpace; 208 flipY = parameter.flipY; 209 } else { 210 convertObjectToTileSpace = false; 211 flipY = true; 212 } 213 214 try { 215 map = loadMap(root, tmxFile, new AtlasResolver.AssetManagerAtlasResolver(manager)); 216 } catch (Exception e) { 217 throw new GdxRuntimeException("Couldn't load tilemap '" + fileName + "'", e); 218 } 219 } 220 221 @Override loadSync(AssetManager manager, String fileName, FileHandle file, AtlasTiledMapLoaderParameters parameter)222 public TiledMap loadSync (AssetManager manager, String fileName, FileHandle file, AtlasTiledMapLoaderParameters parameter) { 223 if (parameter != null) { 224 setTextureFilters(parameter.textureMinFilter, parameter.textureMagFilter); 225 } 226 227 return map; 228 } 229 loadMap(Element root, FileHandle tmxFile, AtlasResolver resolver)230 protected TiledMap loadMap (Element root, FileHandle tmxFile, AtlasResolver resolver) { 231 TiledMap map = new TiledMap(); 232 233 String mapOrientation = root.getAttribute("orientation", null); 234 int mapWidth = root.getIntAttribute("width", 0); 235 int mapHeight = root.getIntAttribute("height", 0); 236 int tileWidth = root.getIntAttribute("tilewidth", 0); 237 int tileHeight = root.getIntAttribute("tileheight", 0); 238 String mapBackgroundColor = root.getAttribute("backgroundcolor", null); 239 240 MapProperties mapProperties = map.getProperties(); 241 if (mapOrientation != null) { 242 mapProperties.put("orientation", mapOrientation); 243 } 244 mapProperties.put("width", mapWidth); 245 mapProperties.put("height", mapHeight); 246 mapProperties.put("tilewidth", tileWidth); 247 mapProperties.put("tileheight", tileHeight); 248 if (mapBackgroundColor != null) { 249 mapProperties.put("backgroundcolor", mapBackgroundColor); 250 } 251 252 mapTileWidth = tileWidth; 253 mapTileHeight = tileHeight; 254 mapWidthInPixels = mapWidth * tileWidth; 255 mapHeightInPixels = mapHeight * tileHeight; 256 257 if (mapOrientation != null) { 258 if ("staggered".equals(mapOrientation)) { 259 if (mapHeight > 1) { 260 mapWidthInPixels += tileWidth / 2; 261 mapHeightInPixels = mapHeightInPixels / 2 + tileHeight / 2; 262 } 263 } 264 } 265 266 for (int i = 0, j = root.getChildCount(); i < j; i++) { 267 Element element = root.getChild(i); 268 String elementName = element.getName(); 269 if (elementName.equals("properties")) { 270 loadProperties(map.getProperties(), element); 271 } else if (elementName.equals("tileset")) { 272 loadTileset(map, element, tmxFile, resolver); 273 } else if (elementName.equals("layer")) { 274 loadTileLayer(map, element); 275 } else if (elementName.equals("objectgroup")) { 276 loadObjectGroup(map, element); 277 } 278 } 279 return map; 280 } 281 loadTileset(TiledMap map, Element element, FileHandle tmxFile, AtlasResolver resolver)282 protected void loadTileset (TiledMap map, Element element, FileHandle tmxFile, AtlasResolver resolver) { 283 if (element.getName().equals("tileset")) { 284 String name = element.get("name", null); 285 int firstgid = element.getIntAttribute("firstgid", 1); 286 int tilewidth = element.getIntAttribute("tilewidth", 0); 287 int tileheight = element.getIntAttribute("tileheight", 0); 288 int spacing = element.getIntAttribute("spacing", 0); 289 int margin = element.getIntAttribute("margin", 0); 290 String source = element.getAttribute("source", null); 291 292 int offsetX = 0; 293 int offsetY = 0; 294 295 String imageSource = ""; 296 int imageWidth = 0, imageHeight = 0; 297 298 FileHandle image = null; 299 if (source != null) { 300 FileHandle tsx = getRelativeFileHandle(tmxFile, source); 301 try { 302 element = xml.parse(tsx); 303 name = element.get("name", null); 304 tilewidth = element.getIntAttribute("tilewidth", 0); 305 tileheight = element.getIntAttribute("tileheight", 0); 306 spacing = element.getIntAttribute("spacing", 0); 307 margin = element.getIntAttribute("margin", 0); 308 Element offset = element.getChildByName("tileoffset"); 309 if (offset != null) { 310 offsetX = offset.getIntAttribute("x", 0); 311 offsetY = offset.getIntAttribute("y", 0); 312 } 313 Element imageElement = element.getChildByName("image"); 314 if (imageElement != null) { 315 imageSource = imageElement.getAttribute("source"); 316 imageWidth = imageElement.getIntAttribute("width", 0); 317 imageHeight = imageElement.getIntAttribute("height", 0); 318 image = getRelativeFileHandle(tsx, imageSource); 319 } 320 } catch (IOException e) { 321 throw new GdxRuntimeException("Error parsing external tileset."); 322 } 323 } else { 324 Element offset = element.getChildByName("tileoffset"); 325 if (offset != null) { 326 offsetX = offset.getIntAttribute("x", 0); 327 offsetY = offset.getIntAttribute("y", 0); 328 } 329 Element imageElement = element.getChildByName("image"); 330 if (imageElement != null) { 331 imageSource = imageElement.getAttribute("source"); 332 imageWidth = imageElement.getIntAttribute("width", 0); 333 imageHeight = imageElement.getIntAttribute("height", 0); 334 image = getRelativeFileHandle(tmxFile, imageSource); 335 } 336 } 337 338 String atlasFilePath = map.getProperties().get("atlas", String.class); 339 if (atlasFilePath == null) { 340 FileHandle atlasFile = tmxFile.sibling(tmxFile.nameWithoutExtension() + ".atlas"); 341 if (atlasFile.exists()) atlasFilePath = atlasFile.name(); 342 } 343 if (atlasFilePath == null) { 344 throw new GdxRuntimeException("The map is missing the 'atlas' property"); 345 } 346 347 // get the TextureAtlas for this tileset 348 FileHandle atlasHandle = getRelativeFileHandle(tmxFile, atlasFilePath); 349 atlasHandle = resolve(atlasHandle.path()); 350 TextureAtlas atlas = resolver.getAtlas(atlasHandle.path()); 351 String regionsName = name; 352 353 for (Texture texture : atlas.getTextures()) { 354 trackedTextures.add(texture); 355 } 356 357 TiledMapTileSet tileset = new TiledMapTileSet(); 358 MapProperties props = tileset.getProperties(); 359 tileset.setName(name); 360 props.put("firstgid", firstgid); 361 props.put("imagesource", imageSource); 362 props.put("imagewidth", imageWidth); 363 props.put("imageheight", imageHeight); 364 props.put("tilewidth", tilewidth); 365 props.put("tileheight", tileheight); 366 props.put("margin", margin); 367 props.put("spacing", spacing); 368 369 if (imageSource != null && imageSource.length() > 0) { 370 int lastgid = firstgid + ((imageWidth / tilewidth) * (imageHeight / tileheight)) - 1; 371 for (AtlasRegion region : atlas.findRegions(regionsName)) { 372 // handle unused tile ids 373 if (region != null) { 374 int tileid = region.index + 1; 375 if (tileid >= firstgid && tileid <= lastgid) { 376 StaticTiledMapTile tile = new StaticTiledMapTile(region); 377 tile.setId(tileid); 378 tile.setOffsetX(offsetX); 379 tile.setOffsetY(flipY ? -offsetY : offsetY); 380 tileset.putTile(tileid, tile); 381 } 382 } 383 } 384 } 385 386 for (Element tileElement : element.getChildrenByName("tile")) { 387 int tileid = firstgid + tileElement.getIntAttribute("id", 0); 388 TiledMapTile tile = tileset.getTile(tileid); 389 if (tile == null) { 390 Element imageElement = tileElement.getChildByName("image"); 391 if (imageElement != null) { 392 // Is a tilemap with individual images. 393 String regionName = imageElement.getAttribute("source"); 394 regionName = regionName.substring(0, regionName.lastIndexOf('.')); 395 AtlasRegion region = atlas.findRegion(regionName); 396 if (region == null) throw new GdxRuntimeException("Tileset region not found: " + regionName); 397 tile = new StaticTiledMapTile(region); 398 tile.setId(tileid); 399 tile.setOffsetX(offsetX); 400 tile.setOffsetY(flipY ? -offsetY : offsetY); 401 tileset.putTile(tileid, tile); 402 } 403 } 404 if (tile != null) { 405 String terrain = tileElement.getAttribute("terrain", null); 406 if (terrain != null) { 407 tile.getProperties().put("terrain", terrain); 408 } 409 String probability = tileElement.getAttribute("probability", null); 410 if (probability != null) { 411 tile.getProperties().put("probability", probability); 412 } 413 Element properties = tileElement.getChildByName("properties"); 414 if (properties != null) { 415 loadProperties(tile.getProperties(), properties); 416 } 417 } 418 } 419 420 Array<Element> tileElements = element.getChildrenByName("tile"); 421 422 Array<AnimatedTiledMapTile> animatedTiles = new Array<AnimatedTiledMapTile>(); 423 424 for (Element tileElement : tileElements) { 425 int localtid = tileElement.getIntAttribute("id", 0); 426 TiledMapTile tile = tileset.getTile(firstgid + localtid); 427 if (tile != null) { 428 Element animationElement = tileElement.getChildByName("animation"); 429 if (animationElement != null) { 430 431 Array<StaticTiledMapTile> staticTiles = new Array<StaticTiledMapTile>(); 432 IntArray intervals = new IntArray(); 433 for (Element frameElement: animationElement.getChildrenByName("frame")) { 434 staticTiles.add((StaticTiledMapTile) tileset.getTile(firstgid + frameElement.getIntAttribute("tileid"))); 435 intervals.add(frameElement.getIntAttribute("duration")); 436 } 437 438 AnimatedTiledMapTile animatedTile = new AnimatedTiledMapTile(intervals, staticTiles); 439 animatedTile.setId(tile.getId()); 440 animatedTiles.add(animatedTile); 441 tile = animatedTile; 442 } 443 444 String terrain = tileElement.getAttribute("terrain", null); 445 if (terrain != null) { 446 tile.getProperties().put("terrain", terrain); 447 } 448 String probability = tileElement.getAttribute("probability", null); 449 if (probability != null) { 450 tile.getProperties().put("probability", probability); 451 } 452 Element properties = tileElement.getChildByName("properties"); 453 if (properties != null) { 454 loadProperties(tile.getProperties(), properties); 455 } 456 } 457 } 458 459 for (AnimatedTiledMapTile tile : animatedTiles) { 460 tileset.putTile(tile.getId(), tile); 461 } 462 463 Element properties = element.getChildByName("properties"); 464 if (properties != null) { 465 loadProperties(tileset.getProperties(), properties); 466 } 467 map.getTileSets().addTileSet(tileset); 468 } 469 } 470 471 } 472