#9 Camera Kung Fu
Welcome to the ninth devlog! (My apologies for the title. It doesn’t mean anything, I just thought it sounded fun.)
This time we’re talking about the CAMERA in the game. I’ve talked about it before, but that was quite abstract and vague. Now that the camera is (almost) fully implemented, I’ve learned a lot, and I want to share some information about that. (Because I also learned that the camera is even more important than I already thought.)
The three properties
In my game, there are basically three properties that determine how the camera positions itself:
- Average position of players => we want to keep players as much in the center of the screen as possible
- Distance between players => the greater the distance, the more the camera needs to zoom out to show everyone
- Level bounds => we don’t want the camera to move far way from the level bounds, because it’s ugly, unnecessary, and I would have to create HUGE environments to make it look good.
(Whenever the camera goes into splitscreen, the same three properties are true, but they’re simply applied per camera.)
The average position
This one is the easiest. We already need to loop through the players (to determine if we need splitscreen or not) – in the same function, we can add their positions and divide it by the total amount of players.
The camera will, by default, focus on this average position. This alone makes up like 80% of the camera positioning: most of the time, focusing on the average position works smoothly and is the only thing needed.
Now it gets harder.
In 2D games, zooming is easy. The player is 10 pixels outside of camera bounds? Well, increase camera scale by that amount, and we’re fine!
In 3D games, it’s harder. Cameras don’t have a zoom-function, they only have a “field of view” and their “position” that determine how zoomed in you are.
Field of view can be used to play with perspective. A high number creates a typical fisheye effect, a low number makes the camera near orthogonal (which ignores perspective). This means you can NOT use it for zooming, because it would distort the image.
All I can do is displace the camera, moving it more towards the players or further away. And that’s what I did.
The script projects the 3D position of each player onto the 2D image on screen. If this 2D position is outside of the screen, we know the camera must zoom out. (If it’s inside by a certain margin, we know the camera can zoom in again.)
Every frame, I check if I need to resize. If so, I pull the camera back (or forward) by a fixed amount. Because players also move with a fixed speed (they cannot teleport or anything), the camera will soon enough be at the right zoom level, no matter how “far off the screen” the player was.
To check whether a player is on screen, I use Godot’s VisibilityNotifier node.
However, if any part of this box is on screen, it counts as if the whole player is on screen. I don’t want that. I already want the camera to start zooming when the player is near the edges.
So I wrote a small script that displaces the notifier, so that it points away from the camera. This way, the camera will see the player as out of bounds, even though he’s still a few units from the edge.
As of this writing, I still need to do the same for zooming in: displace the notifier to be more inside. Right now, the logic states: “if we’re not zooming out, we should be zooming in!” But that often creates a very stuttery effect, because the camera switches zooming in/out every frame. But that is easy to fix and I expect it to work fine.
This … this is a nightmare :p I’ve iterated on like five or ten different approaches, before finding one that works.
First of all, I must determine the actual level bounds for the camera.
- Every level has a “polygon bounds” variable. This is a polygon, which I set manually, that defines exactly where the level ends. This gives me (after some more optimizations) a very cheap way to determine if a player has somehow gotten off the field.
- However, a camera is rectangular, so these bounds don’t work. Instead, from this polygon, I extract a camera bounding box (at the start of the game). This is the maximum width, height and depth of the map.
Then, I must find out the 3D positions the camera is looking at. Remember how we projected 3D to 2D before? Well, we can also do that the other way around. I take the four corners of the camera and project them into 3D space.
Then I need a different approach for the X and Y coordinates. Why? Because of the way we look at the world. The X-axis is perfectly aligned with our camera, the Y-axis is not (because we’re viewing the world from above, at an angle).
For the X coordinates, I intersect these “rays” ( = lines from the camera into the world) with the ground plane. Now that I know the position we’re looking at, I can check if it’s inside the camera bounding box. If not, I get the difference, and push the camera back to the right position!
(NOTE: At the start of each frame, this “offset” variable is reset to zero.)
For the Y coordinates, I intersect these rays with vertical planes. (Imagine a wall that covers the whole backside of the level.)
- For the backside of the level: if the position is higher than the maximum height, I push the camera back.
- For the front side: if the position is lower than the minimum height, I push the camera back.
This works … mostly. There’s one issue: viewing angles. (I get the sense that viewing angles are ALWAYS the issue with my camera.) If our camera is focused on a player that stands on a tall object, our ray will intersect the ground plane at a different angle, which means the camera will sometimes be TOO strict (it will not go to the level bounds, but stop before it reaches them).
I fixed this by adding a margin around the camera bounding box. The camera doesn’t immediately stop if it reaches the edge, but allows X units of “overlap” before it completely stops. This margin is currently set at a value that feels smooth, but will probably need to be more dynamic (based on zooming level) in the future.
Visibility Notifier, again
There’s a single issue left to solve: if the player is standing at the edge of the level, the camera does NOT move further, which is what we want. But, the VisibilityNotifier could also be outside the level, in which case the camera will keep trying to zoom out to see the player.
This results in endless zooming, without the player ever actually showing up :p
As such, whenever I check if a player is off-screen, I ALSO check if that player’s VisibilityNotifier is inside the level bounds. Only if these are both true, do we allow zooming.
And by this point … I realized I could do without the VisibilityNotifier nodes. I know how to calculate if something is on the screen (by projecting those positions), I know the exact two points I want to check, so I just did that.
The bigger picture
As I was playing around with the camera, I became increasingly annoyed. Not only because it was a difficult system to code, but also because the game is really hard to play if you’re not viewing it from the right angle.
This means “when the camera is zoomed in too much” or “the camera is positioned too much at the top, creating a weird top-down view”
Through all these algorithms, I might just forget that a camera that follows your movements too precisely also isn’t what we want.
Even though the system above works fine and keeps everything visible, I am currently limiting the camera movement a little more, making the camera a little more “loose”.
The first way to do that is by reducing the maximum zoomed-in level. The second way is by pulling the camera back in the Z-direction when zooming, instead of pulling it up in the Y-direction (which indirectly creates the weird top-down view). The third way is by means of (linear) interpolation. Instead of moving the camera to the desired position immediately, I can smooth it and allow it to change a little slower.
Eventually, when you put ALL these things together, the camera should not get in your way anymore.
(I also might just make the players and packages a little bigger, so that they are still visible when you’re really zoomed out, because I feel like that is the best way to view the game. Especially when levels get larger later on.)
I hoped to talk a little more about splitscreen and see-through systems, but this devlog already became far too long (and complex).
So that’s it for the ninth devlog!
The lesson here: making a smooth camera that shows enough, but not too much, is really hard. But also really important, so I’m glad there’s progress on this part.
Get Package Party
Leave a comment
Log in with itch.io to leave a comment.