Tiled API utilities.
LibGDX brings its own set of Tiled map utilities, including loading and handling of maps exported from the editor. However, the API contains many wrapped non-standard collections, which makes accessing the loaded maps cumbersome. With Kotlin's reified types and extension methods, the Tiled API can be significantly improved.
In many maps that you create with Tiled you will need to access the properties defined in the editor.
They are either defined on map, layer, object, tileset or tile level. The original LibGDX MapProperties
class returns untyped Object
(or Kotlin's Any!
) whenever retrieving a property and is therefore not ideal
and unsafe.
To improve this, multiple extension methods were added to TiledMap
, MapLayer
, MapObject
, TiledMapTileSet
and TiledMapTile
. Also, a new MissingPropertyException
to handle missing properties with an explicit exception.
The new additions include:
property(key: String): T
: returns an existing property or throws aMissingPropertyException
property(key: String, defaultValue: T): T
: returns the value of a property or the default value if the property is missing.propertyOrNull(key: String): T?
: same asproperty(key)
, but returnsnull
instead of throwing an exception.containsProperty(key: String): Boolean
: returnstrue
if and only if the property exists.
MapProperties
were also extended to support the contains
(in
) and set
([]
assignment) operators.
Unfortunately, since the original class already defines get
method, type-safe get
([]
) operator was not added.
In addition to the property extensions, MapObject
automatically comes with a set of standard properties.
They allow to retrieve the properties automatically set and initialized by the TmxMapLoader
.
New extension fields include:
id
x
y
width
height
rotation
: this property is only available if you rotate your object in Tiled.type
: this property is only available if you enter a text for theType
property in Tiled.
Almost all objects are related to a shape except for TextureMapObject
. Sometimes you need
access to these shapes like e.g. when creating Box2D bodies out of those objects. For that reason
a new extension field was added:
shape
: returns theShape2D
of a map object. This can either be aRectangle
,Circle
,Ellipse
,Polyline
orPolygon
. If there is an object that is not linked to a shape then aMissingShapeException
is thrown.
Similar to MapObject
, there were several standard properties added to TiledMap
as well:
width
height
tileWidth
tileHeight
backgroundColor
: this property is only available if you explicitly select a background color for your map.orientation
hexSideLength
staggerAxis
staggerIndex
Two new extension methods will provide you with the total width and height of your map in pixels:
totalWidth()
totalHeight()
The problems that we face with properties are also relevant in case of map layers. To improve layer handling API the following extensions were added:
contains(layerName: String)
: works as thein
operator.layer(layerName: String)
: returns the layer or throws aMissingLayerException
in case the layer does not exist.
Inlined forEachMapObject
extension method allows to iterate over all MapObject
instances present on the chosen
map layer.
Inlined forEachLayer
extension method allows to iterate over all MapLayer
instances of a specific type to execute
a certain function on them.
isEmpty
and isNotEmpty
extension method to check if the specific collection is empty or not.
Accessing properties of TiledMap
, MapLayer
, MapObject
, TiledMapTileSet
and TiledMapTile
:
import com.badlogic.gdx.maps.MapLayer
import com.badlogic.gdx.maps.MapObject
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*
val mapObj: MapObject = getMapObject()
val mapLayer: MapLayer = getMapLayer()
val map: TiledMap = getMap()
// Retrieves Float property - throws MissingPropertyException if missing:
val myFloatProp: Float = mapObj.property("myFloatProperty")
// Retrieves String property with a default value:
val myProp: String = mapObj.property("myProperty", defaultValue = "")
// The explicit type can be omitted as it is automatically derived from the type of the default value.
// myProp2 is of type Float
val myProp2 = mapLayer.property("myProperty2", defaultValue = 1f)
// Retrieves Int property or null if the property does not exist.
val myOtherProp: Int? = map.propertyOrNull("myOtherProperty")
// Check if a certain property exists:
if (map.containsProperty("lightColor")) {
changeAmbientLightColor(map.property<String>("lightColor"))
}
Using MapProperties
API:
import com.badlogic.gdx.maps.MapProperties
import ktx.tiled.contains
import ktx.tiled.set
val mapProperties = MapProperties()
// Adds a property to the map:
mapProperties["key"] = "value"
// Checks if a property exists:
if ("key" in mapProperties) {
// Reads a property:
val value: String = mapProperties["key"] as String
// Note: since MapProperties already defines `get` method,
// a fully typed extension could not have been added.
// Use property-retrieving extensions of other Tiled classes
// or handle casting manually as in the example above.
}
Retrieving standard properties of a MapObject
like id
, x
or shape
:
import com.badlogic.gdx.maps.MapObject
import com.badlogic.gdx.math.Rectangle
import ktx.tiled.*
val mapObj: MapObject = getMapObject()
// Retrieve position of an object:
val x = mapObj.x
val y = mapObj.y
// Retrieves ID of an object:
val id = mapObj.id
// Retrieves shape of an object:
val shape = mapObj.shape // if you only need the Shape2D instance
val rect = mapObj.shape as Rectangle // if you need the rect
Retrieving standard properties of a TiledMap
like width
or tileheight
:
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*
val map: TiledMap = getTiledMap()
// Gets map size in pixels to e.g. lock camera movement within map boundaries:
val totalWidth = map.totalWidth()
val totalHeight = map.totalHeight()
// Retrieve map size information:
val width = map.width
val height = map.height
val tileWidth = map.tileWidth
val tileHeight = map.tileHeight
Working with layers of a TiledMap
:
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*
val map: TiledMap = getTiledMap()
// Contains can be used either with the regular method call syntax:
if (map.contains("enemyLayer")) {
val enemyLayer = map.layer("enemyLayer")
}
// Or with the "in" operator:
if ("collision" in map) {
val collisionLayer = map.layer("collision")
}
// This will throw a MissingLayerException if the layer does not exist:
val layer = map.layer("myMapLayer")
Iterating over map objects of a specific MapLayer
of a map:
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*
val map: TiledMap = getTiledMap()
// Creates collision bodies for every map object of the collision layer:
map.forEachMapObject("collision") { mapObj ->
createStaticBox2DCollisionBody(mapObj.x, mapObj.y, mapObj.shape)
}
Iterating over a specific type of layers of a map:
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*
val map: TiledMap = getTiledMap()
// Iterate over all object layers and parse them.
// Note that println is only called with layers of the exact MapLayer type.
// For example, TiledMapTileLayer - which is a subclass of MapLayer - does not
// have this exact class and will not be matched.
map.forEachLayer<MapLayer> { layer ->
println(layer)
}
Checking if MapLayers
and MapObjects
collections are empty:
import com.badlogic.gdx.maps.MapObjects
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*
val map: TiledMap = TiledMap()
if (map.layers.isNotEmpty()) {
map.layers.forEach { layer ->
if (layer.objects.isEmpty()) {
// nothing to do if there are no objects
return@forEach
}
parseObjects(layer.objects)
}
}