Skip to content

Commit

Permalink
Merge pull request #234 from Quillraven/develop
Browse files Browse the repository at this point in the history
#233 tile map extensions for libktx
  • Loading branch information
czyzby authored Dec 23, 2019
2 parents 8104f26 + 5217f7c commit bd73e00
Show file tree
Hide file tree
Showing 15 changed files with 937 additions and 0 deletions.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ include(
'math',
'scene2d',
'style',
'tiled',
'vis',
'vis-style'
)
210 changes: 210 additions & 0 deletions tiled/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# KTX : TiledMap utilities

Tiled utilities for LibGDX applications written with Kotlin.

### Why?

As usual with plain libgdx Java the code compared to Kotlin becomes boilerplate and is not
very easy to read especially for users who are not familiar with your code.
With the TiledMap functionality it becomes worse since there are a lot of wrapped collections
and iterators and accessing properties or iterating over objects in the map becomes very cumbersome.

Luckily for us with Kotlin we can write extension functions and properties to make our life
a little bit easier and most notably make our code more readable and safe.

Due to Kotlin's `reified` possibility we can retrieve properties without passing the `Class`
parameter and therefore also have this information during runtime. Also, most of the wrapper classes return `platform types (Any!)`
as the original Java code is null-unsafe. One of the goals of these extensions is also
to improve that situation and make it explicit so that a method either returns `null` or
throws an `exception`.

### Guide

#### Miscellaneous utilities

#### `MapProperties`

In most maps that you create with Tiled you will need access to the properties defined in the editor.
They are either defined on map, layer, object, tileset or tile level. The original libgdx
`MapProperties` class returns `Any!` whenever retrieving a property and is therefore not ideal and unsafe.

For that reason four extension methods got added to `TiledMap`, `MapLayer`, `MapObject`, `TiledMapTileSet`
and `TiledMapTile`. Also, a new `MissingPropertyException` was added for one of these extensions which allows us
to get informed when trying to access a missing mandatory property:
- `property(key)`: returns an existing property or throws a `MissingPropertyException`
- `property(key, defaultValue`: returns the value of a property or the default value if the property is missing
- `propertyOrNull(key)`: same as `property(key)` but returns `null` instead of throwing an exception
- `containsProperty(key)`: returns `true` if and only if the property exists

#### `MapObject`

In addition to the property extensions, `MapObject` automatically comes with a set of standard properties.
They get automatically set and initialized by the `TmxMapLoader`. Extension properties got added
to ease the access:
- `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 the `Type` 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 property got added:
- `shape`: returns the `Shape2D` of a map object. This can either be a `Rectangle`, `Circle`,
`Ellipse`, `Polyline` or `Polygon`. If there is an object that is not linked to a shape then a
`MissingShapeException` is thrown

#### `TiledMap`

Similar to `MapObject` there are several standard properties for `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 extensions will provide you with the total width and height of your map in pixels:
- `totalWidth()`
- `totalHeight()`

The problems that we face with properties is also true for map layers. To improve the
situation here as well following extensions got added:
- `contains(layerName)`
- `layer(layerName)`: returns the layer or throws a `MissingLayerException` in case the layer does not exist

Often you need to iterate over all objects of a layer to do something with them like
creating collision bodies for box2d or other things. To ease that use case a new
extension was added called `forEachMapObject` which takes a lambda that is executed for each
object of the given layer:
- `forEachMapObject(layerName, action)`

### Usage examples

#### General

Properties functionality is the same for `TiledMap`, `MapLayer`, `MapObject`, `TiledMapTileSet` and `TiledMapTile`:

```kotlin
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()

// retrieve Float property - throws MissingPropertyException if missing
val myFloatProp: Float = mapObj.property("myFloatProperty")

// retrieve String property via defaultValue
val myProp: String = mapObj.property("myProperty", "")

// 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", 1f)

// retrieve Int property without defaultValue
val myOtherProp: Int? = map.propertyOrNull("myOtherProperty")

// check if a certain property exists
if (map.containsProperty("lightColor")) {
changeAmbientLightColor(map.property<String>("lightColor"))
}
```

#### `MapObject`

Retrieving standard properties of a `MapObject` like `id`, `x` or `shape`:

