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);
}