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
- Follow up to four targets, always having them within screen view at all times.
- The camera should always be focused on the relative center of all four targets.
- As targets move farther away from each other, zoom camera out an appropriate amount of distance.
- 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
- Based on all targets current positions, what are the minimum and maximum positions.
- What is the center point between the minimum and maximum positions.
- 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!).
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)
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.
Great blog post, just what I've been looking for. I am however have some strange issues that I can't seem to figure out, are you willing to share your final code that you used for your camera in unity?
ReplyDeleteIt's gnarly code for sure, but here's what I'm using now
Deletehttps://gist.github.com/MadballNeek/71af494a096f232c548c
Thank you very much! Been trying to get a solid solution for the camera in my game for months, this will help greatly. I appreciate it.
DeleteNo problem! Let me know if you have any questions about it.
DeleteFor whatever reason, my camera is off center towards the left. I've looked at my code compared to yours and it is very similar, I have no idea what I'm doing wrong. Did you by chance experience this? Even with just one player the camera centers itself below and to the right of the player. (Putting the player in the top left of the cameras view. Was using a 2dtk camera, just tried a standard camera and still happens... so wierd.
DeleteAh, figured it out, had something to do with the declaration of my arrays. I was previously just using arrays to store palyer X and Y's, and now changed to including Lists as well and now it is working properly, though I don't fully understand why. I'm no coding expert, but is it expensive to be declaring new lists and arrays inside the LateUpdate method? (I was trying to declare mine once outside of the method, but apparently that caused issues)
DeleteNot really. Objects like that are going to be very short lived; they should never leave the first generation heap which the garbage collector is very well optimized to clean up.
Delete