1
votes

I am almost done with my first libgdx 2d game, only this is missing. Currently I have my main GameScreen, which contains 3 stages (rendered in this order):

  • GameStage

Contains game actors, map, etc. Basically the game itself.

  • InfoStage

All controls and information about the game, so called "hud".

  • DialogStage

For displaying dialogs so that they are on top of everything.

I am using single OrthographicCamera and viewport of size necessary to display whole map and FitViewport to make sure everything works on resize too or if ratio changes.

Now I would like to make changes so that InfoStage and DialogStage stay pretty much like this - fixed on screen and allow GameStage zooming and pan to move around.

Firstly what does that mean for viewport and camera? Does that mean I will have camera+viewport pair for InfoStage and DialogStage and different camera + viewport pair for GameStage?

Second and most importantly - how to implement zoom and pan for this stage? I've seen some similar questions, but implementations haven't really worked for me and I also didn't like them. It feels like there could be simple separated class to do this - maybe GestureListener or EventListener/InputProcessor with passed Camera/Viewport to do all the work automatically. I found CameraInputController which does something similar so I guess that can be a bit of lead. Has anyone implemented this component? If not, what would you advise to do?

1
It would be very weird to be using the same camera for your game and your UI. Use an ExtendViewport for your game so it can use the whole screen. I haven't implemented gesture zooms for a camera. Since you're using Stage for your game, you're stuck using a Viewport rather than just a Camera. I would start by wrapping an ExtendViewport with a class that can adjust the viewport's world width and height based on some zoom parameter. Zooming closer means shrinking the world size of the viewport. - Tenfour04
I agree there should be more cameras, what do you mean stuck because of using Stage? What's alternative? I am also posting class that is handling pan/zoom. - K.H.
Stage requires you to use Viewport. If you don’t use Stage for your game you have the freedom to manage the camera without conforming to how Viewport works. That’s all I mean. It’s still achievable either way. - Tenfour04

1 Answers

0
votes

So far I made GestureListener, which handles zoom and pan, making sure you never zoom out too far and you can never pan out of map, works pretty smooth, but probably doesn't cover all the possibilities like if y axis is rotated. If you can call fitBounds separately after resize so that world clips in case it needs. I will try to update class as I get it cleaner and more flexible. Pretty sure this scale() method has to have build-in alternative in engine. Another thing I added is ScrollInputListener, which can listen to mouse scroll and perform zoom.

Just add this class wrapped into GestureDetector and add to your input multiplexer. I am using it with FillViewport.

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.input.GestureDetector
import com.badlogic.gdx.utils.viewport.Viewport
class ScrollInputListener(
        val camera: OrthographicCamera,
        val panGestureListener: PanGestureListener
) : InputAdapter() {


    fun amountToZoom(amount: Int): Float {
        return (1.0f + amount.toFloat() / 10f)
    }

    override fun scrolled(amount: Int): Boolean {
        val newZoom = camera.zoom * amountToZoom(amount)
        return panGestureListener.doZoomIfAllowed(newZoom)
    }
}


class CameraMovement(val x: Float, val y: Float) {
    private fun shouldMove(): Boolean {
        return  x!= 0f || y != 0f
    }

    fun move(camera: OrthographicCamera, scale: Float): Boolean {
        if (shouldMove()) {
            camera.translate(-x / scale, y / scale)
            return true
        }
        return false
    }
}

val Viewport.targetRatio: Float
    get() {
        return Gdx.graphics.height.toFloat() / this.worldHeight.toFloat()
    }

val Viewport.sourceRatio: Float
    get() {
        return Gdx.graphics.width.toFloat() / this.worldWidth.toFloat()
    }

fun Viewport.scale(): Float {
    return if (targetRatio < sourceRatio) Gdx.graphics.width.toFloat() / this.worldWidth.toFloat() else Gdx.graphics.height.toFloat() / this.worldHeight.toFloat()
}

class PanGestureListener(
        val camera: OrthographicCamera,
        val viewport: Viewport,
        val minWorldRatio: Double = 0.5
) : GestureDetector.GestureAdapter() {

    override fun pan(x: Float, y: Float, deltaX: Float, deltaY: Float): Boolean {
        val scale = viewport.scale()
        return fitBounds(scale, deltaX, deltaY).move(camera, scale)
    }

    fun fitBounds(scale: Float = viewport.scale(), deltaX: Float = 0f, deltaY: Float = 0f): CameraMovement {
        val mapLeft = 0
        val mapRight = viewport.worldWidth / camera.zoom
        val mapTop = 0
        val mapBottom = viewport.worldHeight / camera.zoom

        val cameraHalfWidth = if (viewport.targetRatio < viewport.sourceRatio) {
            camera.viewportWidth / 2f
        } else {
            camera.viewportWidth / 2f * (viewport.sourceRatio / viewport.targetRatio)
        }
        val cameraHalfHeight = if (viewport.targetRatio < viewport.sourceRatio) {
            camera.viewportHeight / 2f * (viewport.targetRatio / viewport.sourceRatio)
        } else {
            camera.viewportHeight / 2f
        }
        var xMovement = deltaX
        var yMovement = deltaY

        if ((camera.position.x - deltaX / scale) / camera.zoom - cameraHalfWidth < mapLeft) {
            camera.position.x = (mapLeft + cameraHalfWidth) * camera.zoom
            xMovement = 0f
        }
        if ((camera.position.x - deltaX / scale) / camera.zoom + cameraHalfWidth > mapRight) {
            camera.position.x = (mapRight - cameraHalfWidth) * camera.zoom
            xMovement = 0f
        }

        if ((camera.position.y + deltaY / scale) / camera.zoom - cameraHalfHeight < mapTop) {
            camera.position.y = (mapTop + cameraHalfHeight) * camera.zoom
            yMovement = 0f
        }
        if ((camera.position.y + deltaY / scale) / camera.zoom + cameraHalfHeight > mapBottom) {
            camera.position.y = (mapBottom - cameraHalfHeight) * camera.zoom
            yMovement = 0f
        }
        return CameraMovement(xMovement, yMovement)
    }

    var initialZoom = camera.zoom

    internal fun checkZoom(newZoom: Float, scale: Float = viewport.scale()): Boolean {
        if (
                viewport.worldWidth * scale * newZoom > Gdx.graphics.width &&
                viewport.worldHeight * scale * newZoom > Gdx.graphics.height
        ) {
            return false
        }
        if (
                viewport.worldWidth * scale * newZoom < Gdx.graphics.width * minWorldRatio &&
                viewport.worldHeight * scale * newZoom < Gdx.graphics.height * minWorldRatio
        ) {
            return false
        }
        return true
    }

    override fun zoom(initialDistance: Float, distance: Float): Boolean {
        val newZoom = initialZoom * initialDistance / distance
        val scale = viewport.scale()
        return doZoomIfAllowed(newZoom, scale)
    }

    fun doZoomIfAllowed(newZoom: Float, scale: Float = viewport.scale()): Boolean {
        if (checkZoom(newZoom = newZoom, scale = scale)) {
            camera.zoom = newZoom
            fitBounds(scale = scale)
            return true
        }
        return false
    }

    override fun touchDown(x: Float, y: Float, pointer: Int, button: Int): Boolean {
        initialZoom = camera.zoom
        return super.touchDown(x, y, pointer, button)
    }
}