How to create a 3D platformer in Unity (Episode 4: Working with Cinemachine)

Now that we have a player character that is able to move and jump, we’re going to have to work on the game camera because as you may have noticed, when we jump or move too far, the player moves out of the view of the camera. That’s definitely not what we want. In fact, we want the camera to follow the player, we want the camera to have the ability to rotate around the player and lastly, we want the player to be able to move in the direction that the camera is pointing to.

So how do we achieve all of this? Well luckily for us, Unity makes that really easy to do. Unity has a package called Cinemachine which can help us out with that. Cinemachine is an Emmy Award winning package which has been used in film, tv as well as games. It’s very powerful but also very easy to use. So let’s get started with it.

Installing Cinemachine:

Cinemachine will have to be added to our Unity project but that’s going to be really easy to do. At the top of the Unity Editor, use the dropdown to access Window > Package Manager. Once the Package Manager window is open, make sure that you are in the Unity Registry and scroll down the list of packages for the Cinemachine package. Click the Install button on the bottom right of the Package Manager window and that’s it. We’ve got Cinemachine installed. Once the install has completed, you should see a Cinemachine dropdown at the top of your Unity Editor. (See gif below for process and picture to the right of the Unity Editor after the Cinemachine package has been installed.)

Setting up our Freelook Camera:

Next, using the Cinemachine dropdown, we’re going to create a Freelook Camera.

Great! Now that we have our Cinemachine camera created, let’s select it and in the Inspector window of the Cinemachine camera you should see two open slots for Follow and Look At. Take the Player parent object that created and drag it into the Follow slot, then expand the Player and drag the Head object onto the Look At slot. We should now have the camera following the player if you switch to your game view or test your game. (The camera might be a little too close to the player for now but we’ll fix that soon enough.)

You may have noticed that the camera also rotates when you move the mouse but it may not act the way you’re used to a third-person camera acting. So let’s make a few changes to the default settings to customise our camera.

Let’s start by attaching the Freelook camera that we created to the Main Camera.

Make sure you scene view window is open and select the Freelook camera. You may notice that the player now has three red rings floating around it. Those rings or rigs (as you’ll see them named in the Inspector window) confine the movement of the camera around the player. The top rig represents the view when your mouse has moved the camera to focus on the top of the player character while the bottom rig represents the lowest possible view of the player character that you may have.

So let’s widen those rigs to give us a better view of the player. (The way you set up the rigs will determine the angle and view that your player will have of your game. You can customise this as you wish for your own feel. I find that wider rigs works for me with medium height. If you increase the height of all three of the rigs, you could even create a top-down view.) We can change the width and height of the individual rigs by heading to the Orbits section within the Inpsector window of the Freelook camera. (These are my settings below, but feel free to experiment with what feels good to you.)

If you test your game however, you may find that there are two things wrong with the camera. The first being that it doesn’t rotate in the way that you’d like when you move the mouse. The Freelook camera has a section called Axis control which has a few options that you could tweak.

  • If your camera is rotating in the wrong direction, Uncheck the invert checkbox on either the x or y axis to fix it. (I prefer both Invert boxes unchecked.)
  • If you feel like the camera is moving too slow in response to your mouse movement, adjust the Speed.

These are my settings for now. I will probably end up changing the width and height of the rigs once we have a few environmental assets in our scene:

Fixing the player rotation problem:

The second thing that you may find to be troubling is that the player moves independently of the camera. If you move the player character you should see it moving forward and back in the same way that we’d expect it to but the camera doesn’t really help us change the direction that the player would be facing.

We can fix that by first changing the rotation of the player character model to face the direction that the camera is facing and then ensuring that the player moves in the direction that the camera is facing. Let’s open up the PlayerController script to change the player’s rotation.

Once we have the script open, we’re going to have to declare a variable that will help us to reference the camera in the same way that we referenced the Player Controller component. So to do this let’s declare:

public Camera theCamera;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;

    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;


    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
    }
}

Now, we could head over to Unity and drag the main camera into the open Camera slot that we created on the Player Controller script, but we can also ensure that the Player Controller script also has a reference to the camera as soon as the scene loads. (You don’t have to do it this way, both ways work so do whatever you feel comfortable with.)

To do this we can add a statement to our Start function. We haven’t yet added any statements to the Start function but all we have to understand is that while the Update function runs once per frame of our game, the Start function only runs once when the scene is loaded. The statement that we’ll add is:

theCamera = Camera.main;

This will take ensure that the main camera within our scene is always associated with our theCamera object as soon as the scene starts.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;

    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;

    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;

    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
    }
}

Now, if you head over to Unity and try and rotate your player character, you’ll notice that we can rotate our model on either the x, y or z axis. We don’t really want to rotate our model on the x or z axis though (as you can see from the gif below) , we want to rotate our character on the y-axis.

To rotate the player based on the camera’s rotation we can use the statement:

transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);

(For more information on Quaternions and rotation check out the video in this link: https://www.youtube.com/watch?v=3RQmzVGI8tQ&ab_channel=GameDevHQ)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;

    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;

    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);

        transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);

    }
}

If you save the script and run the game, you’ll notice that the player character now rotates when we rotate the camera but that’s not really what we want. We want the player character to rotate to a specific direction when the player is moving, not just when the mouse moves. So to restrict this, let’s add an if statement: (The if statement will check to see if we have any input on the Horizontal or Vertical axes and only then apply the rotation to the player model.)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;

    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;

    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
        if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
        {
            transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);
        }

    }
}

