Design a smooth camera for a fast-paced combat-focused environment with multiple players.
Instead of having the camera attached to the car or have it follow the car expressly, I decided to use a pivot system. The pivot was parented to the camera, and the pivot started off as a child of the car. At the beginning of the game, the pivot would detach from the car and rigidly follow it. This was so that the pivot could know who it should follow without having to explicitly set it. The camera would be offset from the pivot so that rotating the pivot would rotate the camera about the car on its central axis. This setup allowed for easy control of the camera and provided a very nice experience when driving the car.
To get a smooth design, I tried to interpolate everything possible. The interpolation method I used was linear interpolation, using delta time multiplied by a speed variable as the interpolation parameter. Generally, the interpolation was from the current value to the target value, updated every frame. This gave the effect of smoothly moving towards the target value over an unspecified amount of time. This made the camera incredibly smooth, important in a game with quick interactions and no player control where jerky camera reactions should be expressly avoided.
The first problem with a third-person camera is getting it to follow the actor, and how it follows, whether with a slight drag or snapped onto the actor, rotating about a pivot. Generally in car games, cameras rigidly follow the car about a pivot. Initially, I had the position of the camera interpolated behind the car, but I found that I had to set the speed of the interpolation incredibly high to avoid compounding distance between the car and the camera. Eventually, I locked the location (or offset from the car) of the camera. I left the more advanced features of the camera to be solved with rotations around the pivot of the car.
Because the pivot of the camera was rigidly attached to the car, bumps or curbs of the level would cause the camera to jerk up when it could have just stayed at the same level. To fix this, I interpolated only the vertical axis of the pivot. With low speed, however, high jumps off of buildings or ramps would cause the camera to seriously lag behind and in some cases, the car would leave the camera behind altogether. With high speed, the problem just reverted back to the original issue. I came up with the solution of applying different speeds based on the distance from the current vertical height of the pivot to the vertical height of the car. This allowed for the camera to have a kind of suspension, slowly moving when the difference is small and providing great resistance when the distance gets too far.
Most of the movement of the camera revolved around the rotation of the pivot that was locked to the car. Initially, the rotations of the pivot were static. Of course, this resulted in a jerky movement when cars would turn sharply, or even worse, flip over. To fix the rough movement, I interpolated from the current rotation to face the car’s forward vector. Without restricting the axes that the interpolation uses, the resulting camera would be hectic and erratic while airborne, following the exact forward of the car even while upside down or facing the skybox. Because the game was taking place on mostly flat surfaces, like most car games, it was fine to lock the camera to the X and Z axes. I accomplished the restriction by projecting the car’s forward vector onto the XZ plane. I then used this forward vector to get a rotation looking in the direction of that vector. The result was a camera that would pivot around the car and would function well in the air. Another problem faced was caused by the addition of variable gravity, i.e. the ability to drive on certain vertical walls or even upside down.
Gravity platforms were a huge problem in development, constantly causing issues and discrepancies in gameplay. They were also incredibly fun and interesting to playtesters, so our team powered through. In terms of the camera, I had to solve the issue that the camera was no longer functioning properly while on any gravity platform. The camera would still be locked to the XZ plane, so when the car drives onto the side of a building, for example, the camera would stay upright while the car was driving sideways. It would also cause problems within the projection, leading to some zero vectors that would break the system and cause the camera to be extremely erratic.
There were many solutions to this problem, mainly focused on what the player would feel. For instance, the camera could tilt away from the platform so that the player can function properly while also being reminded that this is not regular ground: they are on the side of a building right now. We decided to mirror the functionality on the ground and not give the player any indication besides their surroundings. It felt more natural in this way like this is just a normal part of this world, an everyday occurrence. I implemented this solution by projecting onto an arbitrary plane which is defined by the normal of the gravity platform (or the opposite of the gravity it enforces on the car). I also had to set the upward direction of the rotation so that the camera rotated the correct way on the Z-axis.
A small issue came up with the ramps leading to the vertical sections of the level. The abrupt transition from normal gravity to horizontal gravity caused the camera to immediately use the normal of the gravity platform when it hit the hitbox, subsequently causing a jerk in the camera motion. Even smaller gravity sections following the curve of the ramp (added to help the car maintain speed up the ramp) were not smooth enough. Another call for interpolation. I keep track of the previous upward vector and interpolate it towards the target normal vector.
This was not quite enough for it to look completely clean, however. When the ramps did not have smaller gravity platforms, the camera would not follow the normal of the surface the car is driving on, i.e. the camera would stay locked to the XZ plane when driving on the ramp until hitting the gravity section. This could be fixed by using the normal vector of the surface as the up vector. When I implemented this solution, it caused many problems stemming from cars capsizing and having one ‘wheel’ (they are hover cars) touch the side of a building, causing the camera to shift to that vertical angle. I ended up dampening this functionality and averaging all surface normals' ‘wheels’ touch. This along with changing the game to have fewer players airborne or capsized made this problem almost non-existent.
An issue with any third person camera is colliding with objects behind the player. I decided to solve this by implementing a collision detection check within the camera. I sent a raycast from the camera to the pivot on the car and checked if it hit anything (other than the car). I would then set the offset from the pivot to the camera so that the camera’s position is the point that it hit. This worked relatively well, but still had issues with clipping into buildings from the side because I was not taking into account the full view frustum (or at least the near plane of the camera). The collision detection could have been much better, maybe also incorporating some dithering on objects in front of the camera. Because this was such a fast-paced game, however, it was a lower priority and I ended up not being able to work on better collision in time.
The game has a mechanic where the player can drift to get around sharp corners or do donuts around opponents. While drifting, the camera would focus on the forward of the car, even though the car is now going sideways. Ideally, the player should be able to see where they are going and drifting showed them something incredibly unimportant. I initially substituted the car’s forward vector for the car’s velocity while the car was drifting and set the rotation to look at that, but this generally looked weird and twitchy. The solution was pretty simple: just add an offset from the car’s forward vector while drifting. This allowed the player to see where they are going while drifting.
While the car’s driving code was certainly robust, there was no easy way for me to get which way the car was drifting (that I would not have to make myself). I could get if the car is drifting, but I would need a way to tell which way the car is turning to shift the camera to look that direction. Another issue when getting the offset I needed was the magnitude, i.e. how strong the offset should be. To solve both these problems, I used the left joystick input to determine the direction of turning and how strong the player is turning. Top it off with some more interpolation and the result was a clean transition from driving to drifting that let the player see what they wanted to see.
Player-Controlled Movement: A constant struggle with the game was combat and how to get a player to actually hit another. Aiming with a controller is notorious for being not that great, and when the player’s only way to aim is rotating the car towards another player, it gets pretty tedious to try and hit someone. The first solution I had was giving the player the ability to aim the camera. This would not work or ever car, specifically cars that did not use projectiles or were designed to only shoot from the front. Some cars could use it, though, so I set about implementing it.
With the first iteration of the player-controlled camera, I opted for an absolute rotation design. Meaning the player would use the right joystick to rotate the camera freely horizontally (360 degrees) with a little bit of vertical movement (about 45 degrees up and down). For example, if the player is holding the joystick to the right, the camera would rotate to the right and continue rotating until the player let go of the stick. The camera would stick with this player-chosen rotation until a specified time had elapsed or they had started moving. This method was not great and was one of the worst problems with the camera at the time. In playtests, players thought the camera had to be controlled through the right joystick, and no fixed camera was available.
The next design was a bit better and allowed the player to see around them without difficulty. It was the type of camera Rocket League uses, where the tilt of the right joystick maps to a specific angle around the car. For example, if the player was holding right, the camera would move right and be locked at 45 degrees to the right of the normal camera and when the player released the joystick, the camera would move back to the normal position. This version of the camera was phenomenal, noticeably better than the previous iteration and allowed the player to aim to some degree.
While having the player aim is great, it takes some skill and undoubtedly some practice, aiming for this game was not the best and relatively unreliable. Not all cars could take advantage of aiming and even with the ones that did use it, hitting a target was pretty difficult especially while driving around. So the team decided to go with lock-on functionality. This would allow the player to target others in their area and somewhat reliably aim. While most cars’ weapons still did not use the functionality of aiming (and could not by design), they could all utilize locking a target within their field of view.
The first version of lock-on was pretty simplistic, just finding all opponents in front of the player and targeting the closest one. This only happened when the player pressed B and worked well for the first attempt. The problems came with the heuristics of figuring out which player to select. There needed to be a wider angle for the search, so that opponents still in the player’s vision would be included. Targeting the closest one was also a problem. An opponent right in the center of the player’s view would be ignored in favor of an opponent to above the player but slightly closer.
To fix the lock-on feature, I designed some new heuristics for the camera to take into account. Firstly, the enemy had to be at most 135 degrees from the forward vector of the car. This was to allow for opponents to be within the field of view of the player and to give the lock-on system some freedom. Secondly, the enemy should not be behind a wall. Targeting opponents behind walls is not intended behavior, the player should be able to see or be near the enemy. Finally, the player and the opponent should be relatively close to each other. Locking onto enemies across the entire map should not be possible. Sorting the list of opponents that meet these criteria also changed from sorting by distance to the player to sorting by distance to the line formed by the car’s forward vector. This allowed opponents in the direction the player’s car is facing to be chosen over others that may be closer to the player.