I recently migrated to a new game engine from a custom game engine. I needed to implement a third person character controller. This is something I did previously in the custom engine but forgot how. It took me a few days to relearn. I eventually reverse enginered my previous camera system. Finding resources online was suprisingly challenging. The style of camera I wanted is an "over the shoulder". Lots of the resources I found on how to implement were missing details on vertical axis rotation The implementation I settled on is odd and more of an 'arc ball' implementation that follows a player. The camera has an invisible 'view sphere' with a camera eye on one point of the spehere and a lookat target on the other.
This implementation is also agnostic to input devices. The player can use mouse+keyboard, a touch screen, or gamepad. This implementation assumes the player has a directional input for movement like WASD keys or Dpad. It is also assumed the player has another input for camera control like a mouse, analog stick, or touch screen. For the sake of consistency we will assume keyboard and mouse inputs.
The following implementation uses some fake pseudocode to demonstarte the math mechanics. Mainly it demonstrates the required values and their relative mutations. I am documenting the implementation in this post so I will remember how to do it in the future.
Start with the front vector.
Setup a basic scene with a character mesh and a perspective camera. The character mesh and camera should face the world front vector this vector depends on the coordinate system of your game engine. The coordinate system of the latest engine I'm using is a Z up axis and the X axis is forward. This is similiar to the Unreal engines coordinate system. Previously I was working in a Y up and Z forward coordinate system. Freya Holmér has produced helpful chart on which tools use which coordinate systems It really dosen't matter what world vector you use as long as the camera and character default to facing the same vector and you consistenly use it for all view calculations.
Set a default offset for the view sphere
Next you want to create an offset vector that is the "view sphere offset". This is the vector that will translate the camera to a position over the shoulder of the player. This offset should be relative to the players position. You could update this vector on each frame as the player moves or you could make it a child entity of the player entity so it is calculated by the engine entity transform heiarchy. In our implementation we will manually be updating it each frametick.
Set the default camera and target positions
Next you will set the initial values for the camera eye and camera lookat positions. These vectors will be relative to the view sphere offset vector. They will be on opposite sides of the view sphere sphere. They extend forward and back from the view sphere center.
camera_lookat_position = view_sphere_offset + world_front * view_sphere_radius;
camera_eye_position = view_sphere_offset - world_front * view_sphere_radius;
The difference between the two is the addition and minus operations to the camera offset. The addition operation of the camera_target_position
moves the camera positively to the front and the minus operation moves the camera_eye_position
to the back.
At this point we have all the components for a static over the shoulder view of our player mesh. Next we will add code to to handle player input.
The player rotation quaternion
The first player input we want to address is horizontal axis rotation. The horizontal axis rotation will effect all our other movement calculations. We will need a quaternion to handle horizontal rotation. When the mouse moves on the X axis we rotate the horizontal quaternion on the around the vertical axis. this looks like:
horizontal_rotation_quat *= new_quaternion_from_rotation_z(delta_mouse_position_x * mouse_sensitivity)
This code assumes the quaternion implementation you're using has from_rotation
methods that take a euler angle and create a new quaternion from that angle.
Each gameplay frame we will use the horizontal_rotation_quat
to update the players rotation and heading, we will apply the rotation value in the next section.
Handling player movement input
Now that we have our heading for the players horizontal direction it makes sense to tackle the WASD input We need a few new variables to capture this:
- player_forward_vector - The vector the player is facing and and headed towards
- player_right_vector - The vector to the players right that they can strafe towards
- player_direction - a vector combining the players forward and right vectors indicating their heading
- player_speed - a scaler value represening movement speed
player_forward_vector = horizontal_rotation_quat * world_front_vec;
player_right_vector = horizontal_rotation_quat * world_right;
player_direction = [0,0,0] // empty vector
With those heading vectors in place we can handle the WASD inputs. Assuming we have a keydown input handler:
if(keydown == W) player_direction += player_forward
if(keydown == S) player_direction -= player_forward
if(keydown == A) player_direction -= player_right
if(keydown == D) player_direction += player_right
finally we can update the players position based on the direction and speed
player_position = player_direction * speed;
This code will update the player mesh. We also need to apply the horizontal_rotation_quat
to the camera view and target offests
camera_rotation_quat = horizontal_rotation_quat
camera_front_vector = camera_rotation_quat * world_front;
view_sphere_offset = world_up_vector * up_offset + player_right_vector * right_offset;
target_distance_from_player = camera_front * distance;
camera_distance_from_player = -camera_front * distance;
camera_target_position = player_position + view_sphere_offset + target_distance_from_player;
camera_eye_position = player_position + view_sphere_offset +
Now we have the basics of a third person camera. The player can move and rotate on the horizontal axis and the camera will rotate with the player.
Vertical rotation and aim
Next let us handle vertical rotation. This is where I've seen gaps in many third person camera learning resources. To capture vertical aim we will create another quaternion similiar to the horizontal_rotation_quat. The new quaternion will be called vertical_rotation_quat
and we will update it from the mouse Y axis movement deltas.
vertical_rotation_quat *= new_quaternion_from_rotation_y(delta_mouse_position_y * mouse_sensitivity)
combine vertical and horizontal quaternions
Now that we are capturing horizontal and vertical rotation input we will combine them in to a new quaternion we'll for the overal camera translation
camera_rotation_quat = horizontal_rotation_quat * vertical_rotation_quat;
The camera_rotation_quat
is used to determine what is the cameras front direction:
camera_front = camera_rotation_quat * world_front
given the cameras front vector we can project vectors forward and back from the view_sphere_center to give us the position of the lookat and eye. We then have all the data we need to set the final lookat
and eye
position. This captures all expected user input: WASD movement, Mouse vertical and horizontal look movement.
Game engine agnostic third person camera controller peusdocode
The following is a peusdocode implementation of the mechanics discussed above. It contains a for_each_frame do:
section which is synonymous to a update handler in any game engine.
world_front = X_axis
world_right = Y_axis
world_up = Z_axis
player_position = [0,0,0]
player_front = world_front
player_speed = 5;
view_sphere_up_offset = 7;
view_sphere_right_offset = 2;
view_sphere_radius = 7
view_sphere_offset = player_position + world_up * view_sphere_up_offset + world_right * view_sphere_right_offset;
camera_lookat_position = view_sphere_offset + world_front * view_sphere_radius;
camera_eye_position = view_sphere_offset - world_front * view_sphere_radius;
camera_front = world_front
horizontal_rotation_quat = [0,0,0,0]
vertical_rotation_quat = [0,0,0,0]
mouse_sensitivity = 5;
for_each_frame do:
// update rotation quaternions with mouse delta changes
horizontal_rotation_quat *= new_quat_from_z_rotation_angle(mouse_delta_x * mouse_sensitivity)
vertical_rotation_quat *= new_quaternion_from_rotation_y(delta_mouse_position_y * mouse_sensitivity)
// update player forward and right vectors with new rotation
player_forward = horizontal_rotation_quat * world_front;
player_right = horizontal_rotation_quat * world_right;
// set player direction based on WASD keyboard input
player_direction = [0,0,0] // empty vector
if(keydown == W) player_direction += player_forward
if(keydown == S) player_direction -= player_forward
if(keydown == A) player_direction -= player_right
if(keydown == D) player_direction += player_right
// update player position with new direction and speed
player_position += player_direction * player_speed;
//update view_sphere_offset to the new player_right vector
view_sphere_offset = world_up * view_sphere_up_offset + player_right * view_sphere_right_offset;
// create a camera rotation quat from the horizontal and vertical rotations
camera_rotation_quat = horizontal_rotation_quat * vertical_rotation_quat;
// create a camera front vector that applies the camera rotation to the world front
camera_front = camera_rotation_quat * world_front
// project the eye and offset out from the view sphere center by a radius
// lookat is positive so its in front of player
// eye is negative so it is behind player
camera_lookat_offset = camera_front + view_sphere_radius
camera_eye_offset = camera_front - view_sphere_radius
// finally calculate the camera look at and eye positions
camera_lookat_position = player_position + view_sphere_offset + camera_lookout_offset;
camera_eye_position = player_position + view_sphere_offset + camera_eye_offset;
Additional third person camera features
This is a fairly functional third person camera implmentation but there are extra cirricular features to ad
clamp the vertical rotations
The current implementation has no limit on the vertical rotation. This is a problem because the camera can easily flip over or under the player mesh. To alleviate this we would create a variable that represents the vertical euler angle rotation like
camera_pitch
. This pitch variable would be updated each frame with the mouse coordinates like we do with thevertical_rotation_quaternion
thecamera_pitch
would then be clamped at max min values to prevent the flipping. Thevertical_rotation_quaternion
would then be generated from the pitch angle using the samenew_quaternion_from_rotation_y
helper function.decouple the horizontal rotations
In this current implementation the player and camera horizontal rotations are coupled and use the same
horizontal_rotation_quat
This coupling enforces that the player and camera are always facing the same direction. We could give the player and camera seperate horizontal rotation quaternions which would enable the camera to freely rotate the view sphere space while the player mesh could look in other directions. This camera experience is similiar to what you seein in AAA thrid person camera games like From Software titles.smooth out the movment with lerps
In this current implementation we are instantly updating all our calculations for the camera position. The movement could be improved by adding some interpolation to smooth out the camera and also apply some curves to the movement to give it a smoother effect.
Summary
This camera system is my niave implementation that came togeter after random hacking. I could not get a pitch, yaw, polar coordinate implementation working. Let me know in the comments if there are more correct ways to implement this type of camera system or any improvements.
If you liked this content follow us on Twitter for more!