3rd person jitter and lag on player when moving the player and rotating the camera

Hello everyone! I’ve set up my own basic 3rd person controller system from scratch.

The bug I’ve been experiencing is that whenever I move the character in any direction while rotating the camera in any direction, the player is supposed to rotate with the camera smoothly, but lags behind and jitters a lot. I’ve also noticed that the camera movement is also a little janky.

I know that this description is not adequate, so I’ve attached a link to the Unity Build in question just so you can experience it for yourself, if you wish:

I’ve also attached a link to a video of the jitter as well:

THIS IS HOW THE PLAYER AND CAMERA ARE SET UP IN MY UNITY PROJECT:
o CameraManager (Contains the CameraManager script, no other components)
o CameraPivot (Empty game object)
o MainCamera (Camera component)

o Player (Dynamic Rigidbody that is Interpolated and frozen rotation on X, Y, and Z axes and no Gravity, PlayerLocomotion script, Capsule mesh, Capsule Collider)

The CameraManager script:

using UnityEngine;

public class CameraManager : MonoBehaviour
{
    [SerializeField] private Transform cameraPivotTransform;

    [Header("Camera Values")]
    [SerializeField] private float cameraFollowSpeed = 0.2f;
    [SerializeField] private float cameraLookSpeed = 2f;
    [SerializeField] private float cameraPivotSpeed = 2f;
    [SerializeField] private float minimumPivotAngle = -35f;
    [SerializeField] private float maximumPivotAngle = 35f;
    [SerializeField] private float cameraCollisionRadius = 0.2f;
    [SerializeField] private LayerMask collisionLayers; // Layers we want out camera to collide with
    [SerializeField] private float cameraCollisionOffset = 0.2f; // How much the camera will jump off of objects its colliding with
    [SerializeField] private float minimumCollisionOffset = 0.2f; // How much the camera will jump off of objects its colliding with

    private float lookAngle; // Camera looking up and down
    private float pivotAngle; // Camera looking left and right
    private float defaultPosition;

    private float inputX;
    private float inputY;

    private Transform targetTransform; // The transform the camera will follow
    private Transform cameraTransform;
    private Vector3 cameraFollowVelocity = Vector3.zero;
    private Vector3 cameraVectorPosition;

    private void Awake()
    {
        targetTransform = FindFirstObjectByType<PlayerLocomotion>().transform;
        cameraTransform = Camera.main.transform;
        defaultPosition = cameraTransform.localPosition.z;
    }

    private void Update()
    {
        inputX = PlayerInputListener.instance.lookDirection.x;
        inputY = PlayerInputListener.instance.lookDirection.y;
    }

    private void LateUpdate()
    {
        FollowTarget();
        RotateCamera();
        HandleCameraCollisions();
    }

    private void FollowTarget()
    {
        Vector3 targetPosition = Vector3.SmoothDamp(transform.position, targetTransform.position, ref cameraFollowVelocity, cameraFollowSpeed * Time.deltaTime);
        transform.position = targetPosition;
    }

    private void RotateCamera()
    {
        Vector3 rotation;
        Quaternion targetRotation;

        lookAngle += inputX * cameraLookSpeed * Time.deltaTime;
        pivotAngle -= inputY * cameraPivotSpeed * Time.deltaTime;
        pivotAngle = Mathf.Clamp(pivotAngle, minimumPivotAngle, maximumPivotAngle);

        rotation = Vector3.zero;
        rotation.y = lookAngle;
        targetRotation = Quaternion.Euler(rotation);
        transform.rotation = targetRotation;

        rotation = Vector3.zero;
        rotation.x = pivotAngle;
        targetRotation = Quaternion.Euler(rotation);
        cameraPivotTransform.localRotation = targetRotation;
    }

    private void HandleCameraCollisions()
    {
        float targetPosition = defaultPosition;
        RaycastHit hit;
        Vector3 direction = cameraTransform.position - cameraPivotTransform.position;
        direction.Normalize();

        if (Physics.SphereCast(cameraPivotTransform.position, cameraCollisionRadius, direction, out hit, Mathf.Abs(targetPosition), collisionLayers))
        {
            float distance = Vector3.Distance(cameraPivotTransform.position, hit.point);
            targetPosition = -(distance - cameraCollisionOffset);
        }

        if (Mathf.Abs(targetPosition) < minimumCollisionOffset)
        {
            targetPosition -= minimumCollisionOffset;
        }

        cameraVectorPosition.z = Mathf.Lerp(cameraTransform.localPosition.z, targetPosition, 0.2f);
        cameraTransform.localPosition = cameraVectorPosition;
    }
}

The PlayerLocomotion script:

using UnityEngine;

