diff --git a/math/README.md b/math/README.md index 7980c06b..11d65b03 100644 --- a/math/README.md +++ b/math/README.md @@ -240,6 +240,15 @@ val spawners = listOf( ) ``` +#### `Shape2D` extensions + +- `Rectangle` and `Ellipse` instances can be destructed to four float variables in one step with + `val (x, y, w, h) = rectangle/ellipse` syntax thanks to `component1()`, `component2()`, `component3()` and `component4()` operator methods. +- `Circle` instances can be destructed to three float variables in one step with + `val (x, y, radius) = circle` syntax thanks to `component1()`, `component2()` and `component3()` operator methods. +- `Polygon` and `Polyline` instances can be destructed to two float variables in one step with + `val (x, y) = polygon/polyline` syntax thanks to `component1()` and `component2()` operator methods. + ### Alternatives You can use libGDX APIs directly or rely on third-party math libraries: diff --git a/math/src/main/kotlin/ktx/math/circle.kt b/math/src/main/kotlin/ktx/math/circle.kt new file mode 100644 index 00000000..f0032305 --- /dev/null +++ b/math/src/main/kotlin/ktx/math/circle.kt @@ -0,0 +1,21 @@ +package ktx.math + +import com.badlogic.gdx.math.Circle + +/** + * Operator function that allows to deconstruct this circle. + * @return X component. + */ +operator fun Circle.component1() = this.x + +/** + * Operator function that allows to deconstruct this circle. + * @return Y component. + */ +operator fun Circle.component2() = this.y + +/** + * Operator function that allows to deconstruct this circle. + * @return Radius component. + */ +operator fun Circle.component3() = this.radius diff --git a/math/src/main/kotlin/ktx/math/ellipse.kt b/math/src/main/kotlin/ktx/math/ellipse.kt new file mode 100644 index 00000000..7a0545cc --- /dev/null +++ b/math/src/main/kotlin/ktx/math/ellipse.kt @@ -0,0 +1,27 @@ +package ktx.math + +import com.badlogic.gdx.math.Ellipse + +/** + * Operator function that allows to deconstruct this ellipse. + * @return X component. + */ +operator fun Ellipse.component1() = this.x + +/** + * Operator function that allows to deconstruct this ellipse. + * @return Y component. + */ +operator fun Ellipse.component2() = this.y + +/** + * Operator function that allows to deconstruct this ellipse. + * @return Width component. + */ +operator fun Ellipse.component3() = this.width + +/** + * Operator function that allows to deconstruct this ellipse. + * @return Height component. + */ +operator fun Ellipse.component4() = this.height diff --git a/math/src/main/kotlin/ktx/math/polygon.kt b/math/src/main/kotlin/ktx/math/polygon.kt new file mode 100644 index 00000000..2b20037e --- /dev/null +++ b/math/src/main/kotlin/ktx/math/polygon.kt @@ -0,0 +1,15 @@ +package ktx.math + +import com.badlogic.gdx.math.Polygon + +/** + * Operator function that allows to deconstruct this polygon. + * @return X component. + */ +operator fun Polygon.component1() = this.x + +/** + * Operator function that allows to deconstruct this polygon. + * @return Y component. + */ +operator fun Polygon.component2() = this.y diff --git a/math/src/main/kotlin/ktx/math/polyline.kt b/math/src/main/kotlin/ktx/math/polyline.kt new file mode 100644 index 00000000..d2dcc526 --- /dev/null +++ b/math/src/main/kotlin/ktx/math/polyline.kt @@ -0,0 +1,15 @@ +package ktx.math + +import com.badlogic.gdx.math.Polyline + +/** + * Operator function that allows to deconstruct this polyline. + * @return X component. + */ +operator fun Polyline.component1() = this.x + +/** + * Operator function that allows to deconstruct this polyline. + * @return Y component. + */ +operator fun Polyline.component2() = this.y diff --git a/math/src/main/kotlin/ktx/math/rectangle.kt b/math/src/main/kotlin/ktx/math/rectangle.kt new file mode 100644 index 00000000..ecea1faf --- /dev/null +++ b/math/src/main/kotlin/ktx/math/rectangle.kt @@ -0,0 +1,27 @@ +package ktx.math + +import com.badlogic.gdx.math.Rectangle + +/** + * Operator function that allows to deconstruct this rectangle. + * @return X component. + */ +operator fun Rectangle.component1() = this.x + +/** + * Operator function that allows to deconstruct this rectangle. + * @return Y component. + */ +operator fun Rectangle.component2() = this.y + +/** + * Operator function that allows to deconstruct this rectangle. + * @return Width component. + */ +operator fun Rectangle.component3() = this.width + +/** + * Operator function that allows to deconstruct this rectangle. + * @return Height component. + */ +operator fun Rectangle.component4() = this.height diff --git a/math/src/test/kotlin/ktx/math/CircleTest.kt b/math/src/test/kotlin/ktx/math/CircleTest.kt new file mode 100644 index 00000000..74bf33eb --- /dev/null +++ b/math/src/test/kotlin/ktx/math/CircleTest.kt @@ -0,0 +1,20 @@ +package ktx.math + +import com.badlogic.gdx.math.Circle +import org.junit.Assert.assertEquals +import org.junit.Test + +class CircleTest { + + @Test + fun `should destructure Circle`() { + val circle = Circle(1f, 2f, 3f) + + val (x, y, radius) = circle + + assertEquals(1f, x) + assertEquals(2f, y) + assertEquals(3f, radius) + } + +} diff --git a/math/src/test/kotlin/ktx/math/EllipseTest.kt b/math/src/test/kotlin/ktx/math/EllipseTest.kt new file mode 100644 index 00000000..a684ec8c --- /dev/null +++ b/math/src/test/kotlin/ktx/math/EllipseTest.kt @@ -0,0 +1,21 @@ +package ktx.math + +import com.badlogic.gdx.math.Ellipse +import org.junit.Assert.assertEquals +import org.junit.Test + +class EllipseTest { + + @Test + fun `should destructure Ellipse`() { + val ellipse = Ellipse(1f, 2f, 3f, 4f) + + val (x, y, w, h) = ellipse + + assertEquals(1f, x) + assertEquals(2f, y) + assertEquals(3f, w) + assertEquals(4f, h) + } + +} diff --git a/math/src/test/kotlin/ktx/math/PolygonTest.kt b/math/src/test/kotlin/ktx/math/PolygonTest.kt new file mode 100644 index 00000000..8d876ad5 --- /dev/null +++ b/math/src/test/kotlin/ktx/math/PolygonTest.kt @@ -0,0 +1,19 @@ +package ktx.math + +import com.badlogic.gdx.math.Polygon +import org.junit.Assert.assertEquals +import org.junit.Test + +class PolygonTest { + + @Test + fun `should destructure Polygon`() { + val polygon = Polygon().apply { setPosition(1f, 2f) } + + val (x, y) = polygon + + assertEquals(1f, x) + assertEquals(2f, y) + } + +} diff --git a/math/src/test/kotlin/ktx/math/PolylineTest.kt b/math/src/test/kotlin/ktx/math/PolylineTest.kt new file mode 100644 index 00000000..5ed007a7 --- /dev/null +++ b/math/src/test/kotlin/ktx/math/PolylineTest.kt @@ -0,0 +1,19 @@ +package ktx.math + +import com.badlogic.gdx.math.Polyline +import org.junit.Assert.assertEquals +import org.junit.Test + +class PolylineTest { + + @Test + fun `should destructure Polyline`() { + val polyline = Polyline().apply { setPosition(1f, 2f) } + + val (x, y) = polyline + + assertEquals(1f, x) + assertEquals(2f, y) + } + +} diff --git a/math/src/test/kotlin/ktx/math/RectangleTest.kt b/math/src/test/kotlin/ktx/math/RectangleTest.kt new file mode 100644 index 00000000..d13fbeb4 --- /dev/null +++ b/math/src/test/kotlin/ktx/math/RectangleTest.kt @@ -0,0 +1,21 @@ +package ktx.math + +import com.badlogic.gdx.math.Rectangle +import org.junit.Assert.assertEquals +import org.junit.Test + +class RectangleTest { + + @Test + fun `should destructure Rectangle`() { + val rect = Rectangle(1f, 2f, 3f, 4f) + + val (x, y, w, h) = rect + + assertEquals(1f, x) + assertEquals(2f, y) + assertEquals(3f, w) + assertEquals(4f, h) + } + +} diff --git a/tiled/README.md b/tiled/README.md index 91a2dc7d..00e63a7d 100644 --- a/tiled/README.md +++ b/tiled/README.md @@ -6,7 +6,7 @@ ### Why? -libGDX brings its own set of Tiled map utilities, including loading and handling of maps exported from the editor. +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. @@ -83,6 +83,11 @@ a certain function on them. `isEmpty` and `isNotEmpty` extension method to check if the specific collection is empty or not. +### `BatchTiledMapRenderer` + +`use` extension method to call `beginRender()` and `endRender()` automatically before +your render logic. + ### Usage examples #### General @@ -259,6 +264,26 @@ if (map.layers.isNotEmpty()) { } ``` +Using the `use` extension function to render background layers: + +```kotlin +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.maps.tiled.TiledMap +import com.badlogic.gdx.maps.tiled.TiledMapTileLayer +import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer + +val tiledMap = TiledMap() +val renderer = OrthogonalTiledMapRenderer(tiledMap) +val camera = OrthographicCamera() +val bgdLayers: List = tiledMap.layers + .filter { it.name.startsWith("bgd") } + .filterIsInstance() + +renderer.use(camera) { mapRenderer -> + bgdLayers.forEach { mapRenderer.renderTileLayer(it) } +} +``` + #### Additional documentation - [Official Tiled website.](https://www.mapeditor.org/) diff --git a/tiled/src/main/kotlin/ktx/tiled/batchTiledMapRenderer.kt b/tiled/src/main/kotlin/ktx/tiled/batchTiledMapRenderer.kt new file mode 100644 index 00000000..69ba2e02 --- /dev/null +++ b/tiled/src/main/kotlin/ktx/tiled/batchTiledMapRenderer.kt @@ -0,0 +1,16 @@ +@file:Suppress("PackageDirectoryMismatch") + +package com.badlogic.gdx.maps.tiled.renderers + +/** + * This file is used as a workaround for the tiledMapRenderer extensions. They need + * to call [BatchTiledMapRenderer.beginRender] and [BatchTiledMapRenderer.endRender] methods + * which are protected methods. Since this file matches the package of the [BatchTiledMapRenderer], + * we can call protected methods of it and use our wrapper functions for public API extensions. + */ + +@PublishedApi +internal fun BatchTiledMapRenderer.beginInternal() = this.beginRender() + +@PublishedApi +internal fun BatchTiledMapRenderer.endInternal() = this.endRender() diff --git a/tiled/src/main/kotlin/ktx/tiled/tiledMapRenderer.kt b/tiled/src/main/kotlin/ktx/tiled/tiledMapRenderer.kt new file mode 100644 index 00000000..d4c21297 --- /dev/null +++ b/tiled/src/main/kotlin/ktx/tiled/tiledMapRenderer.kt @@ -0,0 +1,64 @@ +package ktx.tiled + +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.maps.tiled.renderers.BatchTiledMapRenderer +import com.badlogic.gdx.maps.tiled.renderers.beginInternal +import com.badlogic.gdx.maps.tiled.renderers.endInternal +import com.badlogic.gdx.math.Matrix4 +import com.badlogic.gdx.math.Rectangle + +/** + * Automatically calls [BatchTiledMapRenderer.beginRender] and [BatchTiledMapRenderer.endRender]. + * @param camera A camera to set on the renderer before [BatchTiledMapRenderer.beginRender]. + * @param block inlined. Executed after [BatchTiledMapRenderer.beginRender] and before [BatchTiledMapRenderer.endRender]. + */ +inline fun T.use( + camera: OrthographicCamera, + block: (T) -> Unit +) { + this.setView(camera) + this.use(block) +} + +/** + * Automatically calls [BatchTiledMapRenderer.beginRender] and [BatchTiledMapRenderer.endRender]. + * @param projection A projection matrix to set on the renderer before [BatchTiledMapRenderer.beginRender]. + * @param x map render boundary x value. + * @param y map render boundary y value. + * @param width map render boundary width value. + * @param height map render boundary height value. + * @param block inlined. Executed after [BatchTiledMapRenderer.beginRender] and before [BatchTiledMapRenderer.endRender]. + */ +inline fun T.use( + projection: Matrix4, + x: Float, y: Float, + width: Float, height: Float, + block: (T) -> Unit +) { + this.setView(projection, x, y, width, height) + this.use(block) +} + +/** + * Automatically calls [BatchTiledMapRenderer.beginRender] and [BatchTiledMapRenderer.endRender]. + * @param projection A projection matrix to set on the renderer before [BatchTiledMapRenderer.beginRender]. + * @param mapBoundary map render boundary. + * @param block inlined. Executed after [BatchTiledMapRenderer.beginRender] and before [BatchTiledMapRenderer.endRender]. + */ +inline fun T.use( + projection: Matrix4, + mapBoundary: Rectangle, + block: (T) -> Unit +) = this.use(projection, mapBoundary.x, mapBoundary.y, mapBoundary.width, mapBoundary.height, block) + +/** + * Automatically calls [BatchTiledMapRenderer.beginRender] and [BatchTiledMapRenderer.endRender]. + * @param block inlined. Executed after [BatchTiledMapRenderer.beginRender] and before [BatchTiledMapRenderer.endRender]. + */ +inline fun T.use(block: (T) -> Unit) { + this.beginInternal() + + block(this) + + this.endInternal() +} diff --git a/tiled/src/test/kotlin/ktx/tiled/TiledMapRendererTest.kt b/tiled/src/test/kotlin/ktx/tiled/TiledMapRendererTest.kt new file mode 100644 index 00000000..7196eefb --- /dev/null +++ b/tiled/src/test/kotlin/ktx/tiled/TiledMapRendererTest.kt @@ -0,0 +1,75 @@ +package ktx.tiled + +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.maps.tiled.renderers.BatchTiledMapRenderer +import com.badlogic.gdx.maps.tiled.renderers.beginInternal +import com.badlogic.gdx.maps.tiled.renderers.endInternal +import com.badlogic.gdx.math.Matrix4 +import com.badlogic.gdx.math.Rectangle +import io.kotlintest.mock.mock +import org.junit.Assert.assertSame +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +class TiledMapRendererTest { + + @Test + fun `should call beginRender and endRender without setView`() { + val renderer = mock() + + renderer.use { + verify(renderer).beginInternal() + assertSame(renderer, it) + verify(renderer, never()).endInternal() + } + verify(renderer).endInternal() + verify(renderer, never()).setView(any()) + } + + @Test + fun `should call beginRender and endRender with camera setView`() { + val renderer = mock() + val camera = OrthographicCamera() + + renderer.use(camera) { + verify(renderer).beginInternal() + verify(renderer).setView(camera) + assertSame(renderer, it) + verify(renderer, never()).endInternal() + } + verify(renderer).endInternal() + } + + @Test + fun `should call beginRender and endRender with map boundary setView`() { + val renderer = mock() + val mat4 = Matrix4() + val mapBoundary = Rectangle(1f, 2f, 3f, 4f) + + renderer.use(mat4, mapBoundary) { + verify(renderer).beginInternal() + verify(renderer).setView(mat4, mapBoundary.x, mapBoundary.y, mapBoundary.width, mapBoundary.height) + assertSame(renderer, it) + verify(renderer, never()).endInternal() + } + verify(renderer).endInternal() + } + + @Test + fun `should call beginRender and endRender with map boundary rectangle setView`() { + val renderer = mock() + val mat4 = Matrix4() + val mapBoundary = Rectangle(1f, 2f, 3f, 4f) + + renderer.use(mat4, mapBoundary) { + verify(renderer).beginInternal() + verify(renderer).setView(mat4, mapBoundary.x, mapBoundary.y, mapBoundary.width, mapBoundary.height) + assertSame(renderer, it) + verify(renderer, never()).endInternal() + } + verify(renderer).endInternal() + } + +}