back to homepage

How Tag's camera works

19/10/2022

camera behaviour in Tag

I get asked sometimes how the camera in Tag (here on the Construct arcade) works. When I wrote the code for it in 2018 or so it was way uglier than explained here. After a week I forgot how it worked and didn't touch it again because it did it's job and I didn't want to break it. It was insightful for me too to break it down again. Anyway let's dive in!

Tag is a two to four player platforming game where characters race around a closed arena (At least, that's all you need to know for the sake of this article). The arena is bigger than the viewport, so the camera needs to move around and zoom accordingly to keep all players visible at all times. There are many ways to go about this, and I'll explain how I solved this for Tag.

Visualizing the problem

Okay, we know what we want, but what does that look like? Having a visual can help figuring out how to make it. Here is our visual aid for Tag:

Tag arena with a white outlined box around all players and the desired viewport presented as a yellow outlined box

The yellow outline is what we want our viewport/end result to be. It gives some extra room left and right for players to see where they're going. The white outline goes around all players. This is the information we need. The camera should:

  1. move to the center of this box
  2. zoom so that it sees all players, plus a little more.

Tracking the Players

We need to find the positions of all outer-most players, in a way that works for any amount of players. Construct has a handy 'pick nearest/furthest instance from a point' condition we can use. For example, to pick the player nearest to the left edge of the arena. But it has a problem: We can only measure form one point, not from an edge. If we measure from one corner of the screen it might pick the wrong player depending on where they're standing, as shown in the image below.

The blue player is closer to the left egde of the arena but the red player is closer to the corner

If the players were all positioned on one line, this wouldn't happen. So let's do that! We'll add objects to track the X positions and Y positions of all players. We'll put them in a container together, so each player has their own trackers. The PlayerX tracker only moves on the X axis, and PlayerY only on the Y axis.

trackers following the players on their respective axes

We can then pick the nearest and furthest instances to the origin of the scene (0,0), and save the positions of their accompanying players. These are the edges of our imaginary white box!

Construct 2 code for the trackers

Moving the camera

We have everything we need to position the camera. Figuring out the center of the imaginary box is simple enough:

x = (X_0 + X_1) * 0.5
y = (Y_0 + Y_1) * 0.5

And we can apply some simple smoothing using lerp:

lerp(Self.X, (X_0 + X_1) * 0.5, 10*dt)
lerp(Self.Y, (Y_0 + Y_1) * 0.5, 10*dt)

Zooming the camera

Now that the camera is in the right spot, we need to zoom appropriately to fit all players in the viewport. We can either favour width, or height. If the imaginary box is very wide, we want to zoom based on the horizontal distance between our players. If the box is very tall, we want to zoom based on the vertical distance. The image below explains what I mean.

wide boxes and tall boxes

We can calculate the zoom values for horizontal favouring and vertical favouring and then decide which one to use. Let's do horizontal first. This is the code:

distance(X_0, 0, X_1, 0) > HOR_TRESHOLD ? HOR_TRESHOLD/distance(X_0, 0, X_1, 0) : 1

If the distance between the outer-most players is greater than the given treshold, zoom to make the viewport as wide as that distance. If the distance is smaller that the treshold, zoom to 1. This means fully zoomed in, not going any closer. The viewport should have a minimum size after all.

With smoothing using lerp and a specified zoom speed it looks like this:

distance(X_0, 0, X_1, 0) > HOR_TRESHOLD ? lerp(LayoutScale, HOR_TRESHOLD/distance(X_0, 0, X_1, 0), ZoomSpeed*dt) : lerp(LayoutScale, 1, ZoomSpeed*dt)

What value should the treshold be? For Tag, I've determined (by trial and error) that these values should be 900 for the horizontal calculation, and 500 vertically. The vertical calculation looks the same, but used the Y values. When we have these horizontal and vertical zoom scales, we can check which is smaller and therefore the zoom scale we should use to fit all players on screen. The final code looks like this in Construct 2:

complete code

Final thoughts

To be honest, I think the camera behaviour in Tag could be improved. The zooming is too fast sometimes and can jerk if players move quickly. I've heard it can nauseating at times, which you really don't want as a designer! A camera should let players focus on the game and not draw attention to itself. But I hope that explaining the camera in Tag can help you start building your own, better one.

That's all, cheers! You can reach me via Twitter.com.