I've been having trouble with Quaternion lerps. I'm simply looking to rotate my character 90 degrees based on their current rotation. The script below executes that almost perfectly, except for the fact that my character rotates a full 90 degrees long before rotationTime reaches the max value of 1. For some reason, the value of rotationTime is not properly synced with the progress of the lerp, and I can't seem to figure out why. What am I missing?
public class Movement : MonoBehaviour
{
bool Rotating = false;
Quaternion targetRotation;
public float rotationTime = 0f;
public float speed = 0.1F;
private Rigidbody rb;
private void Awake(){
rb = GetComponent<Rigidbody>();
}
public void Turn(InputAction.CallbackContext context){ //executes when 'E' is pressed
if (context.started && Rotating == false) {
targetRotation = Quaternion.Euler(0,transform.eulerAngles.y 90f,0);
rotationTime = 0;
Rotating = true;
}
}
void Update() {
if (Rotating == true) {
rotationTime = rotationTime Time.deltaTime * speed;
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation,rotationTime);
};
if (rotationTime > 1) {
Rotating = false;
}
}
}
CodePudding user response:
I suspect that the main issue here is that you are using transform.rotation
in your Quaternion.Lerp
. You change it every frame, so every frame the start rotation will be closer to the target rotation. You should add some kind of _initRotation
variable, and set it to transform.rotation
in your Turn
method. I mean something like this:
public void Turn(InputAction.CallbackContext context)
{
if (context.started && Rotating == false)
{
targetRotation = Quaternion.Euler(0,transform.eulerAngles.y 90f,0);
_initRotation = transform.rotation;
rotationTime = 0;
Rotating = true;
}
}
...
void Update()
{
...
transform.rotation = Quaternion.Lerp(_initRotation, targetRotation,rotationTime);
...
}
Also, you have a logical issue with the lerp function. It does not affect the result in your particular case, but it can cause problems later.
You increment your rotation time by Time.deltaTime * speed
every frame, it is not correct as it is not time passed from the start of the rotation.
According to the Quaternion.Lerp
documentation, t
value is always clamped to [0, 1]. So it is more convenient to use normalized time value instead of abstract speed
value (right now it has no physical sense, it is just a multiplier).
It would be much clearer to use something like this:
void Update()
{
...
rotationTime = Time.deltaTime;
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, rotationTime / fullRotationTime);
...
}
CodePudding user response:
Generally, I almost always work with carriers, and I recommend that you do the same.
public class Movement : MonoBehaviour
{
bool Rotating = false;
Vector3 targetRotation;
public float rotationTime = 0f;
public float speed = 0.1F;
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
public void Turn(InputAction.CallbackContext context)
{ //executes when 'E' is pressed
if (context.started && Rotating == false)
{
targetRotation = new Vector3(0, transform.eulerAngles.y 90f, 0);
rotationTime = 0;
Rotating = true;
}
}
void Update()
{
if (Rotating == true)
{
rotationTime = rotationTime Time.deltaTime * speed;
transform.eulerAngles = Vector3.Lerp(transform.eulerAngles, targetRotation, rotationTime);
};
if (rotationTime > 1)
{
Rotating = false;
}
}
}
Also consider using coroutines for more readable code and to improve performance.
Good work!
CodePudding user response:
As it was mentioned already by this answer one of your main issues is that every time you use a new
transform.rotation
as interpolation start point=> Your rotation starts fast and then gets slower and slower the closer you reach the target value.
There other issues here though:
You are using the
transform.rotation.eulerAngles
! From the API:When using the .eulerAngles property to set a rotation, it is important to understand that although you are providing X, Y, and Z rotation values to describe your rotation, those values are not stored in the rotation. Instead, the X, Y & Z values are converted to the Quaternion's internal format.
When you read the .eulerAngles property, Unity converts the Quaternion's internal representation of the rotation to Euler angles. Because, there is more than one way to represent any given rotation using Euler angles, the values you read back out may be quite different from the values you assigned. This can cause confusion if you are trying to gradually increment the values to produce animation.
To avoid these kinds of problems, the recommended way to work with rotations is to avoid relying on consistent results when reading .eulerAngles particularly when attempting to gradually increment a rotation to produce animation. For better ways to achieve this, see the Quaternion * operator.
And then in general, since I see there is a
Rigibody
involved you shouldn't set or read anything directly via theTransform
component at all but rather only go through that rigidbody! Otherwise you might break/fight against the physics resulting in strange behavior and breaking collision detection.
In your case in my eyes it is way easier to control the entire thing in a Corouine which avoids the need for all the class fields and imho is way easier to understand, control and maintain:
public class Movement : MonoBehaviour
{
public float speed = 0.1F;
[SerializeField] private Rigidbody rb;
private bool isRotating = false;
private void Awake()
{
if(!rb) rb = GetComponent<Rigidbody>();
}
public void Turn(InputAction.CallbackContext context)
{
//executes when 'E' is pressed
if (context.started && !isRotating)
{
StartCoroutine(RotateRoutine());
}
}
private IEnumerator RotateRoutine()
{
// little safety check first
if(Mathf.Approximately(speed, 0f)
{
yield break;
}
// Just to be really sure you avoid any concurrent routines
if (isRotating)
{
yield break;
}
// Lock so no other routine ca be started
isRotating = true;
// wait until we are in the next physics frame
yield return new WaitForFixedUpdate();
// store the initial rotation -> go throug the rigibody not the transform
var start = rb.rotation;
var end = start * Quaternion.Euler(0, 90f, 0);
var duration = 1 / speed;
for (var rotationTime = 0f; rotationTime < duration; rotationTime = Time.deltaTime)
{
// this would be a linear growing factor from 0 to 1
var factor = rotationTime / duration;
// optionally you could add ease in and out at the ends
// basically you can add whatever curve function you like to grow from 0 to 1 within the given rotationTime
factor = Mathf.SmoothStep(0, 1, rotationTime);
// interpolate from start to end while the factor grows from 0 to 1
var rotation = Quaternion.Slerp(start,end, factor);
// again for rigibdoy rather do this instead of going through transform
rb.MoveRotation();
// again wait for the next physics update
yield return new WaitForFixedUpdate();
}
// Just to be sure to end on clean values
rb.MoveRotation(end);
// release the lock for the next routine to start
isRotating = false;
}
}