Tuesday, January 14, 2014

Single Camera System For Four Players

One design decision of Overtime I've been currently facing is "how big should the game maps be (single screen or multi-screen)?" Small maps allow for focused, more intense battles while limiting game mode possibilities, while larger maps allow for more gameplay and game mode varieties. Small, single screen maps would require only one camera to capture all players and playing environment. Larger, multi-screen maps would require multiple cameras (one for each player) that can follow a target. Since Overtime is a local-multiplayer game only, this also means split-screen cameras.

Why not have both? It's pretty trivial to have maps that anchor the camera to a single spot for small maps, while allowing split-screen cameras for larger maps. After some playtesting, I found the split-screen cameras pretty annoying due to the small screen real estate they provided for each player. I didn't want to scrap the idea of big maps entirely. So, is it possible to create a single camera that can follow up to four different targets? I soon realized that the type of camera I ended up needing is a camera in the style of a fighting game.

Fighting games, such as Super Smash Bros. or even wrestling games, feature single screen cameras that track multiple targets, zooming in and out as the targets get closer and farther away from each other, respectively. This is done by some vector math magic (it's not really magic, as you'll see).

So let's go through the requirements of the camera system
  1. Follow up to four targets, always having them within screen view at all times.
  2. The camera should always be focused on the relative center of all four targets.
  3. As targets move farther away from each other, zoom camera out an appropriate amount of distance.
  4. As targets move closer to each other, zoom camera in, clamping the zoom factor to a specified amount.
From that, we can immediately deduce we need to know the following things
  1. Based on all targets current positions, what are the minimum and maximum positions.
  2. What is the center point between the minimum and maximum positions.
  3. How far do we need to zoom to keep all targets within view.
So far, maybe you've realized that it's impossible for a camera to follow multiple targets; the camera must always be fixed on one point to follow. Obtaining the minimum and maximum positions will allow us to later find the center point between these positions, which we will use as the single point the camera will follow. By following this point, we know the camera will be relatively center of all targets. Here is the pseudocode for obtaining the minimum and maximum positions:

List xPositions;
List yPositions;
foreach target {
    xPositions.Add(target.x);
    yPositions.Add(target.y);
}
maxX = Max(xPositions);
maxY = Max(yPositions);
minX = Min(xPositions);
minY = Min(yPositions);

minPosition = Vector2(minX, minY)
maxPosition = Vector2(minX, minY)

We need to obtain the x and y coordinates of all targets, and store them in a List. We then find the maximum (x, y) and minimum (x, y) values of each to give us our final minimum and maximum positions (Unity has these Max and Min methods in the Mathf class, but you could implement your own easily if needed).

Let's add some diagrams to help visualize this better (the scale is all wrong, I know, but bare with me!).


The smiley faces represent our three players, and their positions. Following the above pseudocode, we come up with a min of (8, 7) and a max of (31, 14). This gives us the outermost coordinates of the area our players are in.

To find the center of these two positions is a trivial step. Simply add the Min and Max vectors, and multiply by 0.5 (favor multiplication over division for performance reasons).


((8, 7) + (31, 14)) * 0.5 = (19.5, 10.5)

Great! We now have the target position that our camera will use to follow. This position will update as our players move, ensuring we're always at the relative center of them. But we're not done just yet. We need to determine the zoom factor.

Quick side note about the zoom factor. When developing a 2D game, you normally use 2D vectors (as we've been doing so far) and an orthographic camera, which ignores the z-axis (in Unity, not really, but the depth is used differently as objects don't change size as the z-axis changes). If you were developing a 3D game, you'd be using 3D Vectors and a perspective camera. Perspective cameras have depth according to their z-axis position. However, determining the zoom factor for both 2D and 3D is quite similar, just how you apply the value differs.

We've already determined that the X and Y coordinates of our camera needs to be (19.5, 10.5), as that's the relative center of all targets on the X and Y axes. What you need now is a vector that's perpendicular to the X and Y coordinates we calculated above. That's where the cross product formula comes in. The more astute reader may be screaming "you can't perform cross product on 2D vectors!" right now. Yes, you're absolutely correct, but bear with me.

The cross product of two vectors give us a vector that's perpendicular (at a right angle) to the two.
Source: Wikipedia


The diagram above shows the cross product of the red and blue vectors as the red vector changes direction, with the resulting perpendicular green vector. Notice how the magnitude of the green vector changes, getting longer and shorter based on the magnitude of the red vector. This is exactly what we need, a vector that's perpendicular to our camera's (X, Y) target position coordinates, whose magnitude changes appropriately based on the angle.

As mentioned before, you can't perform the cross product of 2D vectors. So instead, we'll pad our 2D vectors with a z coordinate of 0.

(19.5, 10.5, 0) x (0, 1, 0) = (0, 0, 19.5)

x is the symbol for cross product. We use a normalized up vector  as our second argument so that the resulting vector is of maximum distance. Using the Z value of 19.5, we can now set the zoom factor. Since orthographic cameras don't technically zoom in the same sense as a perspective camera, we instead change the orthographic size, which provides the same effect.

Now let's assume that the perspective camera of your 3D game needs to act very much like a 2D platformer (always facing the side, never directly above or below). Instead of altering the orthographic size (because that doesn't make sense for a perspective camera ;) ), we use the results of the cross product to set the z-axis directly. This will move the perspective camera accordingly, give us our desired zoom effect.

Here's a video demonstrating the camera movement for two players.