public class PlayerLocomotion : MonoBehaviour
{
    private Vector3 movementDirection;
    private Transform cameraTransform;
    private Rigidbody rb;

    private float inputX;
    private float inputY;

    [SerializeField] private float movementSpeed = 7f;
    [SerializeField] private float rotationSpeed = 15f;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
        cameraTransform = Camera.main.transform;
    }

    private void Update()
    {
        inputX = PlayerInputListener.instance.movementDirection.x;
        inputY = PlayerInputListener.instance.movementDirection.y;
    }

    private void FixedUpdate()
    {
        HandleMovement();
        HandleRotation();
    }

    public void HandleMovement()
    {
        movementDirection = cameraTransform.forward * inputY;
        movementDirection += cameraTransform.right * inputX;
        movementDirection.y = 0f;
        movementDirection.Normalize();
        movementDirection *= movementSpeed;

        Vector3 movementVelocity = movementDirection;
        rb.linearVelocity = movementVelocity;
    }

    public void HandleRotation()
    {
        Vector3 targetDirection = Vector3.zero;

        targetDirection = cameraTransform.forward * inputY;
        targetDirection += cameraTransform.right * inputX;
        targetDirection.y = 0f;
        targetDirection.Normalize();

        if (targetDirection == Vector3.zero)
        {
            targetDirection = transform.forward;
        }

        Quaternion targetRotation = Quaternion.LookRotation(targetDirection);
        Quaternion playerRotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);

        rb.MoveRotation(playerRotation);
    }
}

OTHER POINTS:

  • I made a build for the project, and the problem still persists in the build.
  • I know using Cinemachine might be the right thing to do, but I just want to make a basic 3rd person controller from scratch :slight_smile:

I would seriously appreciate some help on how to make the player rotate smoothly with the camera! I’m kind of losing my mind :slight_smile: I’ve spent way too much time trying to fix this.

If you insist on making your own camera controller, do not fiddle with camera rotation.

The simplest way to do it is to think in terms of two Vector3 points in space:

  1. where the camera is LOCATED
  2. what the camera is LOOKING at
private Vector3 WhereMyCameraIsLocated;
private Vector3 WhatMyCameraIsLookingAt;

void LateUpdate()
{
  cam.transform.position = WhereMyCameraIsLocated;
  cam.transform.LookAt( WhatMyCameraIsLookingAt);
}

Then you just need to update the above two points based on your GameObjects, no need to fiddle with rotations. As long as you move those positions smoothly, the camera will be nice and smooth as well, both positionally and rotationally.

For a third-person camera I would try a “look from” that’s just behind and to one side of the player, and a “look at” that is in front. Lerp to smooth 'em out a bit, see where you get.

But still, camera stuff is pretty tricky… as you already noted in your post, all the Kool Kids are using Cinemachine from the Unity Package Manager.

There’s even a dedicated Camera / Cinemachine area: see left panel.

1 Like

HandleRotation should be called from Update instead of FixedUpdate. This is because you’re using MoveRotation on a dynamic rigidbody.

You should be able to simplify your camera script by using LookAt to make it look at the player or by using RotateAround to rotate around the player.

1 Like

Thank you for your replies! I will try both these things later today when I have the chance

Sorry for the late reply, but I tried both of your suggestions and they both helped A LOT! It’s not perfect just yet, but the camera is wayyyyy smoother.

@Kurt-Dekker your answer has made the camera incredibly smooth, and @zulo3d, after I moved the HandleRotation() method to Update instead of LateUpdate then the rotation of the player was also way smoother. So thank you both so much for your amazing contributions!

However it’s still not exactly perfect. There is still a little bit of jitter on the player when moving the player character in any direction while moving the camera as well. However, it is so little that you won’t be able to see it in the recordings I took of it :sweat_smile:. So if you want to see it then use the updated build:
https://drive.google.com/file/d/1qC3U3wUdq0JBrhp4jmdAsAaQDMar-zwd/view?usp=sharing
The best way to see the jitter is to continuously rotate the camera around the player while walking in any direction. The jitter is more pronounced the faster the camera is moving around the player.

UPDATED SETUP:
I changed the MainCamera to not have any parents in the hierarchy, and the only thing I changed in the PlayerLocomotion script was moving the HandleRotation() method into the Update function. This is the updated Camera script which is attached to the MainCamera:

using UnityEngine;

public class CameraManager : MonoBehaviour
{
    [Header("Camera Values")]
    [SerializeField] private float cameraMovementSpeed;
    [SerializeField] private float cameraSmoothMovementSpeed;
    [SerializeField] private float cameraSmoothRotationSpeed;

    private float inputX;
    private float inputY;

    private Transform targetTransform; // The transform the camera will follow

