Why is my camera accumulating difference on rotation?

Kind of solved:
In my head the old solution made perfect sense, and still does to be honest. Due to how small the imprecision was and how much it needed to accumulate to be noticeable, I suspect (albeit with no proof) it was caused by float imprecision.
The solution that worked for me was storing the last walk input from the player in the input class to be available at all times. This way the rotation is made relative to the fresh input direction, not by vectors that had been transformed many times, possibly accumulating float imprecisions.

First of all, sorry for bothering you guys. This is my first time using unity (only had very little experience in pygame) and I’ve been following a few tutorials for the past days, but found some problems.

I’m trying to implement a first person character controller using the new input system and cinemachine.
My solution seemed to work, but I noticed that when walking while rotating the camera, the character rotation and movement direction are having a very small desynchronization that accumulates over time.
If I press W and rotate my camera aggressively, the rotation differences can be so big that the character may move backwards while the camera faces forward. I’ve been debugging the code for this for a couple of days and couldn’t find the issue.

Video of me pressing only W and rotating the camera, causing the movement to go in the wrong direction instead of forward.

I have a camera manager class, a player movement management class and a input manager class.
In my current project structure, the input manager calls the player movement and camera manager classes when their respective actions are detected by the new input system.

This is the call flow for when the player moves the mouse:
Input Manager

void OnMoveCameraPerformed(InputAction.CallbackContext context)
        {
            Vector2 cameraInput = context.ReadValue<Vector2>();
            Vector3 oldProjectedCameraForward = cameraManager.activeCamera.transform.forward;

            cameraManager.RotateFromInput(cameraInput);
     
            // adjust the walk direction to match the direction the player is looking at (very likely where the problem resides)
            if (!_playerMovement.IsWalking)
                return;
     
            oldProjectedCameraForward.y = 0f;
            oldProjectedCameraForward.Normalize();
     
            Vector3 newProjectedCameraForward = cameraManager.activeCamera.transform.forward;
            newProjectedCameraForward.y = 0f;
            newProjectedCameraForward.Normalize();
            _playerMovement.NextWalk = Quaternion.FromToRotation(oldProjectedCameraForward, newProjectedCameraForward)
                                       * _playerMovement.NextWalk;
        }

Assuming nextwalk is not Vector3.zero, then the player is already moving towards a direction. I hope this little sketch clarifies:


So, what the Quaternion.FromToRotation(oldProjectedCameraForward, newProjectedCameraForward) * _playerMovement.NextWalk; line is trying to do is get the rotation from the red to the purple vector, and applying it to the blue one.

Camera rotation method

    public override void RotateFromInput(Vector2 horizontalVerticalInput)
    {
        // input treatment
        float horizontalRotationDelta =
            horizontalVerticalInput.x * Time.deltaTime * _defaultMultiplier * mouseSensitivity;
        float verticalRotationDelta =
            -horizontalVerticalInput.y * Time.deltaTime * _defaultMultiplier * mouseSensitivity;

        // adding to class rotation and clamping it. (rotation saved in class so that it's easier to clamp and not let the player do a 360 with the vertical camera rotation)
        _verticalRotation += verticalRotationDelta;
        _verticalRotation = Mathf.Clamp(_verticalRotation, -90f, 90f);
        _horizontalRotation += horizontalRotationDelta;

        // Setting the new rotation from class rotation.
        Vector3 newRotation = new Vector3(_verticalRotation, _horizontalRotation, 0f);

        transform.localEulerAngles = newRotation;
        playerTransform.localEulerAngles += new Vector3(0f, horizontalRotationDelta, 0f);
    }

This is the flow for when the player presses WASD:

Input manager

        void OnWalkPerformed(InputAction.CallbackContext context)
        {
            Vector3 walkInput = context.ReadValue<Vector3>();
            Vector3 projectedCameraForward = cameraManager.mainCamera.transform.forward;
            projectedCameraForward.y = 0f;
            projectedCameraForward.Normalize();
            Vector3 walkDirection = Quaternion.FromToRotation(Vector3.forward, projectedCameraForward) * walkInput;
            _playerMovement.WalkTowards(walkDirection);
        }

If I understood the input system correctly correctly, this is only called if the player changes the WASD vector, which means it will only be called the first time.
Consequently, rotating the camera will not automatically change the player’s movement direction, which is why in the camera rotating code I try to rotate the walk vector accordingly. (probably where the bug resides)

Player movement

        public void WalkTowards(Vector3 direction)
        {
            NextWalk = _walkSpeed/_accelerationTimeSeconds * direction;
        }

(note that this sort of schedules the walk instead of applying it immediately. this is because the actual motion is applied only on the next fixedupdate, aiming to follow the recommendation of only doing physics stuff on fixedupdate)

The flow of the playermovement’s fixedUpdate:

FixedUpdate

        public void FixedUpdate()
        {
            ApplyWalk();
            Move();
        }

ApplyWalk()

        private void ApplyWalk()
        {
            if (NextWalk == Vector3.zero)
            {
                Vector3 brakeVector = walkVelocity.normalized * _walkSpeed / _accelerationTimeSeconds;
                // if the brake vector is bigger than the velocity vector, set v3.zero to avoid going backwards.
                if (brakeVector.magnitude > walkVelocity.magnitude)
                {
                    walkVelocity = Vector3.zero;
                    return;
                }
                walkVelocity -= brakeVector;
                return;
            }
            walkVelocity += NextWalk;
            if (walkVelocity.magnitude >= _walkSpeed)
            {
                walkVelocity = walkVelocity.normalized* _walkSpeed;
            }
        }

Move()

private void Move()
        {

            _hitbox.MovePosition(transform.position + (_playerVelocity + walkVelocity) * Time.fixedDeltaTime);
        }

So, welcome to the forum. This is your first post. You win the “Best Formatted First Post” and “Most Thorough First Post” award for the year. Maybe for the past decade.

I don’t have a complete understanding of your code, but this below is a code smell to me. You’re setting the camera’s rotation explicitly, but you’re turning the player’s rotation by a delta. If you want your camera and player to always be in sync, then either set them both explicitly, or set one explicitly from the other after it has been adjusted.

transform.localEulerAngles = newRotation;
playerTransform.localEulerAngles += new Vector3(0f, horizontalRotationDelta, 0f);

Definitely pepper your code with Debug.Log() and study the interplay of variables, if you can’t just single-step through with a debugger. It might be something about crossing a 360/0 barrier on your angles or something simple like that.

In terms of learning the API, you may be interested in Vector3.ProjectOnPlane(), and Vector3.ClampMagnitude(). Doing it your way works, but can be less readable or maintainable if you later use your code in different situations.

2 Likes

Well thank you very much haha. I know from trying to help friends on college how mentalizing other people’s logic can be hard… in the end I’m trying to help you guys help me XD.

I’ll definitely test your suggestions and points and update the post, thanks.

I found a kind of unrelated solution, but your answer made me learn a bunch of new stuff and put me in the right track. Thank you!