Understanding the relationship between translate, scale, and the idea of a 2D “camera” can sometimes be a bit tricky. It is worth reviewing the coordinate system and 2D transformations tutorials:
However, in the end, there are three key transformations:
- translate to the center of the screen
- optionally scale the view, e.g. based on player speed or inter-player distance
- translate again to recenter the camera on the player – or on a selected point of view, the midpoint between two players, or the center of a selected group of objects et cetera.
/**
* Two Player Camera 2D
* 2019-06 - Jeremy Douglass - Processing 3.4
* Keep two balls (players) in view by midpoint/distance.
* Press any key to switch modes.
*
* Switch between modes: FLAT, TRACK, TRACK_ZOOM.
* 0 FLAT is a normal 2D view.
* 1 TRACK centers view on midpoint between two balls.
* 2 TRACK_ZOOM tracks + zooms distance between balls.
*/
Ball p1;
Ball p2;
float[] bounds;
PVector view;
int mode; // 0=FLAT, 1=TRACK, 2=TRACK_ZOOM
void setup() {
size(200, 200);
fill(0);
// create a bounding box with two bouncing balls
bounds = new float[]{1, height*.2, width-1, height*.6};
p1 = new Ball(random(width), height/2, 2, 10, bounds);
p2 = new Ball(random(width), height/2, 2, 10, bounds);
}
void draw() {
background(128);
pushMatrix();
noFill();
// update ball locations
p1.move();
p2.move();
// view x,y is a world-space location halfway between two balls
view = PVector.lerp(p1.pos, p2.pos, 0.5);
// view.z is the recommended zoom based on inter-ball distance
view.z = (width-20)/(20+p1.pos.dist(p2.pos));
switch(mode){
case 0: // FLAT
break;
case 1: // TRACK
translate(width/2, height/2); // center screen on 0,0
translate(-view.x, -view.y); // recenter screen on view
break;
case 2: // TRACK_ZOOM
translate(width/2, height/2); // center screen on 0,0
scale(view.z); // scale screen to ball distance
translate(-view.x, -view.y); // recenter screen on view
break;
}
// draw
rect(bounds[0], bounds[1], bounds[2], bounds[3]);
p1.render();
p2.render();
ellipse(view.x, view.y, 3, 3);
popMatrix();
// status
String msg = "mode:" + mode + " xy:" + (int)view.x + "," + (int)view.y + " zoom:" + nf(view.z, 0, 1) + "\nPress any key";
text(msg, 5, 15);
}
// simple bouncing ball with a bounding box
class Ball {
float[] box;
PVector pos;
PVector speed;
float rad;
Ball(float x, float y, float speed, float rad, float[] box) {
this.pos = new PVector(x, y);
this.rad = rad;
this.box = box;
this.speed = PVector.random2D().setMag(speed);
}
void move() {
if (pos.x > box[0]+box[2] || pos.x < box[0]) speed.x *= -1;
if (pos.y > box[1]+box[3] || pos.y < box[1]) speed.y *= -1;
pos.add(speed);
}
void render() {
ellipse(pos.x, pos.y, 10, 10);
}
}
// press any key to switch between modes 0, 1, 2
void keyReleased() {
mode = (mode+1)%3;
}
These operations are order dependent if you are scaling: you must center your coordinate system first, then scale from the center, then translate to your view. If you try to scale first or last then bad things happen: either scaling happens from the top left, or the view scale does not match the drawing scale and they are misaligned.