Clamping a quaternion rotation causes shaking - c#

I am using the unity3D game engine. I have a player that is a cube with a rigidbody attached to it. Gravity is turned off, and angular drag is set to 0. I am rotating it use AddTorque, but I want to make sure that the x angle is never more than the rotationClampLow (currently at 45 degrees) and never less than rotationClampHigh (currently 315 degrees). So the x angle can be anything except for greater than 45 or less than 315.
This is the code I currently have:
// rotationSpeed is a float currently set at 3.
void RotatePlayer () {
horizontalRotation = rotationSpeed * Input.GetAxisRaw("HorizontalRotate");
verticalRotation = rotationSpeed * Input.GetAxisRaw("VerticalRotate");
if ( horizontalRotation != 0 )
Debug.Log("Horizontal rotation input: " + horizontalRotation);
ClampPlayerRotation();
rb.angularVelocity = Vector3.zero;
rb.AddTorque(horizontalRotation, verticalRotation, 0f, ForceMode.VelocityChange);
}
void ClampPlayerRotation () {
// I use 180 because it is halfway between 45 and 315
if ( rb.rotation.eulerAngles.x >= rotationClampLow && rb.rotation.eulerAngles.x <= 180) {
// Make sure that the x rotation isn't more than the low clamp
transform.eulerAngles = new Vector3(rotationClampLow, rb.rotation.eulerAngles.y, rb.rotation.eulerAngles.z);
//Make sure no positive torque is added to the x axis
if ( horizontalRotation > 0 )
horizontalRotation = 0;
}
else if ( rb.rotation.eulerAngles.x <= rotationClampHigh && rb.rotation.eulerAngles.x > 180 ) {
// Make sure that the x rotation isn't less than the high clamp
transform.eulerAngles = new Vector3(rotationClampHigh, rb.rotation.eulerAngles.y, rb.rotation.eulerAngles.z);
//Make sure no negative torque is added to the x axis
if ( horizontalRotation < 0 )
horizontalRotation = 0;
}
Currently when the x rotation reaches 45, the cube stops rotation, but when it reaches 315 it starts to jitter between 310 and 315 degrees. How can this problem be resolved? Any help would be appreciated.
Edit: Here is what is happening
Edit 2: RotatePlayer() is being called from FixedUpdate().
Edit 3: Changed code from:
rb.rotation = Quaternion.Euler(new Vector3(rotationClampLow, rb.rotation.eulerAngles.y, rb.rotation.eulerAngles.z));
To:
transform.eulerAngles = new Vector3(rotationClampLow, rb.rotation.eulerAngles.y, rb.rotation.eulerAngles.z);

Related

Camera rotation jitters after a certain threshold (video)

So I have this setup:
Camera rotation is clamped on the Y axis. It can only rotate from -75 to 75 degrees left and right, and its rotation is controlled by the mouse.
Player body can rotate around its Y axis, and its Y rotation value is added to -75 and 75 degree camera clamps in order to keep the camera rotation space in front of the player body. Example: If the player rotates 90 degrees on the Y axis, the clamp values would change to 15 and 165 degrees.
The problem arises when the player body exceeds 359 degrees on the Y axis because camera clamp values jump from 284 and 434 back to -75 and 75, which causes a noticeable snap in camera movement. How can I eliminate this snap?
Video example - Note the camera clamps (Limit L, Limit R) in the MouseLook script after 00:40
*The handleBodyRotation method just rotates the player body to face the red cube which is a child of the camera.
public class MouseLook : MonoBehaviour
{
public Transform CameraBox;
public Transform lookAtPivot;
public Transform lookAtObject;
public Transform playerBody;
public float sensitivity;
public float bodyRotSpeed;
float xRotation = 0f;
float yRotation = 0f;
public float limitL;
public float limitR;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
}
void handleCamRotation()
{
float mouseX = Input.GetAxis("Mouse X") * sensitivity * Time.deltaTime;
float mouseY = Input.GetAxis("Mouse Y") * sensitivity * Time.deltaTime;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, -45, 35);
yRotation += mouseX;
yRotation = Mathf.Clamp(yRotation, limitL, limitR);
//Handle and clamp Camera, lookAtPivot, and lookAtObject rotation
transform.rotation = Quaternion.Euler(xRotation, yRotation, 0f);
//Handle left limit and right limit
limitL = -75 + playerBody.transform.rotation.eulerAngles.y;
limitR = 75 + playerBody.transform.rotation.eulerAngles.y;
}
void handleBodyRotation()
{
Quaternion targetRotation = Quaternion.Euler(playerBody.transform.rotation.eulerAngles.x, lookAtObject.transform.rotation.eulerAngles.y, playerBody.transform.rotation.eulerAngles.z);
playerBody.transform.localRotation = Quaternion.Lerp(playerBody.transform.localRotation, targetRotation, bodyRotSpeed * Time.deltaTime);
}
void Update()
{
//Camera Rotation (clamped)
handleCamRotation();
//Body Rotation
if (Input.GetKey(KeyCode.W))
{
handleBodyRotation();
}
}
}
Your problem is the value of Transform.eulerAngles
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.
In particular the eulerAngles will always return a value between 0 and 359.9999... !
You say yourself it jumps to 284 and 434 and then back to -75 and 75.
Why?
Because
359 - 75 is 284 and 359 + 75 is 434
0 - 75 is -75 and 0 + 75 is 75.
Your eulerAngles.y value was somewhere close to the wrap point at 359.999... <-> 0.
I think you would have to wrap around these values like e.g.
limitL = -75 + playerBody.transform.rotation.eulerAngles.y;
limitR = 75 + playerBody.transform.rotation.eulerAngles.y;
if(limitR > 360) limitR -= 360;
if(limitR < 0) limitR += 360;
if(limitL > 360) limitL -= 360;
if(limitL < 0) limitL += 360;
And not sure but I guess you also would have to clamp like
if(limitR > limitL)
{
yRotation = Mathf.Clamp(yRotation, limitL, limitR);
}
else
{
yRotation = Mathf.Clamp(yRotation, limitR, limitL);
}
Don't know how Unity handled that case .. might be redundant.
Note: Typed on smartphone but I hope the idea gets clear

