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;
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
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 the
camera_pitchwould then be clamped at max min values to prevent the flipping. The
vertical_rotation_quaternionwould then be generated from the pitch angle using the same
decouple the horizontal rotations
In this current implementation the player and camera horizontal rotations are coupled and use the same
horizontal_rotation_quatThis 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.
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!