Skip to content

Commit

Permalink
Added camera utilities. #183
Browse files Browse the repository at this point in the history
  • Loading branch information
czyzby committed Mar 26, 2020
1 parent bca5540 commit 8050af3
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
- **[FEATURE]** (`ktx-async`) Added `RenderingScope` factory function for custom scopes using rendering thread dispatcher.
- **[FEATURE]** (`ktx-graphics`) Added `LetterboxingViewport` from `ktx-app`.
- **[FEATURE]** (`ktx-graphics`) Added `takeScreenshot` utility function that allows to save a screenshot of the application.
- **[FEATURE]** (`ktx-graphics`) Added `Camera` utilities.
- `center` extension method allows to center the camera's position to screen center or the center of the chosen rectangle.
- `moveTo` extension method allows to move the camera immediately at the chosen target position with optional offset.
- `lerpTo` extension method allows to move the camera smoothly to the chosen target position with optional offset.
- `update` inlined extension method allows to change camera state with automatic `Camera.update` call.
- **[FEATURE]** (`ktx-math`) Added `lerp` and `interpolate` extension functions for `Float` ranges.
- **[FEATURE]** (`ktx-preferences`) Added a new KTX module: Preferences API extensions.
- Added `set` operators for `String`, `Int`, `Float`, `Double`, `Long`, `Boolean`, `Pair<String, Any>` and `Any`
Expand Down
53 changes: 53 additions & 0 deletions graphics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ and `end()` calls.

#### Cameras and viewports

- `Camera.center` extension method allows to center the camera's position to screen center or the center of the chosen rectangle.
- `Camera.moveTo` extension method allows to move the camera immediately at the chosen target position with optional offset.
- `Camera.lerpTo` extension method allows to move the camera smoothly to the chosen target position with optional offset.
- `Camera.update` inlined extension method allows to change camera state with automatic `Camera.update` call.
- `LetterboxingViewport` combines `ScreenViewport` and `FitViewport` behavior: it targets a specific aspect ratio and
applies letterboxing like `FitViewport`, but it does not scale rendered objects when resized, keeping them in fixed size
similarly to `ScreenViewport`. Thanks to customizable target PPI value, it is ideal for GUIs and can easily support
Expand Down Expand Up @@ -196,6 +200,54 @@ class Application: ApplicationAdapter() {
}
```

Centering camera position:

```Kotlin
import com.badlogic.gdx.graphics.OrthographicCamera
import ktx.graphics.center

fun centerCamera(camera: OrthographicCamera) {
// Sets position to the middle of the screen:
camera.center()

// Sets position to the middle of the chosen rectangle:
camera.center(x = 100f, y = 100f, width = 800f, height = 800f)
}
```

Moving the camera to a target:

```Kotlin
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.math.Vector2
import ktx.graphics.lerpTo
import ktx.graphics.moveTo

fun moveCamera(camera: OrthographicCamera, target: Vector2) {
// Moves the camera immediately at the target:
camera.moveTo(target)

// Moves the camera smoothly to the target:
camera.lerpTo(target, lerp = 0.1f)
}
```

Changing the camera state with automatic update:

```Kotlin
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.math.Vector2
import ktx.graphics.lerpTo
import ktx.graphics.update