How can I get a 1 to -1 float value from the localEulerAngels of a third person camera in Unity3D?

I have a have a ball with a rigidbody attached and I am adding torque to the ball based on the angle of a third person camera (always looking at the ball) witch rotates around the ball. I am trying to change the direction of the ball based on the position of the camera in its rotation around the ball. I am using Vector3.right for forward and reverse torque and Vector3.back for right and left. I have the forward/reverse value, rightVal working but I have been working on the left/right value, backVal for three days and am at a loss. Here is how I am trying to accomplish this.
void CalcRightVal()
{
float degrees = Camera.main.transform.localEulerAngles.y;
if (degrees > 0 && degrees < 180.0f)
{
rightVal = (-degrees / 360) * 4 + 1;
}
if (degrees > 180.0f && degrees < 360.0f) //
{
rightVal = (-degrees / 360) * 4 + 1;
if (rightVal < -1.0f)
{
rightVal = (degrees / 360) * 4 - 3;
}
}
}
void ApplyTorque()
{
Vector3 dir = new Vector3(rightVal, 0, backVal);
ball.AddTorque(dir * ballAcceleration);
}
In ApplyTorque() I need the backVal to go from 0 to -1 i.e. Vector3.back when degrees == 90. Then backVal should increase in value to 0 when degrees == 180 then up to 1 when degrees == 270 then back to 0 when degrees == 360. This will apply torque to the right when the camera is at 90 degrees and left when the camera is at 270 degrees. The Idea is that the user will add input to forward i.e. KeyCode.W to add acceleration and reverse or KeyCode.S for breaks. The camera will be responsible for changing the direction of the force. I am not terrible at math but this one has me beat.
While rightVal is at 1 or -1, backVal needs to be at 0 and vice versa. That way when the camera is directly behind the ball or 0 degrees the torque applied will be Vector3.right or (1, 0, 0) and when the camera is at 90 degrees or on the left of the ball, torque applied will be Vector3.back or (0, 0, -1). This will apply torque to make the ball turn right. Basically wherever the camera is the ball rolls forward from the point of view of the user. Thank you for the help, and all help is always appreciated.
Here is a graphic to help out.
Thank You.
If you are looking for a smoothed increase/decrease of the backval you can simply do:
backVal = -Mathf.Sin(degrees * Mathf.Deg2Rad);
If you want a linear relation however, it will be slightly more complicated:
private void CalcBackValue()
{
float degrees = Camera.main.transform.localEulerAngles.z;
if (degrees >= 0 && degrees < 90)
backVal = -degrees / 90f; // 0 to -1
else if (degrees >= 90 && degrees < 270)
backVal = -2 + degrees / 90; // -1 to 0 to 1
else if (degrees >= 270 && degrees < 360)
backVal = 4 - degrees / 90; // 1 to 0
}
You will probably need to expand on this to include the negative euler angles, but essentially you are finding the values along the following curves in the second example:

