Third person Camera rotation problem

Howdy I’m working on a third person game. I have a problem with the camera when I rotate it on the Y axis. This changes its position X when it reaches a certain rotation. I want to keep the X position that I have when I rotate the camera but I do not succeed … Here is a video with what happens: bandicam 2020-04-03 04-58-41-522.mp4 - Google Drive

Below is the code:

cameraController.cs

public class CameraController : MonoBehaviour
 {
 
     public Transform target; //Player
     public Transform pivot; //Pivot for rotating camera
     public Vector3 offset;
     public bool useOffsetValues;
     public float rotateSpeed;
     public float maxViewAngle;
     public float minViewAngle;
     public bool invertY;
 
     void Start()
     {
       if (!useOffsetValues)
       {
         offset = target.position - transform.position;
       }
       pivot.transform.position = target.transform.position;
       //pivot.transform.parent = target.transform;
       pivot.transform.parent = null;
 
       Cursor.lockState = CursorLockMode.Locked; //Hides Mouse's Cursor
     }
 
     void LateUpdate()
     {
       pivot.transform.position = target.transform.position;
       //Rotating Player by Mouse X Axis
       float horizontal = Input.GetAxis("Mouse X") * rotateSpeed;
       pivot.Rotate(0, horizontal, 0);
 
       //Rotating Player's Pivot by Mouse Y Axis
       float vertical = Input.GetAxis("Mouse Y") * rotateSpeed;
       //pivot.Rotate(-vertical, 0, 0);
       if (invertY)
       {
         pivot.Rotate(vertical, 0, 0);
       } else
       {
         pivot.Rotate(-vertical, 0, 0);
       }
 
       //Limit up/down Camera rotation
       if (pivot.rotation.eulerAngles.x > maxViewAngle && pivot.rotation.eulerAngles.x < 180f)
       {
         pivot.rotation = Quaternion.Euler(maxViewAngle, pivot.rotation.eulerAngles.y, 0);
       }
       if (pivot.rotation.eulerAngles.x > 180f && pivot.rotation.eulerAngles.x < 360f + minViewAngle)
       {
         pivot.rotation = Quaternion.Euler(360f + minViewAngle, pivot.rotation.eulerAngles.y, 0);
       }
 
       //Rotate Camera by Mouse X Axis
       float desiredYAngle = pivot.eulerAngles.y;
       float desiredXAngle = pivot.eulerAngles.x;
       Quaternion rotation = Quaternion.Euler(desiredXAngle, desiredYAngle, 0);
       transform.position = target.position - (rotation * offset);
 
       //Camera follow Player
       if (transform.position.y < target.position.y)
       {
         transform.position = new Vector3(transform.position.x, target.position.y -.5f, transform.position.z);
       }
       transform.LookAt(target);
     }
 }

playerController.cs

public class PlayerController : MonoBehaviour
{

    public float moveSpeed;
    public float jumpForce;
    public CharacterController controller;
    public float gravityScale;
    public Animator anim;
    public Transform pivot;
    public float rotateSpeed;
    public GameObject playerModel;

    private Vector3 moveDirection;

    void Start()
    {
      controller = GetComponent<CharacterController>();
    }

    void Update()
    {
      float yStore = moveDirection.y;
      moveDirection = (transform.forward * Input.GetAxis("Vertical")) + (transform.right * Input.GetAxis("Horizontal"));
      moveDirection = moveDirection.normalized * moveSpeed;
      moveDirection.y = yStore;

      if (controller.isGrounded)
      {
        moveDirection.y = 0f;
        if (Input.GetButtonDown("Jump"))
        {
          moveDirection.y = jumpForce;
        }
      }

      //Applying gravity
      moveDirection.y = moveDirection.y + (Physics.gravity.y * gravityScale * Time.deltaTime);
      controller.Move(moveDirection * Time.deltaTime);

      if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
      {
        transform.rotation = Quaternion.Euler(0f, pivot.rotation.eulerAngles.y, 0);
        Quaternion newRotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0f, moveDirection.z));
        playerModel.transform.rotation = Quaternion.Slerp(playerModel.transform.rotation, newRotation, rotateSpeed * Time.deltaTime);
      }

      anim.SetBool("isGrounded", controller.isGrounded);
      anim.SetFloat("Speed", (Mathf.Abs(Input.GetAxis("Vertical")) + Mathf.Abs(Input.GetAxis("Horizontal"))));
    }
}

