Basically I want to stop the camera from moving in subpixels, as I think this leads to sprites visibly changing their dimensions if just ever so slightly. (Is there a better term for that?) Note that this is a pixel-art game where I want to have crisp pixelated graphics. Here's a gif that shows the problem:
Now what I tried was this: Move the camera, project the current position (so it's screen coordinates) and then round or cast to int. After that convert it back to world coordinates and use that as the new camera position. As far as I know, this should lock the camera to actual screen coordinates, not fractions thereof.
For some reason, however, the y
value of the new position just explodes. In a matter of seconds it increases to something like 334756315000
.
Here is a SSCCE (or an MCVE) based on the code in the LibGDX wiki:
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
public class PixelMoveCameraTest implements ApplicationListener {
static final int WORLD_WIDTH = 100;
static final int WORLD_HEIGHT = 100;
private OrthographicCamera cam;
private SpriteBatch batch;
private Sprite mapSprite;
private float rotationSpeed;
private Viewport viewport;
private Sprite playerSprite;
private Vector3 newCamPosition;
@Override
public void create() {
rotationSpeed = 0.5f;
playerSprite = new Sprite(new Texture("/path/to/dungeon_guy.png"));
playerSprite.setSize(1f, 1f);
mapSprite = new Sprite(new Texture("/path/to/sc_map.jpg"));
mapSprite.setPosition(0, 0);
mapSprite.setSize(WORLD_WIDTH, WORLD_HEIGHT);
float w = Gdx.graphics.getWidth();
float h = Gdx.graphics.getHeight();
// Constructs a new OrthographicCamera, using the given viewport width and height
// Height is multiplied by aspect ratio.
cam = new OrthographicCamera();
cam.position.set(0, 0, 0);
cam.update();
newCamPosition = cam.position.cpy();
viewport = new ExtendViewport(32, 20, cam);
batch = new SpriteBatch();
}
@Override
public void render() {
handleInput();
cam.update();
batch.setProjectionMatrix(cam.combined);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();
mapSprite.draw(batch);
playerSprite.draw(batch);
batch.end();
}
private static float MOVEMENT_SPEED = 0.2f;
private void handleInput() {
if (Gdx.input.isKeyPressed(Input.Keys.A)) {
cam.zoom += 0.02;
}
if (Gdx.input.isKeyPressed(Input.Keys.Q)) {
cam.zoom -= 0.02;
}
if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
newCamPosition.add(-MOVEMENT_SPEED, 0, 0);
}
if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
newCamPosition.add(MOVEMENT_SPEED, 0, 0);
}
if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
newCamPosition.add(0, -MOVEMENT_SPEED, 0);
}
if (Gdx.input.isKeyPressed(Input.Keys.UP)) {
newCamPosition.add(0, MOVEMENT_SPEED, 0);
}
if (Gdx.input.isKeyPressed(Input.Keys.W)) {
cam.rotate(-rotationSpeed, 0, 0, 1);
}
if (Gdx.input.isKeyPressed(Input.Keys.E)) {
cam.rotate(rotationSpeed, 0, 0, 1);
}
cam.zoom = MathUtils.clamp(cam.zoom, 0.1f, 100 / cam.viewportWidth);
float effectiveViewportWidth = cam.viewportWidth * cam.zoom;
float effectiveViewportHeight = cam.viewportHeight * cam.zoom;
cam.position.lerp(newCamPosition, 0.02f);
cam.position.x = MathUtils.clamp(cam.position.x,
effectiveViewportWidth / 2f, 100 - effectiveViewportWidth / 2f);
cam.position.y = MathUtils.clamp(cam.position.y,
effectiveViewportHeight / 2f, 100 - effectiveViewportHeight / 2f);
// if this is false, the "bug" (y increasing a lot) doesn't appear
if (true) {
Vector3 v = viewport.project(cam.position.cpy());
System.out.println(v);
v = viewport.unproject(new Vector3((int) v.x, (int) v.y, v.z));
cam.position.set(v);
}
playerSprite.setPosition(newCamPosition.x, newCamPosition.y);
}
@Override
public void resize(int width, int height) {
viewport.update(width, height);
}
@Override
public void resume() {
}
@Override
public void dispose() {
mapSprite.getTexture().dispose();
batch.dispose();
}
@Override
public void pause() {
}
public static void main(String[] args) {
new Lwjgl3Application(new PixelMoveCameraTest(), new Lwjgl3ApplicationConfiguration());
}
}
and here's the sc_map.jpg
and the dungeon_guy.png
I'd also be interested to learn about simpler and/or better ways to fix this problem.