fun moveCamera(camera: OrthographicCamera, target: Vector2) {
camera.update {
lerpTo(target, lerp = 0.1f)
// camera.update() will be called automatically after this block.
}
}
```

#### Synergy

Use [`ktx-math`](../math) for `Vector2` and `Vector3` extensions, including idiomatic Kotlin factory
Expand All @@ -207,6 +259,7 @@ There are some general purpose LibGDX utility libraries out there, but most lack

- [Kiwi](https://github.com/czyzby/gdx-lml/tree/master/kiwi) is a general purpose Guava-inspired LibGDX Java utilities
library with some utilities similar to `ktx-graphics`.
- [Cyberpunk](https://github.com/ImXico/Cyberpunk) framework provides similar utilities for cameras and screenshots.

#### Additional documentation

Expand Down
54 changes: 54 additions & 0 deletions graphics/src/main/kotlin/ktx/graphics/camera.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,63 @@ package ktx.graphics
import com.badlogic.gdx.Application.ApplicationType.Android
import com.badlogic.gdx.Application.ApplicationType.iOS
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Camera
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Scaling
import com.badlogic.gdx.utils.viewport.ScalingViewport

/**
* Centers this [Camera] in the middle of the given rectangle. [width] and [height]
* represent the size of the rectangle in world (camera/viewport) units.
* [x] and [y] are optional offsets in world units that will modify camera position.
*
* Note that [Camera.update] should be called in order to update to the new position.
*/
fun Camera.center(
width: Float = Gdx.graphics.width.toFloat(),
height: Float = Gdx.graphics.height.toFloat(),
x: Float = 0f, y: Float = 0f) {
position.set(x + width / 2f, y + height / 2f, 0f)
}

/**
* Immediately moves [Camera] to the selected [target] coordinates.
* Not suitable for all cases, as it may cause a very rough motion when used every frame.
* [x] and [y] are optional offsets in world units that will modify camera position.
*
* Note that [Camera.update] should be called in order to update to the new position.
*/
fun Camera.moveTo(target: Vector2, x: Float = 0f, y: Float = 0f) {
position.set(target.x + x, target.y + y, 0f)
}

/**
* Will smoothly move the [Camera] at the selected [target] coordinated.
* The [Camera] will follow a [target] with a smooth linear interpolation, based on a [lerp] amount.
* [lerp] should be in range of 0 (slowest) to 1 (fastest).
* The lower that interpolation amount, the smoother - and thus slower - the following motion.
*
* [x] and [y] are optional offsets in world units that will modify the final camera position.
* Offsets are added as-is and are not interpolated.
*
* Note that [Camera.update] should be called in order to update to the new position.
* [moveTo] can be used to set the initial position while honoring the same [x] and [y] offset.
*/
fun Camera.lerpTo(target: Vector2, lerp: Float, x: Float = 0f, y: Float = 0f) {
val newX = this.position.x + ((target.x - position.x) * lerp) + x
val newY = this.position.y + ((target.y - position.y) * lerp) + y
position.set(newX, newY, 0f)
}

/**
* Inlines the [operation], which can update the camera position.
* Automatically calls [Camera.update] after the [operation] is finished.
*/
inline fun Camera.update(operation: Camera.() -> Unit) {
operation()
update()
}

/**
* Combines [screen][com.badlogic.gdx.utils.viewport.ScreenViewport] and
* [fit][com.badlogic.gdx.utils.viewport.FitViewport] viewports. Similarly to ScreenViewport, world size is changed on
Expand Down
99 changes: 97 additions & 2 deletions graphics/src/test/kotlin/ktx/graphics/cameraTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,107 @@ package ktx.graphics

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.lwjgl.LwjglNativesLoader
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.math.Vector3
import com.nhaarman.mockitokotlin2.*
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class CameraUtilitiesTest {
@Before
fun `mock graphics`() {
LwjglNativesLoader.load()
Gdx.graphics = mock {
on { width } doReturn 800
on { height } doReturn 600
}
}

@Test
fun `should center camera to screen center`() {
val camera = OrthographicCamera()

camera.center()
camera.update()

assertEquals(Vector3(400f, 300f, 0f), camera.position)
}

@Test
fun `should center camera to rectangle center with offset`() {
val camera = OrthographicCamera()

camera.center(x = 100f, y = 50f, width = 1000f, height = 500f)
camera.update()

assertEquals(Vector3(600f, 300f, 0f), camera.position)
}

@Test
fun `should immediately move camera to target`() {
val target = Vector2(100f, 200f)
val camera = OrthographicCamera()

camera.moveTo(target)
camera.update()

assertEquals(Vector3(100f, 200f, 0f), camera.position)
}

@Test
fun `should immediately move camera to target with offset`() {
val target = Vector2(100f, 200f)
val camera = OrthographicCamera()

camera.moveTo(target, x = 50f, y = -25f)
camera.update()

assertEquals(Vector3(150f, 175f, 0f), camera.position)
}

@Test
fun `should smoothly move the camera`() {
val target = Vector2(200f, 400f)
val camera = OrthographicCamera()
camera.position.set(100f, 100f, 0f)
camera.update()

camera.lerpTo(target, lerp = 0.5f)
camera.update()

assertEquals(Vector3(150f, 250f, 0f), camera.position)
}

@Test
fun `should smoothly move the camera with non-interpolated offsets`() {
val target = Vector2(200f, 400f)
val camera = OrthographicCamera()
camera.position.set(100f, 100f, 0f)
camera.update()

camera.lerpTo(target, lerp = 0.5f, x = -25f, y = 100f)
camera.update()

assertEquals(Vector3(125f, 350f, 0f), camera.position)
}

@Test
fun `should update camera automatically`() {
val camera = spy(OrthographicCamera())
var operationCalls = 0

camera.update {
operationCalls++
verify(camera, never()).update()
}

assertEquals(1, operationCalls)
verify(camera, times(1)).update()
}
}

/**
* Tests [LetterboxingViewport] class, which merges ScreenViewport and FitViewport behavior.
*/
Expand Down

0 comments on commit 8050af3

Please sign in to comment.