Rotate any GameObject between Two Angles [duplicate]

I would like to rotate an object back and forth between 90,-90 on the Y axis. The problem is I can set the object in the editor at -90, but when I run the project -90 suddenly becomes 270. Anyway here is the code that I'm using:
void Update()
{
if (transform.eulerAngles.y >= 270)
{
transform.Rotate(Vector3.up * speed * Time.deltaTime);
}
else if (transform.eulerAngles.y <= 90)
{
transform.Rotate(Vector3.up * -speed * Time.deltaTime);
}
}
It always gets stuck in the middle around 360 degrees. Help?
Just like moving GameObject back and forth, you can rotate GameObject back and forth with Mathf.PingPong. That's what it is used for. It will return value between 0 and 1. You can pass that value to Vector3.Lerp and generate the eulerAngle required to perform the rotation.
This can also be done with a coroutine but Mathf.PingPong should be used if you don't need to know when you have reached the destination. Coroutine should be used if you want to know when you reach the rotation destination.
public float speed = 0.36f;
Vector3 pointA;
Vector3 pointB;
void Start()
{
//Get current position then add 90 to its Y axis
pointA = transform.eulerAngles + new Vector3(0f, 90f, 0f);
//Get current position then substract -90 to its Y axis
pointB = transform.eulerAngles + new Vector3(0f, -90f, 0f);
}
void Update()
{
//PingPong between 0 and 1
float time = Mathf.PingPong(Time.time * speed, 1);
transform.eulerAngles = Vector3.Lerp(pointA, pointB, time);
}
EDIT:
With the coroutine method that you can use to determine the end of each rotation.
public GameObject objectToRotate;
public float speed = 0.36f;
Vector3 pointA;
Vector3 pointB;
void Start()
{
//Get current position then add 90 to its Y axis
pointA = transform.eulerAngles + new Vector3(0f, 90f, 0f);
//Get current position then substract -90 to its Y axis
pointB = transform.eulerAngles + new Vector3(0f, -90f, 0f);
objectToRotate = this.gameObject;
StartCoroutine(rotate());
}
IEnumerator rotate()
{
while (true)
{
//Rotate 90
yield return rotateObject(objectToRotate, pointA, 3f);
//Rotate -90
yield return rotateObject(objectToRotate, pointB, 3f);
//Wait?
//yield return new WaitForSeconds(3);
}
}
bool rotating = false;
IEnumerator rotateObject(GameObject gameObjectToMove, Vector3 eulerAngles, float duration)
{
if (rotating)
{
yield break;
}
rotating = true;
Vector3 newRot = gameObjectToMove.transform.eulerAngles + eulerAngles;
Vector3 currentRot = gameObjectToMove.transform.eulerAngles;
float counter = 0;
while (counter < duration)
{
counter += Time.deltaTime;
gameObjectToMove.transform.eulerAngles = Vector3.Lerp(currentRot, newRot, counter / duration);
yield return null;
}
rotating = false;
}
First of all you should always ensure that the angle stays between 0 and 359, meaning that 0 == 360 :
float angle = transform.eulerAngles.y % 360.00f;
After that you can check if it's between your minimum and maximum value :
if ( angle >= 270.00f && angle <= 90.00f )
{
// do your logic here ...
}
Another issue with your code is that it will stuck somewhere between 350 and 10 degrees. To explain this in more details let's assume your speed is 10, Time.deltaTime will be 1 as well and starting angle is 300, now frame steps :
Frame | Angle | Condition
----- | ----- | :-----------:
1 | 300 | angle >= 270
2 | 310 | angle >= 270
3 | 320 | angle >= 270
6 | 350 | angle >= 270
7 | 360 | angle >= 270
8 | 10 | angle <= 90
9 | 0 | angle <= 90
10 | 350 | angle >= 270
... and this will go forever.
To deal with this you have to make some condition based on user input or if you want your camera to "bounce" between these two angle then you can try something like this :
// make private field in that object :
float currentlyRotated = 0;
bool shouldAdd = true;
void Update()
{
var d = Vector3.up * (shouldAdd ? speed : -speed) * Time.deltaTime;
var angle = transform.eulerAngles.y + (shouldAdd ? speed : -speed);
angle %= 360.00f;
transform.Rotate(d);
if ( angle > 90 && angle < 270 )
{
shouldAdd = !shouldAdd;
}
}