```kotlin
import com.badlogic.gdx.maps.MapObject
import com.badlogic.gdx.math.Rectangle
import ktx.tiled.*


val mapObj: MapObject = getMapObject()

// retrieve position of object
val x = mapObj.x
val y = mapObj.y

// retrieve id of object
val id = mapObj.id

// retrieve shape
val shape = mapObj.shape // if you only need the Shape2D instance
val rect = mapObj.shape as Rectangle // if you need the rect
```

#### `TiledMap`

Retrieving standard properties of a `TiledMap` like `width` or `tileheight`:

```kotlin
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*


val map: TiledMap = getTiledMap()

// get 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
```

Check and retrieve a layer of a `TiledMap`:

```kotlin
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*


val map: TiledMap = getTiledMap()

// contains can either be used with the normal syntax
if(map.contains("enemyLayer")) {
val enemyLayer = map.layer("enemyLayer")
}

// or with the "in" syntax
if("collision" in map) {
val collisionLayer = map.layer("collision")
}

// the next line 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:

```kotlin
import com.badlogic.gdx.maps.tiled.TiledMap
import ktx.tiled.*


val map: TiledMap = getTiledMap()

// create collision bodies for every map object of the collision layer
map.forEachMapObject("collision") { mapObj ->
createStaticBox2DCollisionBody(mapObj.x, mapObj.y, mapObj.shape)
}
```

#### Additional documentation

- [LibGDX Tile maps official wiki.](https://github.com/libgdx/libgdx/wiki/Tile-maps)
- [Tiled editor official documentation](https://doc.mapeditor.org/en/stable/)
3 changes: 3 additions & 0 deletions tiled/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
provided "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
}
2 changes: 2 additions & 0 deletions tiled/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
projectName=ktx-tiled
projectDesc=Tiled utilities for LibGDX applications written with Kotlin.
23 changes: 23 additions & 0 deletions tiled/src/main/kotlin/ktx/tiled/exception.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ktx.tiled

import com.badlogic.gdx.maps.MapLayer
import com.badlogic.gdx.maps.MapObject
import com.badlogic.gdx.maps.MapProperties
import com.badlogic.gdx.maps.objects.TextureMapObject
import com.badlogic.gdx.maps.tiled.TiledMap
import com.badlogic.gdx.utils.GdxRuntimeException

/**
* [GdxRuntimeException] that is thrown when trying to access a non-existing property of a [MapProperties] instance
*/
class MissingPropertyException(message: String) : GdxRuntimeException(message)

/**
* [GdxRuntimeException] that is thrown when trying to access a non-existing [MapLayer] of a [TiledMap] instance
*/
class MissingLayerException(message: String) : GdxRuntimeException(message)

/**
* [GdxRuntimeException] that is thrown when trying to access a shape of a [MapObject] that do not have any shape like [TextureMapObject]
*/
class MissingShapeException(message: String) : GdxRuntimeException(message)
38 changes: 38 additions & 0 deletions tiled/src/main/kotlin/ktx/tiled/mapLayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ktx.tiled

import com.badlogic.gdx.maps.MapLayer
import com.badlogic.gdx.maps.MapProperties

/**
* Extension method to directly access the [MapProperties] of a [MapLayer]. If the property
* is not defined then this method throws a [MissingPropertyException].
* @param key property name
* @return value of the property
* @throws MissingPropertyException If the property is not defined
*/
inline fun <reified T> MapLayer.property(key: String): T = this.properties[key, T::class.java]
?: throw MissingPropertyException("Property $key does not exist for layer ${this.name}")

/**
* Extension method to directly access the [MapProperties] of a [MapLayer]. The type is automatically
* derived from the type of the given default value. If the property is not defined the defaultValue will be returned.
* @param key property name
* @param defaultValue default value in case the property is missing
* @return value of the property or defaultValue if property is missing
*/
inline fun <reified T> MapLayer.property(key: String, defaultValue: T): T = this.properties[key, defaultValue, T::class.java]

/**
* Extension method to directly access the [MapProperties] of a [MapLayer]. If the property
* is not defined then this method returns null.
* @param key property name
* @return value of the property or null if the property is missing
*/
inline fun <reified T> MapLayer.propertyOrNull(key: String): T? = this.properties[key, T::class.java]

/**
* Extension method to directly access the [MapProperties] of a [MapLayer] and its [containsKey][MapProperties.containsKey] method
* @param key property name
* @return true if the property exists. Otherwise false
*/
fun MapLayer.containsProperty(key: String) = properties.containsKey(key)
Loading

0 comments on commit bd73e00

Please sign in to comment.