    private Vector3 thirdPersonCameraOffsetVector; // The position of the camera where the player's position is the origin
    private Vector3 targetCameraPosition; // The target position of the camera in world space
    private Vector3 targetLookAtPosition; // The position the camera should be looking at

    private void Awake()
    {
        targetTransform = FindFirstObjectByType<PlayerLocomotion>().transform;
    }

    private void Start()
    {
        thirdPersonCameraOffsetVector = new Vector3(0f, 0f, -3f);
        targetLookAtPosition = targetTransform.position;
    }

    private void Update()
    {
        inputX = PlayerInputListener.instance.lookDirection.x;
        inputY = PlayerInputListener.instance.lookDirection.y;
    }

    private void LateUpdate()
    {
        FollowAndLookAtTarget();
    }

    private void FollowAndLookAtTarget()
    {
        thirdPersonCameraOffsetVector = Quaternion.AngleAxis(inputX * cameraMovementSpeed * Time.deltaTime, Vector3.up) * thirdPersonCameraOffsetVector;
        thirdPersonCameraOffsetVector = Quaternion.AngleAxis(-inputY * cameraMovementSpeed * Time.deltaTime, transform.right) * thirdPersonCameraOffsetVector;
        targetCameraPosition = targetTransform.position + thirdPersonCameraOffsetVector;
        transform.position = Vector3.Lerp(transform.position, targetCameraPosition, Time.deltaTime * cameraSmoothMovementSpeed);

        targetLookAtPosition = Vector3.Lerp(targetLookAtPosition, targetTransform.position, Time.deltaTime * cameraSmoothRotationSpeed);
        transform.LookAt(targetLookAtPosition);
    }
}

If anyone could tell me how to fix the tiny jitter that is left I would be very grateful :slight_smile:

I just noticed that you didn’t freeze the Y axis on the rigidbody. You should freeze X, Y and Z. MoveRotation will still be able to rotate the rigidbody despite them all being frozen.

On line 46 of your latest camera script you should use Slerp instead of Lerp. Slerp rotates to the target.

If the input values are from the mouse then you should remove the deltaTime from lines 43 and 44. The mouse is already frame rate independent.

If your camera will always be looking at the player then there’s not really any need to lerp the LookAt. So remove line 48 and pass the targetTransform directly into LookAt.

I have frozen the Y-axis now thank you! I’m also using Slerp now on line 46 and have removed the deltaTime on lines 43 and 44. And the issue is still happening.

I don’t want the camera to always be looking at the player so sadly I will not be able to put the Transform of the player into the LookAt. However, I did try it out just to see what would happen, and the player was a little bit smoother after that, but there was still jitter when rotating the player and the camera at the same time.

I honestly have no idea how to fix this. Any more ideas? :pray:

Here is the updated CameraManager file just for convenience:

using UnityEngine;

public class CameraManager : MonoBehaviour
{
    [Header("Camera Values")]
    [SerializeField] private float cameraMovementSpeed;
    [SerializeField] private float cameraSmoothMovementSpeed;
    [SerializeField] private float cameraSmoothRotationSpeed;

    private float inputX;
    private float inputY;

    private Transform targetTransform; // The transform the camera will follow

    private Vector3 thirdPersonCameraOffsetVector; // The position of the camera where the player's position is the origin
    private Vector3 targetCameraPosition; // The target position of the camera in world space
    private Vector3 targetLookAtPosition; // The position the camera should be looking at

    private void Awake()
    {
        targetTransform = FindFirstObjectByType<PlayerLocomotion>().transform;
    }

    private void Start()
    {
        thirdPersonCameraOffsetVector = new Vector3(0f, 0f, -3f);
        targetLookAtPosition = targetTransform.position;
    }

    private void Update()
    {
        inputX = PlayerInputListener.instance.lookDirection.x;
        inputY = PlayerInputListener.instance.lookDirection.y;
    }

    private void LateUpdate()
    {
        FollowAndLookAtTarget();
    }

    private void FollowAndLookAtTarget()
    {
        thirdPersonCameraOffsetVector = Quaternion.AngleAxis(inputX * cameraMovementSpeed, Vector3.up) * thirdPersonCameraOffsetVector;
        thirdPersonCameraOffsetVector = Quaternion.AngleAxis(-inputY * cameraMovementSpeed, transform.right) * thirdPersonCameraOffsetVector;
        targetCameraPosition = targetTransform.position + thirdPersonCameraOffsetVector;
        transform.position = Vector3.Slerp(transform.position, targetCameraPosition, Time.deltaTime * cameraSmoothMovementSpeed);

        targetLookAtPosition = Vector3.Lerp(targetLookAtPosition, targetTransform.position, Time.deltaTime * cameraSmoothRotationSpeed);
        transform.LookAt(targetLookAtPosition);
    }
}