@unity_QPt4yqS82mflYg, I don’t see where the problem is, but the code seems a bit complicated. For one, the pivot rotation is calculated and clamped on the x and y axes, and the Eulers are extracted from that to create to create the final rotation. Maybe I’m missing it, but not just apply the pivot rotation directly and skip that step?

Have you put a debugging statement in the see what the Euler values of x and y when it happens? You might be hitting some kind floating point rounding problem if the values are very small, and you are extracting them from a Quaternion and then applying them to another Quaternion - I don’t know.

Another thing that probably does not anything to do with it, but I think the code should calculate the offset of the camera in every frame.

Here is a script I wrote from scratch about a month ago that seems to work perfectly. You can limit both axes rotation, lock both, or let them rotate free. The only thing it does not do yet is “chase” the player - the offset distance to the camera does not change, so the camera remains locked to the player. In the Update loop, you’ll need to delete all the mouse down statements, and just leave the 2 input statements.

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

public class CameraFollow : MonoBehaviour {

	[Header( "Target" )]
	public Transform target;
	[Header( "Camera will be placed behind target", order = 1 )]
	[Header( "At distance and Min Pitch Degrees", order = 2 )]
	[Header(" ", order = 2 )]
	public float distance = 3.5f;
	
	[Header( "Camera Rotation Speeds" )]
	public float y_sensitivity = 1f;
	public float x_sensitivity = 1f;

	[Header( "Rotation Clamps In Degrees -180 to 180", order = 1 )]
	[Header( "Max < Min Free Rotation, Min = Max Lock Rotation", order = 2 )]


	public float maxPitchDegrees = 70f;
	public float minPitchDegrees = 20f;
	public float maxYawDegrees = 90f;
	public float minYawDegrees = -90f;

	[Header( "Rotation Clamp Overrides" )]
	public bool pitchRotLocked = false;
	public bool yawRotLocked = false;
	public bool pitchRotFree = false;
	public bool yawRotFree = false;

	// Other private variables
	private Vector3 offset;
	private float pitch, yaw;
	private Quaternion oldTargetRotation;
	private bool dragging = false;
	private Transform my; // shortcut to this camera transform
	private Transform tg; // shortcut to the target

	void Start() {

		my = transform; // make short cuts
		tg = target;

		// set my position using distance and minPitchDegrees
		float z = Mathf.Sin( minPitchDegrees ) * distance;
		float y = Mathf.Cos( minPitchDegrees ) * distance;
		my.position = new Vector3( tg.position.x,
			tg.position.y + y, tg.position.z - z );
		my.LookAt( tg.transform, Vector3.up ); // start out focusing on object

		// record these for use later
		offset = my.position - tg.position;
		oldTargetRotation = tg.transform.rotation;

	}

	// Update
	void Update() {
		// Get axis of X & Y using mouse to use when we
		// orbit the mouse around the object
		// here we are making the user left click and drag
		// if you want rotation without dragging, delete all the mousedown code
		//
		if( Input.GetMouseButtonDown( 0 ) ) {
			dragging = true;
		}

		if( Input.GetMouseButtonUp( 0 ) ) {
			dragging = false;
		}

		pitch = 0;
		yaw = 0;
		if( Input.GetMouseButton( 0 ) && dragging ) {
			if( !pitchRotLocked ) pitch = Input.GetAxis( "Mouse Y" ) * y_sensitivity;
			if( !yawRotLocked ) yaw = Input.GetAxis( "Mouse X" ) * x_sensitivity;
		}
	}

