Create Third-Person Controller in Unity!

In just a few steps, learn how to make your very own Third Person Character controller in Unity. Practical explanation + code included.

Seemanta Debdas
Eincode
10 min readNov 11, 2021

--

Whether you’re getting started in Game Development or making your dream game, creating a Character Controller is one of the first steps you would encounter in your journey.
In this article, we’ll learn how to create an excellent base on which we can easily add on top!

Following is detailed coverage of a Third Person Controller, which mainly focuses on Character movement, jump, rotation with respect to the camera position, and Camera control using Cinemachine(Unity’s solution for creating interactive Camera Movement).

Let’s Get Started!

Resources:

Github Repository: https://github.com/SeemantaDebdas/Blog-TPC

Full Course: https://academy.eincode.com/courses/the-complete-unity-guide-3d-beginner-to-rpg-game-dev-in-c

Setting Up the Scene

  • Creating The Project
    Open Unity Hub and Create a project using the 3D template. After the project finishes loading, make a plane for our Player to stand on.
    To make a Plane,
    Right, Click on the Hierarchy → 3D Object →Plane.
    Reset the transform if it’s not at Position (0,0,0) globally.
  • Creating Our Player
    Right, Click on the Hierarchy → Create Empty.
    Name the newly created GameObject as “Player.”
    Right now, our Player is not visible, so let’s create some graphics.
    On the Hierarchy,
    Right, Click on Player → 3D object → Capsule. This will help us visualize our Player.
    Select our Player in the Hierarchy, then in the scene view, select move tool or press ‘W’ and drag our Player upwards along the y-axis, until the Y value of Position in the transform component becomes 1 (until it’s on top of the Plane).
    Although we can see our Player now, there’s no way to tell which way it’s facing. Let’s add a “nose” to determine which way it’s currently facing.
    On the Hierarchy,
    Right, Click on Player → 3D object → Cube.
    While having the Cube Selected in the Scene view, select the scale tool(or press ‘R’) and make it shorter on the X and Y axis(something like 0.3 on both axis). Then select the move tool(or press ‘W’) and drag it up to the head of our Player.
It would help if you ended up with a similar setup like this. Check the transform of the Cube.

Also, to make our Player stand out even more, let’s add some material to it.
To Create Material, Go to Project Panel → Select the Assets Folder → Create → Material. Let’s name it “NoseMat” (optional). Click on the NoseMat. Select the white box beside Albedo in the Inspector and drag the selector down to choose a darker color.

Modifying NoseMat Material

Now drag and drop our Material to the Nose(Cube) we created earlier. Dragging it either to the Cube in the scene or the Hierarchy will work.

Similarly, go ahead and add a material(of a different color) to the Plane to make our Player stand out even more.

  • Adding Character Controller
    Finally, Click on the Player, and in the Inspector Panel, select Add Component and type Character Controller, and select it.
    The Character Controller takes care of collision, movement, and rotation for us. The only thing that we need to implement externally is gravity(more on that later).
    Also, the Character Controller has a collider of its own, so we can go ahead and remove the colliders of the Capsule and Cube.
    We have successfully set up our scene! Before making our Player move, let’s first create our Camera move based on our mouse movement. For this, we’ll be using Cinemachine.

Cinemachine

Cinemachine is an easy-to-install plugin that lets us add functionality to cameras we’ve already got or make new ones with amazing behavior. It allows us to create complex camera behavior without writing a single line of code.

  • Setup Cinemachine
    To setup Cinemachine in our project,
    Go to Windows → Package Manager → Search “Cinemachine” → Click Install.
    If you can’t find Cinemachine in Package Manager, under the packages dropdown, switch to Unity Registry. Then search again.
Package Manager
  • Create Cinemachine FreeLook Camera
    If we Right Click on the Hierarchy and hover over Cinemachine, we’ll find a lot of Cameras. For our intended purpose, we’ll be using the FreeLook Camera. So go ahead and select that.
  • Modifying FreeLook Camera
    Select the FreeLook Camera. In the Inspector, under the CinemachineFreeLook component, drag our Player in both the Follow and Look At slots. Notice that the Main Camera’s Position gets updated(see Game View). The Main Camera now has a CinemachineBrain component that allows the rest of the Cinemachine Cameras to manipulate its components.
    As we can see, the Camera seems to be a bit too close to our Player. To change this, go down to the Orbits under the CinemachineFreeLook component, and let’s make the following changes:
    Set the Binding Mode to World Space. Below that, we’ll find the three rigs along which our Camera moves and rotates(Top, Middle, and Bottom rig, which are also visible in the scene view). This is because we don’t want our Camera to be influenced by the rotation of our Character.
    Adjust the height and radius of the rigs to increase the distance of the Camera from the Player.
