CameraController.java
package io.github.neonteam10;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
/**
* A class which holds the {@link Camera} object and implements the camera movement logic.
*/
public class CameraController extends InputAdapter {
private final OrthographicCamera camera;
private final Vector2 lastDragPosition;
private boolean isCurrentlyDragging = false;
private float desiredZoomLevel = 0.5f;
// Map boundaries for constraining the camera.
private float mapWidth;
private float mapHeight;
// Maximum zoom level to keep the whole map visible
private float maxZoomLevel;
public CameraController() {
camera = new OrthographicCamera();
camera.zoom = desiredZoomLevel;
lastDragPosition = new Vector2();
}
/**
* Set the map dimensions to constrain the camera.
*
* @param mapWidth the width of the map in pixels
* @param mapHeight the height of the map in pixels
*/
public void setMapDimensions(float mapWidth, float mapHeight) {
this.mapWidth = mapWidth;
this.mapHeight = mapHeight;
updateMaxZoomLevel();
}
/**
* Updates the camera's viewport size.
*
* @param width the new viewport width
* @param height the new viewport height
*/
public void setViewportDimensions(int width, int height) {
camera.viewportWidth = width;
camera.viewportHeight = height;
updateMaxZoomLevel();
}
@Override
public boolean scrolled(float amountX, float amountY) {
desiredZoomLevel += amountY * 0.03f;
return true;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
if (button == Input.Buttons.RIGHT) {
isCurrentlyDragging = true;
lastDragPosition.set(screenX, screenY);
return true;
}
return false;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
if (isCurrentlyDragging) {
isCurrentlyDragging = false;
return true;
}
return false;
}
@Override
public boolean touchDragged(int screenX, int screenY, int pointer) {
if (!isCurrentlyDragging) {
return false;
}
// Convert mouse movement into world space which allows movement of the camera whilst keeping the mouse cursor
// fixed relative to the same point in world space.
var mouseNow = camera.unproject(new Vector3(screenX, screenY, 0));
camera.translate(camera.unproject(new Vector3(lastDragPosition, 0)).sub(mouseNow));
lastDragPosition.set(screenX, screenY);
return true;
}
/**
* Handles any input events and updates the camera accordingly. Should be called once per frame.
*
* @param deltaTime the delta time between the last call of update
*/
public void update(float deltaTime) {
// Allow use of WSAD keys to move camera if not currently dragging the camera. Decide speed factor based on
// whether shift is currently held.
if (!isCurrentlyDragging) {
final var cameraMovementSpeed = Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) ? 250.0f : 100.0f;
final var cameraMovementDelta = deltaTime * cameraMovementSpeed;
if (Gdx.input.isKeyPressed(Input.Keys.W)) {
camera.translate(0.0f, cameraMovementDelta);
}
if (Gdx.input.isKeyPressed(Input.Keys.S)) {
camera.translate(0.0f, -cameraMovementDelta);
}
if (Gdx.input.isKeyPressed(Input.Keys.A)) {
camera.translate(-cameraMovementDelta, 0.0f);
}
if (Gdx.input.isKeyPressed(Input.Keys.D)) {
camera.translate(cameraMovementDelta, 0.0f);
}
}
camera.update();
// Handle camera zooming. The code below gives the effect of zooming into a point by keeping the mouse position
// constant relative to world space.
var mouseStart = camera.unproject(new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0));
// Clamp desired zoom level. Do this every frame in case the maximum zoom level gets updated.
desiredZoomLevel = MathUtils.clamp(desiredZoomLevel, 0.2f, maxZoomLevel);
// Update zoom level and call update so that the next call to unproject has updated values.
camera.zoom = Interpolation.linear.apply(camera.zoom, desiredZoomLevel, deltaTime * 12.0f);
camera.update();
// The difference in mouse position in world space before and after zooming is the amount we need to
// translate by to keep the mouse position constant.
var mouseEnd = camera.unproject(new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0));
camera.translate(mouseStart.sub(mouseEnd));
// Clamp camera position to the map boundaries.
var halfViewport = new Vector2(camera.viewportWidth, camera.viewportHeight).scl(camera.zoom / 2.0f);
camera.position.x = MathUtils.clamp(camera.position.x, halfViewport.x, mapWidth - halfViewport.x);
camera.position.y = MathUtils.clamp(camera.position.y, halfViewport.y, mapHeight - halfViewport.y);
// Final camera update with the new position.
camera.update();
}
/**
* Updates the maximum zoom level to ensure the whole map fits within the viewport.
*/
private void updateMaxZoomLevel() {
// Calculate the maximum zoom level based on the aspect ratio and map size.
float viewportAspectRatio = camera.viewportWidth / camera.viewportHeight;
float mapAspectRatio = mapWidth / mapHeight;
// Set maxZoomLevel based on the smallest fitting value for width or height.
if (viewportAspectRatio < mapAspectRatio) {
// Taller aspect ratio: limit based on map height.
maxZoomLevel = mapHeight / camera.viewportHeight;
} else {
// Wider aspect ratio: limit based on map width.
maxZoomLevel = mapWidth / camera.viewportWidth;
}
}
/**
* @return the orthographic camera controlled by this class
*/
public OrthographicCamera getCamera() {
return camera;
}
/**
* @return whether the user is currently panning the camera with the mouse
*/
public boolean isPanning() {
return isCurrentlyDragging;
}
/**
* @return whether the camera is currently zooming in
*/
public boolean isZoomingIn() {
return (desiredZoomLevel - camera.zoom) < -0.01f;
}
/**
* @return whether the camera is currently zooming out
*/
public boolean isZoomingOut() {
return (desiredZoomLevel - camera.zoom) > 0.01f;
}
}