	private void FixedUpdate() {

		// For each input axis:
		// 1 if have input axis motion
		// 2	if we do not have free rotation we need to clamp rotation:
		// 3		get preview angle for rotateAround for each axis:
		// 3.a			subtract any target rotation then
		// 3.b			add to rotateAround angle
		// 4		get preview angle in min/max degrees
		// 5		clamp preview angle to min/max if input in direction of limit
		// 6		if rotation allowed, rotate around
		// 7	else free rotate around
		//
		if( Mathf.Abs( pitch ) > 0 ) { // 1
			if( !pitchRotFree ) {  // 2
				Vector3 prevAngle = ( ( my.rotation * Quaternion.Inverse( tg.rotation ) ) *
					Quaternion.AngleAxis( pitch, -my.right ) ).eulerAngles; // 3.a & 3.b
				float xDeg = RotToDeg( prevAngle.x ); // 4
				if( xDeg < minPitchDegrees && pitch > 0 ) pitch = 0; // 5
				else if( xDeg > maxPitchDegrees && pitch < 0 ) pitch = 0; // 5
				if( Mathf.Abs( pitch ) > 0 ) my.RotateAround( tg.position, -my.right, pitch ); // 6
			} else my.RotateAround( tg.position, -my.right, pitch ); // 7
		}

		if( Mathf.Abs( yaw ) > 0 ) { // 1
			if( !yawRotFree ) { // 2
				Vector3 prevAngle = ( ( my.rotation * Quaternion.Inverse( tg.rotation ) ) *
					Quaternion.AngleAxis( yaw, Vector3.up ) ).eulerAngles; // 3.a & 3.b
				float yDeg = RotToDeg( prevAngle.y ); // 4
				if( yDeg < minYawDegrees && yaw < 0 ) yaw = 0; // 5
				else if( yDeg > maxYawDegrees && yaw > 0 ) yaw = 0; // 5
				if( Mathf.Abs( yaw ) > 0 ) my.RotateAround( tg.position, Vector3.up, yaw ); // 6
			} else my.RotateAround( tg.position, -my.right, pitch ); // 7
		}

		// update my offset vector of any rotateAround based on target position
		offset = my.position - tg.position;

		// update my offset vector if we have rotated my target in its movement script
		// 1 if the current target rotation != to last frame's target rotation:
		// 2	subtract the current target rotation from old rotation to get the difference
		// 3	add the difference to my current offset vector
		// 4	add the difference to my current rotation
		// 5	reset old target rotation for next frame
		//
		if( Quaternion.Dot( tg.rotation, oldTargetRotation ) <= 1f - 0.000001f ) { // 1
			Quaternion deltaRot = tg.rotation * Quaternion.Inverse( oldTargetRotation ); // 2
			offset = deltaRot * offset; // 3
			my.rotation = deltaRot * my.rotation; // 4
			oldTargetRotation = tg.rotation; // update for next time around
		}
		// finally, apply all the offset changes to my position
		my.position = tg.position + offset;
	}

	private float RotToDeg( float rot ) {
		float deg = 0;
		if( rot > 180f ) {
			deg = rot - 360f;
		} else {
			deg = rot;
		}
		return deg;
	}

	private void OnValidate() {
		if( maxPitchDegrees > 180 ) {
			maxPitchDegrees = 180;
		} else if( maxPitchDegrees < -180 ) {
			maxPitchDegrees = -180;
		}
		if( minPitchDegrees >= maxPitchDegrees ) {
			minPitchDegrees = maxPitchDegrees;
			pitchRotLocked = true;
		} else if( minPitchDegrees < -180 ) {
			minPitchDegrees = -180;
			pitchRotLocked = false;
		}

		if( maxYawDegrees > 180 ) {
			maxYawDegrees = 180;
		} else if( maxYawDegrees < -180 ) {
			maxYawDegrees = -180;
		}
		if( minYawDegrees >= maxYawDegrees ) {
			minYawDegrees = maxYawDegrees;
			yawRotLocked = true;
		} else if( minYawDegrees < -180 ) {
			minYawDegrees = -180;
			yawRotLocked = false;
		}
	}
}

@streeetwalker
Thanks for your answer, but I think the problem is from here…

if (pivot.rotation.eulerAngles.x > maxViewAngle && pivot.rotation.eulerAngles.x < 180f)
        {
          pivot.rotation = Quaternion.Euler(maxViewAngle, pivot.rotation.eulerAngles.y, 0);
        }
        if (pivot.rotation.eulerAngles.x > 180f && pivot.rotation.eulerAngles.x < 360f + minViewAngle)
        {
          pivot.rotation = Quaternion.Euler(360f + minViewAngle, pivot.rotation.eulerAngles.y, 0);
        }

Or here

//Rotate Camera by Mouse X Axis
      float desiredYAngle = pivot.eulerAngles.y;
      float desiredXAngle = pivot.eulerAngles.x;
      Quaternion rotation = Quaternion.Euler(desiredXAngle, desiredYAngle, 0);
      transform.position = target.position - (rotation * offset);

I can’t figure out… 2 days of searching and modifying and nothing…
I also tried as you said to take offset for each frame, but then the camera starts to spin very fast and does not keep the values.

I followed this tutorial for the camera.

The Y axis of the offset is set to -2. If I change it in 2 it doesn’t do the same if I keep the camera on the ground level but if I try to raise it it makes it worse.