Values I chose to go with. Feel free to play around with the values until you get the desired result

Now, if we hit Play, we can look around using our Mouse. If you are not satisfied with the result, go ahead and fine-tune the values until you get the desired result.

Also, I set the Y-Axis speed at five and X-Axis speed at 450 and checked Invert under Y-Axis. (Optional)

More about CinemachineFreeLook here:
https://docs.unity3d.com/Packages/com.unity.cinemachine@2.3/manual/CinemachineFreeLook.html

Now that we have set up our Camera let’s make our Player move!

Player Movement

We’ll handle our movement using a script.
To create a script,
Go to Project Panel → Select the Assets Folder → Right Click → Create → C# Script. Call it “Movement.”
Please attach it to our Player GameObject by dragging the script and dropping it on the Player in the Hierarchy. Alternatively, we can select the Player → Select AddComponent in Inspector → Type in “Movement” → Hit Enter.
Let’s open up the Movement Script in Visual Studio to start coding!
First, let’s capture Player Movement Input and store them in a Vector3. We’ll do so in the Update function by writing the following line of code:

Vector3 movement = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized;

Input.GetAxisRaw(“Horizontal”) returns -1 if we press the ‘a’ key or left arrow button and +1 if we press the ‘d’ key or right arrow button. Input.GetAxisRaw(“Vertical”) returns -1 if we press the ‘s’ key or down arrow button and +1 if we press the ‘w’ key or up arrow button. Adding “.normalized” normalizes the Vector. This is to be done to stop the Player from going at a higher speed when moving diagonally.

We’ll be using the Character Controller to move our Player. To use it, we must first cache it in our script like so:

CharacterController controller; 
// Start is called before the first frame update
void Awake()
{
controller = GetComponent<CharacterController>();
}

Then, based on any Input from the user, we move the Character using a function called controller.Move() that takes in a vector3 as the parameter. Like so:

if (movement.magnitude >= 0.1f) 
{
controller.Move(movement * speed * Time.deltaTime);
}
// "speed" is a private float variable that is used to control the speed of the player

Time.deltaTime is being multiplied to make the movement frame rate independent.
This code will make our Player move but won’t make it rotate towards the direction of movement. To make our Player face the direction, it’s moving. We use the Mathf.Atan2() function.

Player Rotation

Let’s first point our Player towards the direction he’s moving. We do that by figuring out how much we need to rotate our Player on the y-axis to face the direction.

There’s a neat little function that will help us achieve that. It’s called Mathf.Atan2(). Modify the previous code as shown below to rotate the Player:

if (movement.magnitude >= 0.1f) 
{
float targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0, targetAngle, 0); controller.Move(movement* speed * Time.deltaTime);
}

So, what is Atan2()?
→ Well according to Unity Documentation, Atan2 returns a value that is the angle between the x-axis and a 2D vector starting at zero and terminating at (x,y). Let’s break it down

Atan2 visualization.

Let’s view our Player from Top-view. The horizontal axis is the x-axis, whereas the vertical one is the z-axis(depicted as y in the diagram). To get the angle from the Player’s Forward axis(x-axis) to the point (x,y), we pass in x=movement.x and y=movement.z as parameters. The resulting angle is in radians, so we multiply it by Mathf.Rad2Deg.

Example:
Say we pressed the forward and right key together. then the resulting movement vector will be:
(0.7, 0, 0.7) since we are normalizing it. Therefore, movement.x = 0.7 and movement.z = 0.7
The Atan2 value when these values are passed in as x and y respectively is 0.785398163397, which is equivalent to 45 degrees.
When we pass 45 as eulerAngle.y, our Player rotates to face the direction it’s moving in.
But in Unity, our Character will have a rotation of 0 when it’s pointing forward. To compensate for this, instead of passing in y as a parameter first, we pass in x. The diagram will look something like this:

How Atan2 functions in Unity

Try to find out the values for some more values of x and y to understand this better.

Now, Although our Player rotates, the rotation is not snappy. To make it smooth, we use the following code:

Variables to make rotation smooth:

//serialize field makes the variable appear in inspector without //having to make it private[SerializeField] float rotationSmoothTime; 
float currentAngle;
float currentAngleVelocity;

