I made a lot of programming experiments on my free time while in elementary school, high school and at the university. The ones that were of a visual nature were typically in 2D. Those of a 3D nature were either primitive sprite-based pseudo-3D, vector-style 3D, or various types of raytracers. All of these used home-made rendering engines, either due to the limitations of the programming languages used or the immense learning curve of the graphics libraries I could find at the time. “Proper” 3D graphics became a pretty big knowledge gap; while I could throw together simple rendering engines from scratch, making something that’s actually fast, useful, and, um, not terrible was still beyond me.
In the last year or so, I have made a few experiments in Unity in an attempt to not only gain some experience with a proper game engine (instead building something from scratch as usual), but also start to get a feel for how to use a “real” 3D system. While the intricate specifics of how rendering pipelines work are still mysteries for the future, I at least have a platform where I can generate and manipulate geometry and have a state-of-the-art renderer display it for me without having to worry too much about the details.
For some reason, “How do you program a camera for a third-person platformer?” was a question that I was curious to see if I could figure out an answer to. I often find that starting with nothing and figuring out things by myself as I work is more educational than just googling what the current state-of-the-art solution is. That solution may be better than anything I can come up with, but struggling with the problem myself gives me a deeper understanding of the fundamental problems that such a solution solves in the first place.
Unless there’s some third option I can’t think of, third-person 3D cameras could be roughly grouped into two categories that I call scripted cameras and autonomous cameras. The former has the level designer provide metadata about how the camera behaves in any given area. The latter tries to figure out on its own how it should behave by analyzing the terrain around the player, possibly with minor hints from the level designer to behave in specific ways at certain points of interest. So far, my camera experiments have been restricted to the scripted cameras category.
To allow me to experiment, I constructed a small world – a vaguely spiral-shaped mountainous island – and implemented extremely simple player movement and physics to allow me to walk around in it. My goal was to make a camera where a level designer can relatively easily configure how the camera moves, where it should focus and define areas where the camera behaves differently (such as around points of interest).
My very first attempt involved using a 3D Bézier curve to create a virtual path along which the camera can move. For each frame, the camera’s position is then simply set to whatever point along that curve is closest to the player. The camera angle is then set to point towards the player.
By allowing for multiple separate paths (whatever point on any of the paths is closest to the player becomes the camera location), it’s possible for different areas to have independent camera setups without having to tie all possible camera locations into a single seamless path.
This is a very simple system, but it works well enough for basic purposes. There are some drawbacks to it, however.
The first drawback I noted was that it was somewhat awkward to define the path. Bézier curves are simple in 2D, but become more cumbersome in 3D due to the lack of depth perception. It’s considerably harder to estimate where exactly in 3D space a given point or portion of the line is without moving objects close to it to provide some frame of reference or looking at it from all angles to figure out how it lines up with everything else. Defining a path in 3D space suspended in mid-air felt very much like a trial-and-error process.
The second drawback is the strict correlation between camera position and what part of the path is being used. Consider a situation where a player stands in front of a tall tower. As the player moves closer, we may want to have the camera move to a separate path much farther away to show the scale of the building compared to the player. This becomes problematic as the selection of potential camera positions is directly tied to the camera distance.
Interestingly, both these issues can be solved by adding a level of indirection; we detach the camera from the path itself.
We adjust our system by reinterpreting the paths. They’re no longer the set of camera positions; they are the expected player paths, the approximations of where the player will be. The closest point on the closest path relative to the player is then interpreted as an idealized player position. The camera is positioned relative to this idealized position, but still angled to point towards the actual player position.
By instead defining where we expect the player to go, defining the paths became considerably easier since the paths are always on ground level, making the depth perception problem less of an issue.
The camera position relative to the expected path is controlled by three parameters: a pivot point, a distance and a height. The position is calculated as the point that’s distance units away from the idealized point in the direction going away from the pivot. This calculation is done entirely in the XZ plane, keeping the camera at the same Y coordinate as the idealized point. This is then adjusted by moving the camera upwards by height units.
The pivot acts as a secondary focus of sorts for the scene, the player being the primary one. A pivot close to a curved path causes exaggerated camera movements to highlight whatever thing is located at the pivot point, while a remote pivot has a more neutral feel to the camera movement.
Most of the paths in my sandbox world set their pivot to a point in the center of the island, moving the camera out towards the ocean and primarily pointing in towards the center. This creates an almost side-scroller feel to the camera; the player primarily moves to the right with occasional movements towards and away from the camera.
For this version of the experiment, I also added some momentum to smooth out the camera movement. This is most notable when swapping paths, giving the player a chance to react to the change since the movement is relative to the current camera angle.
This system of defining expected paths and assigning pivots, distances and heights works surprisingly well despite being relatively simple. In this form, it does however have a few annoying limitations. If a portion of an otherwise simple path requires special settings (such as a higher camera height), it would require breaking the path into three pieces: the before and after pieces with the same settings, and the extra piece between them with the different settings. This is awkward, so we add an extra layer of indirection; we add the ability to provide hints, i.e. defining areas where the standard camera settings are overridden.
Hints can be created using any collider to define its region and setting a new distance and height. Additionally, hints may also choose to override the pivot of the path, changing where the camera focuses. One example where this is used on this island is a dead end where the camera moves to focus on the end of it instead of trying to point the camera towards the center of the island like most other parts.
In addition, the system also has the ability to restrict certain paths to only be considered while the player is within some collider region, allowing finer-grained control over how the camera moves. This was necessary in one spot due to the spiral nature of the map; the major path on the second level was sometimes slightly farther away from a second-level player than a minor path on the first level, making the camera focus on the wrong thing. This was solved by limiting the lower path to only be up for consideration while the player is within a cylindrical area around it, one that does not reach all the way up to the next tier of the spiral.
So yeah, that’s basically it for now. A simplistic third-person camera system that still works reasonably well and that is relatively simple to set up to match the level geometry. It’s not a masterpiece, but it’s a reasonable first attempt. Or, well, second attempt.
Unity is pretty cool. I hope to learn more about it.