Unity C# rotate object back and forth between vals [duplicate]

I would like to rotate an object back and forth between 90,-90 on the Y axis. The problem is I can set the object in the editor at -90, but when I run the project -90 suddenly becomes 270. Anyway here is the code that I'm using:
void Update()
{
if (transform.eulerAngles.y >= 270)
{
transform.Rotate(Vector3.up * speed * Time.deltaTime);
}
else if (transform.eulerAngles.y <= 90)
{
transform.Rotate(Vector3.up * -speed * Time.deltaTime);
}
}
It always gets stuck in the middle around 360 degrees. Help?
Just like moving GameObject back and forth, you can rotate GameObject back and forth with Mathf.PingPong. That's what it is used for. It will return value between 0 and 1. You can pass that value to Vector3.Lerp and generate the eulerAngle required to perform the rotation.
This can also be done with a coroutine but Mathf.PingPong should be used if you don't need to know when you have reached the destination. Coroutine should be used if you want to know when you reach the rotation destination.
public float speed = 0.36f;
Vector3 pointA;
Vector3 pointB;
void Start()
{
//Get current position then add 90 to its Y axis
pointA = transform.eulerAngles + new Vector3(0f, 90f, 0f);
//Get current position then substract -90 to its Y axis
pointB = transform.eulerAngles + new Vector3(0f, -90f, 0f);
}
void Update()
{
//PingPong between 0 and 1
float time = Mathf.PingPong(Time.time * speed, 1);
transform.eulerAngles = Vector3.Lerp(pointA, pointB, time);
}
EDIT:
With the coroutine method that you can use to determine the end of each rotation.
public GameObject objectToRotate;
public float speed = 0.36f;
Vector3 pointA;
Vector3 pointB;
void Start()
{
//Get current position then add 90 to its Y axis
pointA = transform.eulerAngles + new Vector3(0f, 90f, 0f);
//Get current position then substract -90 to its Y axis
pointB = transform.eulerAngles + new Vector3(0f, -90f, 0f);
objectToRotate = this.gameObject;
StartCoroutine(rotate());
}
IEnumerator rotate()
{
while (true)
{
//Rotate 90
yield return rotateObject(objectToRotate, pointA, 3f);
//Rotate -90
yield return rotateObject(objectToRotate, pointB, 3f);
//Wait?
//yield return new WaitForSeconds(3);
}
}
bool rotating = false;
IEnumerator rotateObject(GameObject gameObjectToMove, Vector3 eulerAngles, float duration)
{
if (rotating)
{
yield break;
}
rotating = true;
Vector3 newRot = gameObjectToMove.transform.eulerAngles + eulerAngles;
Vector3 currentRot = gameObjectToMove.transform.eulerAngles;
float counter = 0;
while (counter < duration)
{
counter += Time.deltaTime;
gameObjectToMove.transform.eulerAngles = Vector3.Lerp(currentRot, newRot, counter / duration);
yield return null;
}
rotating = false;
}
First of all you should always ensure that the angle stays between 0 and 359, meaning that 0 == 360 :
float angle = transform.eulerAngles.y % 360.00f;
After that you can check if it's between your minimum and maximum value :
if ( angle >= 270.00f && angle <= 90.00f )
{
// do your logic here ...
}
Another issue with your code is that it will stuck somewhere between 350 and 10 degrees. To explain this in more details let's assume your speed is 10, Time.deltaTime will be 1 as well and starting angle is 300, now frame steps :
Frame | Angle | Condition
----- | ----- | :-----------:
1 | 300 | angle >= 270
2 | 310 | angle >= 270
3 | 320 | angle >= 270
6 | 350 | angle >= 270
7 | 360 | angle >= 270
8 | 10 | angle <= 90
9 | 0 | angle <= 90
10 | 350 | angle >= 270
... and this will go forever.
To deal with this you have to make some condition based on user input or if you want your camera to "bounce" between these two angle then you can try something like this :
// make private field in that object :
float currentlyRotated = 0;
bool shouldAdd = true;
void Update()
{
var d = Vector3.up * (shouldAdd ? speed : -speed) * Time.deltaTime;
var angle = transform.eulerAngles.y + (shouldAdd ? speed : -speed);
angle %= 360.00f;
transform.Rotate(d);
if ( angle > 90 && angle < 270 )
{
shouldAdd = !shouldAdd;
}
}