Set rotationSmoothTime to small values(e.g:-0.15) for faster rotations.

Implementing smooth rotation and modifying previous code:

if (movement.magnitude >= 0.1f)
{
float targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;
currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref currentAngleVelocity, rotationSmoothTime);
transform.rotation = Quaternion.Euler(0, currentAngle, 0);
controller.Move(movement* speed * Time.deltaTime);
}

The function that we used is called Mathf.SmoothDampAngle() that takes an angle and set’s current angle to that angle over rotationSmoothTime. The shorter the rotationSmoothTime, the quicker the transition will happen.
Instead of passing in our targetAngle, we pass in the smoothed currentAngle for rotating the Player along the y-axis.

Rotation with respect to camera direction

To make our Player move while taking the rotation of the Camera into consideration, we add the Camera’s EulerAngle of the y-axis to our target direction.

Referencing the Camera:

CharacterController controller;
Camera cam;
private void Awake()
{
controller = GetComponent<CharacterController>();
cam = Camera.main;
}

Camera.main fetches any Camera with the “Main Camera” tag.

Moving with respect to camera rotation:

float targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg + cam.transform.eulerAngles.y;

Moving in the Rotated Direction

Finally, to move in the Rotated Direction, we multiply the rotation(targetAngle) with our Player’s Vector3.forward.
Multiplying a rotation by a Vector rotates the Vector by that amount of rotation.

// Entire movement code encapsulated in a function
// and calling it from Update
private void HandleMovement()
{
//capturing Input from Player
Vector3 movement = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized;
if (movement.magnitude >= 0.1f)
{
float targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg + cam.transform.eulerAngles.y;
currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref currentAngleVelocity, rotationSmoothTime);
transform.rotation = Quaternion.Euler(0, currentAngle, 0);
Vector3 rotatedMovement = Quaternion.Euler(0, targetAngle, 0) * Vector3.forward;
controller.Move(rotatedMovement * speed * Time.deltaTime);
}
}

Now that our Player can move as desired let’s make it Jump!

Player Jump

As stated earlier, since we are using Character Controller for Player Movement, we need to implement gravity through code.
First, let’s create a void Function, name it HandleGravityandJump(), and call it in the Update Function.
Also, create a velocityY float variable that will contain our Player’s vertical velocity.

The value of the variable velocityY will depend upon 3 cases:

  • The Player is on not the ground
    When the Player is not on the ground, we subtract the value (gravity * gravityMulitplier *Time.deltaTime) from velocityY on every frame. Where gravity = -9.8f, gravityMulitplier = 2/3/4….(depending upon how intense you want it to be).
  • The Player is on the ground.
    When the Player is on the ground, we no longer continue to exert a downward force. Instead, we set the velocityY to be a slightly negative value such as -0.5 or so.
  • Player is Jumping
    When the Player is on the ground, and the Jump button is pressed, we set velocityY to a value of √(jumpHeight *2*gravity). It’s a neat little physics formula that’ll help us set velocity based on a specific height.

With all of these in mind, the following is the code to make our Player Jump:

[Header("Gravity")]
[SerializeField] float gravity = 9.8f;
[SerializeField] float gravityMultiplier = 2;
[SerializeField] float groundedGravity = -0.5f;
[SerializeField] float jumpHeight = 3f;

Variables used for implementing jump and gravity functionality.

void HandleGravityAndJump()
{
if (controller.isGrounded && velocityY < 0f)
velocityY = groundedGravity;
if (controller.isGrounded && Input.GetKeyDown(KeyCode.Space))
{
velocityY = Mathf.Sqrt(jumpHeight * 2f * gravity);
}
velocityY -= gravity * gravityMultiplier * Time.deltaTime;
controller.Move(Vector3.up * velocityY * Time.deltaTime);
}

With that, our Third-Person Controller is now Up and Running! HandleGravityandJump() function that is called from Update.

Conclusion

If this simple Third Person Character Controller piqued your curiosity, then you should consider opting for The Complete Unity Guide 3D- Beginner to RPG Game Dev in C# offered by Eincode. This course features among the most immersive and practical resources out there.
Curated by experienced software engineer Filip Jerga, this course starts with the fundamentals. Then, it progresses gradually to eventually take its subscribers through the journey of developing their own RPG game by using Unity 2020 and C#.

Cheers!

Debdas

--

--

Seemanta Debdas
Eincode
Writer for

Game Dev enthusiast contributing to the Gaming Industry!