0
\$\begingroup\$

So, I'm making a platformer game from scratch in Java and I am trying to make the camera movement smoother. Here is what it currently is

public class Camera {
    private int x, y;

    public Camera(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void tick (GameObject player) {
        setX((int) (-player.getX() + Reference.WIDTH / 2 + 16));
        setY((int) (-player.getY() + Reference.HEIGHT / 2 + 64));
    }
}

It moves too strictly and it always centres the player to the screen, I don't like it.

I tried watching this video, but it's in GameMaker. Any way to translate this to Java? https://youtu.be/6cIN6iW5ois

Any help would be appreciated, thanks! :)

\$\endgroup\$
  • \$\begingroup\$ I fixed the link, that's what I want. But it's in GameMaker \$\endgroup\$ – ShivGames Mar 17 '17 at 6:55
4
\$\begingroup\$

Currently you have:

setX((int) (-player.getX() + Reference.WIDTH / 2 + 16));
setY((int) (-player.getY() + Reference.HEIGHT / 2 + 64));

This should be placing the player in the center of the view (perhaps offset by some constant... I do not know if the coordinates are from the center or the corner, I do not know how big the player is either... it does not really matters).

We will take these values you have and store them as tweening easing target:

int targetX = (int) (-player.getX() + Reference.WIDTH / 2 + 16);
int targetY = (int) (-player.getY() + Reference.HEIGHT / 2 + 64);
setX(targetX);
setY(targetY);

Yes, that does the same as before, it is just refactoring. We know have to apply the tweening algorithm linear interpolation (a.k.a. lerp):

x += (target - x) * CONSTANT

That is the same as:

x = x + (target - x) * CONSTANT

Using the value 0.1 for the constant in your code, it ends up as follows (since we have to use the current value twice, I will take it as local variables too):

int targetX = (int) (-player.getX() + Reference.WIDTH / 2 + 16);
int targetY = (int) (-player.getY() + Reference.HEIGHT / 2 + 64);
int x = getX();
int y = getY();
setX(x + (targetX - x) * 0.1);
setY(y + (targetY - y) * 0.1);

Alternatively, you can tell java to stop being in the way:

int targetX = (int) (-player.getX() + Reference.WIDTH / 2 + 16);
int targetY = (int) (-player.getY() + Reference.HEIGHT / 2 + 64);
x += (targetX - x) * 0.1;
y += (targetY - y) * 0.1;

The result is what Itay Karen calls lerp-smoothing in the article Scroll Back: The Theory and Practice of Cameras in Side-Scrollers.

As suggested in the video you linked, experiment with the constant factor to get the effect you want. If you decide to change the constant for a function of the difference of target and the current value... then it is no longer a linear interpolation, but it is still easing.

I will also suggest looking into edge-snapping, it may fit your game. It is implemented as a clamp that prevent the camera to show beyond the edge of the world.


Demo without edge-snapping:

var time = (new Date()).getTime(); // time in milliseconds
var speed = 50;
var player = {x: 8, y:50, width:16, height:64};
var camera = {x: 0, y:32, width:100, height:100};
var world = {width: 200, height:200};

function update()
{
  // get current time
  let new_time = (new Date()).getTime();
  let elapsed = (new_time - time) / 1000.0;
  
  // moving the player
  
  player.x = player.x + speed * elapsed;
  if (player.x < 0)
  {
    player.x = 0
    speed = -speed;
  }
  if (player.x > world.width - player.width)
  {
    player.x = world.width - player.width;
    speed = -speed;
  }
  player.y = 50;
  
  // move the camera
  
  // note: since I'm moving a "camera" object, instead of computing a transformation
  // the target has reversed sign.
  
  var targetX = player.x - camera.width/2 + player.width/2;
  var targetY = player.y - camera.height/2 + player.height/2;
  camera.x += (targetX - camera.x) * 0.1;
  camera.y += (targetY - camera.y) * 0.1;
  
  // update the elements
  var playerElement = document.getElementById('player');
  playerElement.style.left = player.x + "px";
  playerElement.style.top = player.y + "px";
  var cameraElement = document.getElementById('camera');
  cameraElement.style.left = camera.x + "px";
  cameraElement.style.top = camera.y + "px";
  
  // update time
  
  time = new_time;
}
setInterval(update, 40);
body,div{margin:0px}
#camera{border:1px solid blue; position:absolute; top:32px; width:100px;height:100px;}
#player{background:green; position:absolute; top:50px; width:16px;height:64px;}
#world{border:1px solid red; position:absolute; width:200px;height:200px;}
<div id="camera"></div>
<div id="player"></div>
<div id="world"></div>


Demo with with-snapping on target:

var time = (new Date()).getTime(); // time in milliseconds
var speed = 50;
var player = {x: 8, y:50, width:16, height:64};
var camera = {x: 0, y:32, width:100, height:100};
var world = {width: 200, height:200};