Difficulty with projectile's tracking code

I wrote some code for a projectile class in my game that makes it track targets if it can:
if (_target != null && !_target.IsDead)
{
Vector2 currentDirectionVector = this.Body.LinearVelocity;
currentDirectionVector.Normalize();
float currentDirection = (float)Math.Atan2(currentDirectionVector.Y, currentDirectionVector.X);
Vector2 targetDirectionVector = this._target.Position - this.Position;
targetDirectionVector.Normalize();
float targetDirection = (float)Math.Atan2(targetDirectionVector.Y, targetDirectionVector.X);
float targetDirectionDelta = targetDirection - currentDirection;
if (MathFunctions.IsInRange(targetDirectionDelta, -(Info.TrackingRate * deltaTime), Info.TrackingRate * deltaTime))
{
Body.LinearVelocity = targetDirectionVector * Info.FiringVelocity;
}
else if (targetDirectionDelta > 0)
{
float newDirection = currentDirection + Info.TrackingRate * deltaTime;
Body.LinearVelocity = new Vector2(
(float)Math.Cos(newDirection),
(float)Math.Sin(newDirection)) * Info.FiringVelocity;
}
else if (targetDirectionDelta < 0)
{
float newDirection = currentDirection - Info.TrackingRate * deltaTime;
Body.LinearVelocity = new Vector2(
(float)Math.Cos(newDirection),
(float)Math.Sin(newDirection)) * Info.FiringVelocity;
}
}
This works sometimes, but depending on the relative angle to the target projectiles turn away from the target instead. I'm stumped; can someone point out the flaw in my code?
Update: thinking about it and trying stuff has led me to the conclusion that it has something to do with when the direction (being in radians) is below 0 and the current projectile angle is above 0.
The variable targetDirectionDelta is not always the shortest direction to the target. If the absolute value of targetDirectionDelta is greater than PI radians, it will appear the projectile is turning away from the target. Turning in the other direction is shorter and expected.
Example:
currentDirection = 2
targetDirection = -2
The projectile can turn -4 radians (in the negative direction), or 2*(PI-2) radians (about 2.2 radians) (in the positive direction).
For this case, your code always calculates the longer direction, but you are expecting the projectile to turn towards the shorter direction:
targetDirectionDelta = targetDirection - currentDirection

Categories