Making a 2D Camera
So lets say you're helping a friend make a 2D platformer game. They're a little new at this, so they're getting a bit stuck. They have a few things already made, like the player, the ground, the inputs, and they even imported Matter.js to take care of the physics.
The first thing we should do is take a look at their game and see how it's working. Try interacting with the game a bit, and read through the code that they've written. If you need to restart the game, try editing the code to refresh the canvas. You can also use Ctrl+Z / Cmd+Z to undo changes.
Wait, what's Matter.js again? If Matter.js is altogether alien to you, it might be worth opening up the documentation or looking through the examples. There's also a good tutorial series on The Coding Train on how to use Matter.js with p5. Your friend doesn't want to spend all day thinking about how to implement physics, so they just settled for using a physics library instead.
Arrow keys: Left / Right Spacebar: Jump
The aesthetics of the game could use some polish, but your friend isn't really looking for feedback on that. You might have noticed a bigger problem though: it's pretty small. Only the size of the canvas, in fact.
"How do I make the level bigger?" your friend might ask. Well, we could add more elements outside of the canvas, but that's not really what they're asking. Their real question is:
How might I see more of this world as the player moves around it?
Let's take a moment to reflect on this. In p5, everything is drawn in relation to the origin point (0, 0). By default, this is in the top left of the canvas.
This means, in order to see other parts of the game, we'll have to move the origin. What's more, we need to move the origin in relation to the player so that it stays on the screen.
In summary:
- In order to see more of the game, we need to move the origin
- In order to keep the player on the screen, we need to make the origin movement relational to the player object
Moving the Origin
Fortunately, p5 has a few functions built in that let us do just that. For making a 2D camera that pans left and right, we can make good use of the translate() function.
translate()
As you already know, the origin position (0,0) is at the top left of the p5 canvas. Here's a sketch with a dot drawn at the origin to illustrate.
But the cool thing about translate() is that we can change where this dot is drawn on the canvas without changing the coordinates it's being drawn at.
Call translate() with two arguments — the first moves the origin along the x-axis, the second along the y-axis. Using width/2, height/2 moves the dot to the center.
But what about
push()andpop()? These functions save and restore the transform state so thattranslate()changes don't leak out and affect the rest of your drawing code. It's common practice (but not required) to indent between eachpush()andpop(). In the example above it isn't strictly necessary, but it becomes essential when doing many translations in a row. Read more about push() and pop() →
Animating the translation
The amount of translation can also be animated over time:
By passing frameCount into sin() and cos(), we can make the dot revolve around the center while still drawing it at (0, 0).
Why multiply
frameCountby a small number, and the result ofsin/cosby a larger one? Becausesin()andcos()interpret their inputs as radians (0 to ~6.28), counting in full increments would be a very fast animation. They also always return values between -1 and 1, so multiplying the result by a larger number expands the area the dot moves through.
Now that you're more familiar with matrix transformations using translate(), you're ready to start making a camera.
Making the Camera
Before building a camera class, let's go over what it needs to do:
A 2D Camera needs to:
- Move the origin to see the game world
- Keep track of the player object's position
The first part we already have a good idea of — we can use translate(). But we need separate functions for when the camera transformation begins and ends, since the canvas could get messy fast if we don't revert the matrix when the camera is done.
With that in mind, the first task breaks down further:
Move the origin to see the game world:
- Save the origin position using
push() translate()the origin based on a given value- Revert the origin back using
pop()
Based on these requirements, we can begin to outline a class like this:
class Camera {
constructor() {}
update() {}
begin() {}
end() {}
}The constructor() and update() can be filled in later. For now, add push() and translate() to begin(), and put a translation value in the constructor:
class Camera {
constructor() {
this.translation = createVector(0, 0);
}
update() {}
begin() {
push();
translate(this.translation.x, this.translation.y);
}
end() {
pop();
}
}Now let's make an instance of it in the game. Declare let cam;, then call cam = new Camera() in setup(). Finally, wrap all of the draw calls inside cam.begin() and cam.end().
Try playing around with the x value of this.translation and see how the canvas changes. If you're feeling fancy, try sin(frameCount) * 100 to animate it.
Following the Player
Now that the camera can translate the world, it's time to translate it in relation to the player. The camera first needs to know where the player is — this is done in the update() function.
By passing a target value into update(), the function can manipulate this.translation. Since we only need the x coordinate, the y value stays at 0:
update(target) {
this.translation = createVector(target.x, 0);
}Then in draw(), call cam.update() right after Engine.update() — before background() or any drawing — so the camera position is always fresh for the current frame:
function draw() {
Engine.update(engine);
cam.update(player.body.position);
background(105, 0, 64);
cam.begin();
// ...draw everything...
cam.end();
}Why
.body.position? Because the player is a Matter.js body, its physics data lives on the.bodyproperty.
Wait, the translation is going the wrong way! Since the translation moves the origin, we need to move it in the opposite direction of the thing we're tracking. Negate the x value in
update():
update(target) {
this.translation = createVector(-target.x, 0);
}Hmm, still not quite right 🤔 The player ends up at the left edge of the canvas. To fix this, add an offset so the player is centered — adding half the canvas width to the translation does it:
update(target) {
this.translation = createVector(-target.x + width / 2, 0);
}Great! Now the camera works. The rest of the level could use some more designing, but we can leave that to your friend's creative discretion.