function update()
{
  // get current time
  let new_time = (new Date()).getTime();
  let elapsed = (new_time - time) / 1000.0;
  
  // moving the player
  
  player.x = player.x + speed * elapsed;
  if (player.x < 0)
  {
    player.x = 0;
    speed = -speed;
  }
  if (player.x > world.width - player.width)
  {
    player.x = world.width - player.width;
    speed = -speed;
  }
  player.y = 50;
  
  // move the camera
  
  // note: since I'm moving a "camera" object, instead of computing a transformation
  // the target has reversed sign.
  
  var targetX = player.x - camera.width/2 + player.width/2;
  var targetY = player.y - camera.height/2 + player.height/2;
  
  targetX = Math.min(world.width - camera.width, Math.max(0, targetX));
  targetY = Math.min(world.height - camera.height, Math.max(0, targetY));
  
  camera.x += (targetX - camera.x) * 0.1;
  camera.y += (targetY - camera.y) * 0.1;
  
  // update the elements
  var playerElement = document.getElementById('player');
  playerElement.style.left = player.x + "px";
  playerElement.style.top = player.y + "px";
  var cameraElement = document.getElementById('camera');
  cameraElement.style.left = camera.x + "px";
  cameraElement.style.top = camera.y + "px";
  
  // update time
  
  time = new_time;
}
setInterval(update, 40);
body,div{margin:0px}
#camera{border:1px solid blue; position:absolute; top:32px; width:100px;height:100px;}
#player{background:green; position:absolute; top:50px; width:16px;height:64px;}
#world{border:1px solid red; position:absolute; width:200px;height:200px;}
<div id="camera"></div>
<div id="player"></div>
<div id="world"></div>


Demo with with-snapping on camera:

var time = (new Date()).getTime(); // time in milliseconds
var speed = 50;
var player = {x: 8, y:50, width:16, height:64};
var camera = {x: 0, y:32, width:100, height:100};
var world = {width: 200, height:200};

function update()
{
  // get current time
  let new_time = (new Date()).getTime();
  let elapsed = (new_time - time) / 1000.0;
  
  // moving the player
  
  player.x = player.x + speed * elapsed;
  if (player.x < 0)
  {
    player.x = 0;
    speed = -speed;
  }
  if (player.x > world.width - player.width)
  {
    player.x = world.width - player.width;
    speed = -speed;
  }
  player.y = 50;
  
  // move the camera
  
  // note: since I'm moving a "camera" object, instead of computing a transformation
  // the target has reversed sign.
  
  var targetX = player.x - camera.width/2 + player.width/2;
  var targetY = player.y - camera.height/2 + player.height/2;
  camera.x += (targetX - camera.x) * 0.1;
  camera.y += (targetY - camera.y) * 0.1;
  
  camera.x = Math.min(world.width - camera.width, Math.max(0, camera.x));
  camera.y = Math.min(world.height - camera.height, Math.max(0, camera.y));
  
  // update the elements
  var playerElement = document.getElementById('player');
  playerElement.style.left = player.x + "px";
  playerElement.style.top = player.y + "px";
  var cameraElement = document.getElementById('camera');
  cameraElement.style.left = camera.x + "px";
  cameraElement.style.top = camera.y + "px";
  
  // update time
  
  time = new_time;
}
setInterval(update, 40);
body,div{margin:0px}
#camera{border:1px solid blue; position:absolute; top:32px; width:100px;height:100px;}
#player{background:green; position:absolute; top:50px; width:16px;height:64px;}
#world{border:1px solid red; position:absolute; width:200px;height:200px;}
<div id="camera"></div>
<div id="player"></div>
<div id="world"></div>


Note on terminology

The terms "position-locking", "edge-snapping", "lerp-smoothing" are from Itay Karen and are described in the linked article Scroll Back: The Theory and Practice of Cameras in Side-Scrollers.

Referring to this as tweening is not wrong per-se. Yet tweening is a term used in key frame animation, and if you look for how to do tweening, you will get tutorials on using animation tools. So, instead look for how to implement easing.

However, avoid quantitative easing, that is economy / finance / monetary policy lingo meaning that the central bank will buy financial assets in such way that it causes a smooth inscrease in money supply and the prices of financial assets (see: yield curve)... while that is also easing, you will not get code from articles on monetary policy.

\$\endgroup\$
  • \$\begingroup\$ Thanks! That worked! But how do I do that clamping thing. What should I set the max and min value to? \$\endgroup\$ – ShivGames Mar 17 '17 at 16:32
  • \$\begingroup\$ @ShivGames for the horizontal you would clamp to min: Reference.WIDTH, max: width_of_world - Reference.WIDTH. and height instead of width for the vertical. Edit: you may need some offset, I'm not sure, but start at that. \$\endgroup\$ – Theraot Mar 17 '17 at 16:35
  • \$\begingroup\$ but that doesn't seem to work for me!! \$\endgroup\$ – ShivGames Mar 18 '17 at 12:44
  • \$\begingroup\$ @ShivGames I have added demostrations of the code, note that I'm not moving the camera but an rect that represents what the camera would be showing... that's why I'm flipping the sign of targetX and targetY. Now, I'm guessing you are placing the player from the center instead of doing it from the corner... anyway, avoid Magic Numbers. \$\endgroup\$ – Theraot Mar 19 '17 at 19:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.