If you save the script and head back to Unity to test the game you’ll notice that the player rotates when we move BUT it still moves in the direction of the player model’s x and z axes and not the camera direction.

Now we’ve been setting the direction that player will move in when we set the moveDirection at the beginning of the script and right now it’s set to be the direction of the Horizontal and Vertical axis. Since we no longer need to use the axes to set the moveDirection, let’s comment out that statement and replace it with:

moveDirection = (transform.forward * Input.GetAxisRaw(“Vertical”)) + (transform.right * Input.GetAxisRaw(“Horizontal”));

This will ensure that the player will move towards the front of the character model (which we’ve set to face the direction of the camera when we move) when we move with the W/S or Up and Down arrow keys. It will also allow us to move to the left and right in the same way.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;

    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;

    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        //moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));

        moveDirection = (transform.forward * Input.GetAxisRaw("Vertical")) + (transform.right * Input.GetAxisRaw("Horizontal"));

        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
        if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
        {
            transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);
        }
    }
}

Increasing the play space:

Since we’re testing the movement, I’m going to take the opportunity to increase the size of our ground plane to give us some room to play around in. (I’ve set the scale of the ground plane to 10 on the x-axis and 10 on the z-axis.)

Making the player face the direction that we intend to move in:

First we need to store the rotation that we’d like the player to face so that we can reference that rotation. We also know that we only want this to happen when we move so we can create this logic within the if statement that we previously created. To create and store the new rotation let’s use the statement:

Quaternion faceRotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0f, moveDirection.z));

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;

    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;

    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        //moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));

        moveDirection = (transform.forward * Input.GetAxisRaw("Vertical")) + (transform.right * Input.GetAxisRaw("Horizontal"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
        if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
        {
            transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);
            Quaternion faceRotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0f, moveDirection.z));

        }
    }
}

We also need to create a reference to the player model itself so that we can rotate it using the new rotation that we’re creating. (See below)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;

    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;
    public GameObject thePlayer;


    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        //moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));

        moveDirection = (transform.forward * Input.GetAxisRaw("Vertical")) + (transform.right * Input.GetAxisRaw("Horizontal"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
        if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
        {
            transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);
            Quaternion faceRotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0f, moveDirection.z));

        }
    }
}

Head back to the Unity Editor and wait for the script to compile before dragging the Body game object of our player into the newly created thePlayer game object slot.

We can then go back to our PlayerController script and set the model’s rotation to the rotation that we created.

We’re going to do this using a Slerp (link to Unity documentation and a video explaining Slerp) which will smoothly rotate the player model in the direction that we set when we moved.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;
    public float rotateSpeed;


    private Vector3 moveDirection;

    public CharacterController charController;
    public Camera theCamera;
    public GameObject thePlayer;

    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        //moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));

        moveDirection = (transform.forward * Input.GetAxisRaw("Vertical")) + (transform.right * Input.GetAxisRaw("Horizontal"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
        if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
        {
            transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);
            Quaternion faceRotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0f, moveDirection.z));
            thePlayer.transform.rotation = Quaternion.Slerp(thePlayer.transform.rotation, faceRotation, rotateSpeed * Time.deltaTime);

        }
    }
}

Head back to Unity set the rotateSpeed to 5 and test out your creation.

Now that’s really all we need to do for the Cinemachine camera but I do have one thing that I want to address before I end this tutorial. If you have a look at our Inspector window of our Player game object with the PlayerController script attached to it, you’ll see all of our variables stacked up like this.

This may not bother you but it bothers ME soOo much.

To clean this up I’ll be adding Header sections to my code: (as show below)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [Header("Movement Variables:")]

    public float moveSpeed;
    public float jumpForce;
    public float gravityScale = 5f;
    public float rotateSpeed;

    private Vector3 moveDirection;

    [Header("Referenced Objects:")]
    public CharacterController charController;
    public Camera theCamera;
    public GameObject thePlayer;

    // Start is called before the first frame update
    void Start()
    {
        theCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        float ySave = moveDirection.y;
        //moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));

        moveDirection = (transform.forward * Input.GetAxisRaw("Vertical")) + (transform.right * Input.GetAxisRaw("Horizontal"));
        moveDirection = moveDirection * moveSpeed;
        moveDirection.y = ySave;

        if(Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpForce;
        }

        moveDirection.y += Physics.gravity.y * Time.deltaTime * gravityScale;

        charController.Move(moveDirection * Time.deltaTime);

        // transform.position = transform.position + (moveDirection * Time.deltaTime * moveSpeed);
        if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
        {
            transform.rotation = Quaternion.Euler(0f, theCamera.transform.rotation.eulerAngles.y, 0f);
            Quaternion faceRotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0f, moveDirection.z));
            thePlayer.transform.rotation = Quaternion.Slerp(thePlayer.transform.rotation, faceRotation, rotateSpeed * Time.deltaTime);
        }
    }
}

These won’t affect your code in any way but they will organise your variables in the Inspector window. (Which looks so much better.)

That’s all for this tutorial. I hope you enjoyed it. Leave a comment if you didn’t understand something or drop me a tweet @zaydcarelse .

In the next episode we’ll look at adding Kay Lousberg’s animations to the character.

Until then, stay safe!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s