LibGDX keep camera within bounds of TiledMap

I have a simple TiledMap which I can render fine. I have a player hopping (with Box2D) around and my camera follows the player around:

cam.position.set(
player.position().x * Game.PPM + Game.V_WIDTH / 4,
player.position().y * Game.PPM,
0
);
cam.update();


However, the camera will move "off" the TiledMap. How could I keep my camera on the TiledMap, so when I near the edges of my map, the camera stops scrolling and the player moves towards the edge of the camera?

Alright, so you're working with two rectangles here. A larger static one (the map) and a smaller moving one (the camera) inside of it. What you want is to not let the bounds of the smaller rectangle move outside the inner bounds of the larger rectangle.

// These values likely need to be scaled according to your world coordinates.
// The left boundary of the map (x)
int mapLeft = 0;
// The right boundary of the map (x + width)
int mapRight = 0 + map.getWidth();
// The bottom boundary of the map (y)
int mapBottom = 0;
// The top boundary of the map (y + height)
int mapTop = 0 + map.getHeight();
// The camera dimensions, halved
float cameraHalfWidth = cam.viewportWidth * .5f;
float cameraHalfHeight = cam.viewportHeight * .5f;

// Move camera after player as normal

float cameraLeft = cam.position.x - cameraHalfWidth;
float cameraRight = cam.position.x + cameraHalfWidth;
float cameraBottom = cam.position.y - cameraHalfHeight;
float cameraTop = cam.position.y + cameraHalfHeight;

// Horizontal axis
if(map.getWidth() < cam.viewportWidth)
{
cam.position.x = mapRight / 2;
}
else if(cameraLeft <= mapLeft)
{
cam.position.x = mapLeft + cameraHalfWidth;
}
else if(cameraRight >= mapRight)
{
cam.position.x = mapRight - cameraHalfWidth;
}

// Vertical axis
if(map.getHeight() < cam.viewportHeight)
{
cam.position.y = mapTop / 2;
}
else if(cameraBottom <= mapBottom)
{
cam.position.y = mapBottom + cameraHalfHeight;
}
else if(cameraTop >= mapTop)
{
cam.position.y = mapTop - cameraHalfHeight;
}


So the logic is pretty simple. Keep the small box inside the larger box. Once you understand that idea feel free to collapse that code down. You could even move it into a series of nested Min/Max statements in your camera position tracking if you'd prefer.

• I should go to bed earlier. I had something like this implemented, but couldn't get it to work. But instead of calling a nice setPosition() method, that would fix the bounds, I was still setting the cam position directly (e.g. cam.position.set()) This works like a charm! Thanks for the explanation! – Ariejan May 13 '14 at 5:42

You can easily clamp the camera position to the map boundaries like this:

camera.position.x = MathUtils.clamp(camera.position.x, camViewportHalfX, mapWidth - camViewportHalfX);
camera.position.y = MathUtils.clamp(camera.position.y, camViewportHalfY, mapHeight - camViewportHalfY);

• What is camViewportHalfX and camViewportHalfY? – Cypher Jun 23 '16 at 2:43
• @Cypher: It's half the size of the viewport X and Y axes. – Matthias Jun 23 '16 at 8:03
• So camViewportHalfX would be the equivalent to camera.viewportWidth / 2? – Cypher Jun 23 '16 at 21:29

To move Camera in TiledMap bounds, OrthogonalTiledMapRenderer was used.

I have also noticed it behaves unexpectedly: while Camera reaches map bounds, tiled map, like by inertia, moves some pixels too far (it depends on the velocity of the swipe).

As a solution, on each Camera movement, Camera is forcibly put in map bounds. See putInMapBounds() method.

To avoid glitches in TiledMap rendering, used Math.min(float, float).

Use this listener to handle your Camera:

/**
* @author Gram <gram7gram@gmail.com>
*/
public class CameraListener extends InputAdapter {

private final UIStage stage;
private final Camera camera;
private final Vector3 curr;
private final Vector3 last;
private final Vector3 delta;
private final int mapWidth;
private final int mapHeight;

public CameraListener(UIStage stage) {
this.stage = stage;
this.camera = stage.getViewport().getCamera();

curr = new Vector3();
last = new Vector3(-1, -1, -1);
delta = new Vector3();

TiledMapTileLayer layer = stage.getLevel().getMap().getFirstLayer();
mapWidth = layer.getWidth() * DDGame.TILE_HEIGHT;
mapHeight = layer.getHeight() * DDGame.TILE_HEIGHT;
}

@Override
public boolean touchDragged(int x, int y, int pointer) {

camera.unproject(curr.set(x, y, 0));

if (!(last.x == -1 && last.y == -1 && last.z == -1)) {
camera.unproject(delta.set(last.x, last.y, 0));
delta.sub(curr);
camera.translate(Math.min(delta.x, 5), Math.min(delta.y, 5), 0);
if (isInMapBounds()) {
stage.moveBy(Math.min(delta.x, 5), Math.min(delta.y, 5));
}
}

last.set(x, y, 0);

putInMapBounds();

return false;
}

private boolean isInMapBounds() {

return camera.position.x >= camera.viewportWidth / 2f
&& camera.position.x <= mapWidth - camera.viewportWidth / 2f
&& camera.position.y >= camera.viewportHeight / 2f
&& camera.position.y <= mapHeight - camera.viewportHeight / 2f;

}

private void putInMapBounds() {

if (camera.position.x < camera.viewportWidth / 2f)
camera.position.x = camera.viewportWidth / 2f;
else if (camera.position.x > mapWidth - camera.viewportWidth / 2f)
camera.position.x = mapWidth - camera.viewportWidth / 2f;

if (camera.position.y < camera.viewportHeight / 2f)
camera.position.y = camera.viewportHeight / 2f;
else if (camera.position.y > mapHeight - camera.viewportHeight / 2f)
camera.position.y = mapHeight - camera.viewportHeight / 2f;

stage.moveTo(
camera.position.x,
camera.position.y);

}

@Override
public boolean touchUp(int x, int y, int pointer, int button) {
last.set(-1, -1, -1);
Log.info("Camera at " + camera.position.x + ":" + camera.position.y);
return false;
}
}


If you have the zoom factor to take care of, I found this to work much better:

public void fixBounds() {
float scaledViewportWidthHalfExtent = viewportWidth * zoom * 0.5f;
float scaledViewportHeightHalfExtent = viewportHeight * zoom * 0.5f;

// Horizontal
if (position.x < scaledViewportWidthHalfExtent)
position.x = scaledViewportWidthHalfExtent;
else if (position.x > xmax - scaledViewportWidthHalfExtent)
position.x = xmax - scaledViewportWidthHalfExtent;

// Vertical
if (position.y < scaledViewportHeightHalfExtent)
position.y = scaledViewportHeightHalfExtent;
else if (position.y > ymax - scaledViewportHeightHalfExtent)
position.y = ymax - scaledViewportHeightHalfExtent;
}