How to measure Z rotation accurately with rapid changes - c#

I am programming a game in unity and for one of the games enemies I wanted their sword to rotate in a 180 degree arc in front of them, I was able to make the sword rotate around the enemy but when I try to check if it has done the full arc it does not measure it properly since it is rotating at such high speeds.
public GameObject pivotPoint;
public SpriteRenderer renderer;
// Start is called before the first frame update
void Start()
{
StartCoroutine("rotate");
}
IEnumerator rotate()
{
renderer.enabled = true;
yield return new WaitForSeconds(2);
while(true)
{
transform.RotateAround(pivotPoint.transform.position, new Vector3(0, 0, 1), -1 * Time.deltaTime);
if(transform.eulerAngles.z >= -47)
{
if(transform.eulerAngles.z > 132)
{
renderer.enabled = false;
StopCoroutine("rotate");
}
}
yield return null;
}
yield return null;
}
I have tried using fixed delta time which made it a lot worse and tried to play around with the checks just to hopefully find a sweet spot where it could consitently get the right number but none have worked so far.

First, I'm not sure how this is a problem, because it looks like you're rotating 1 degree per second, so it should take 3 minutes (180 seconds) to rotate 180 degrees.
Regardless, assuming you're changing the speed to something faster, you're going to have trouble getting Euler angles out of the rotation because they may be limited to +/- 90 degrees or similar.
I think you'd be better off accumulating the fractional angle and checking that. I don't understand your nested if statements, because the angle must be >= -47 if it's > 132, I I'll drop it and rewrite your code as follows:
float totalAngle = 0f;
while(true)
{
float speed = -1f;
float fractionalAngle = speed * Time.deltaTime;
transform.RotateAround(pivotPoint.transform.position, Vector3.up, fractionalAngle);
totalAngle += fractionalAngle;
if(fractionalAngle < -180)
{
renderer.enabled = false;
StopCoroutine("rotate");
}
yield return null;
}
You were asking about 180 so I made it 180. Your code had -1 so I left the speed negative and went to -180. Change speed to get it faster.

I wouldn't use eulerAngles here at all but rather track how much you have rotated and make sure you rotate exactly 180°.
// Adjust this via the Inspector
public float anglePerSecond = 90f;
IEnumerator Start()
{
renderer.enabled = true;
yield return new WaitForSeconds(2);
var rotated = 0f;
while(rotated < 180f)
{
var step = Mathf.Min(anglePerSecond * Time.deltaTime, 180 - rotated);
transform.RotateAround(pivotPoint.transform.position, Vector3.forward, -step);
rotated += step;
yield return null;
}
renderer.enabled = false;
}

Related

Quaternion loop not synced with time in Unity C#

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;
}
}
}
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);
...
}
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!
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 the Transform 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;
}
}

Ensure rotation takes the same amount of time regardless of angle to be rotated in Unity

I hope somebody can help. I have a script that rotates a globe, over which a plane (which is stationary) flies from 1 country to another. This all works fine and is achieved with the following script
while (!DestinationReached)
{
Vector3 planetToCountry = (Destination.localPosition - Vector3.zero);//.normalized; //vector that points from planet to country
Vector3 planetToCamera = (camTr.position - tr.position).normalized; //vector that points from planet to camera/plane
Quaternion a = Quaternion.LookRotation(planetToCamera);
Quaternion b = Quaternion.LookRotation(planetToCountry);
b = Quaternion.Inverse(b);
newRotation = a * b;
tr.rotation = Quaternion.Slerp(tr.rotation, newRotation, 0.01f);
if (Approximately(tr.rotation, newRotation, 0.0001f)) //if here the plane has arrived
{
Debug.Log("Destination reached");
DestinationReached = true;
}
yield return new WaitForEndOfFrame();
}
It essentially calculates the angle between the plane (the camera is attached to the plane GO and views it from above) and the destination the globe needs to rotate to so that the plane looks as though it flies to the destination.
The issue I have is I need to make the flight time uniform regardless of the angle the globe must rotate, so lets say it must be 5 seconds, regardless if the plane flies from Paris to Ireland or Paris to Australia. Anybody have any ideas on how to do this.
I have to admit, I nicked this script for the web, as my Vector and Quaternion mathematics is hopeless :)
If you want to be flexible and e.g. add some easing at beginning and end but still finish within a fixed duration I would do it like this (I'll just assume here that your calculating the final rotation is working as intended)
// Adjust the duration via the Inspector
[SerializeField] private float duration = 5f;
private IEnumerator RotateRoutine()
{
// calculate these values only once!
// store the initial rotation
var startRotation = tr.rotation;
// Calculate and store your target ratoation
var planetToCountry = (Destination.localPosition - Vector3.zero);
var planetToCamera = (camTr.position - tr.position);
var a = Quaternion.LookRotation(planetToCamera);
var b = Quaternion.LookRotation(planetToCountry);
b = Quaternion.Inverse(b);
var targetRotation = a * b;
if(duration <= 0)
{
Debug.LogWarning("Rotating without duration!", this);
}
else
{
// track the time passed in this routine
var timePassed = 0f;
while (timePassed < duration)
{
// This will be a factor from 0 to 1
var factor = timePassed / duration;
// Optionally you can alter the curve of this factor
// and e.g. add some easing-in and - out
factor = Mathf.SmoothStep(0, 1, factor);
// rotate from startRotation to targetRotation via given factor
tr.rotation = Quaternion.Slerp(startRotation, targetRotation, factor);
// increase the timer by the time passed since last frame
timePassed += Time.deltaTime;
// Return null simply waits one frame
yield return null;
}
}
// Assign the targetRotation fix in order to eliminate
// an offset in the end due to time imprecision
tr.rotation = targetRotation;
Debug.Log("Destination reached");
}
So the problem here is the t used on your Quaternion.Slerp method, it's constant. This t is the "step" the slerp will do, so if it's contstant, it won't depend on time, it will depend on distance.
Try instead to do something like this, being timeToTransition the time you want that every rotation will match:
public IEnumerator RotatintCoroutine(float timeToTransition)
{
float step = 0f;
while (step < 1)
{
step += Time.deltaTime / timeToTransition;
//...other stuff
tr.rotation = Quaternion.Slerp(tr.rotation, newRotation, step);
//...more stuff if you want
yield return null;
}
}
Edit: addapted to your code should look like this
float timeToFly = 5f;
while (!DestinationReached)
{
step += Time.deltaTime / timeToTransition;
Vector3 planetToCountry = (Destination.localPosition - Vector3.zero);//.normalized; //vector that points from planet to country
Vector3 planetToCamera = (camTr.position - tr.position).normalized; //vector that points from planet to camera/plane
Quaternion a = Quaternion.LookRotation(planetToCamera);
Quaternion b = Quaternion.LookRotation(planetToCountry);
b = Quaternion.Inverse(b);
newRotation = a * b;
tr.rotation = Quaternion.Slerp(tr.rotation, newRotation, step);
if (step >= 1) //if here the plane has arrived
{
Debug.Log("Destination reached");
DestinationReached = true;
}
yield return null();
}

How to implement lerp for smooth movement

I want achieve moving object on x axis only with lerp to get smoothly movement.
this is picture what i need
I don't know how i can implement lerp to this code to get smooth movement between these values, it now works but it teleport the player and that is not smooth movement what i want to achieve
This is my working code that teleports player:
void Start()
{
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 desiredPos = new Vector3(transform.position.x + 1.5f, transform.position.y, transform.position.z);
transform.position = desiredPos;
}
if (Input.GetMouseButtonDown(1))
{
Vector3 desiredPos = new Vector3(transform.position.x -1.5f, transform.position.y, transform.position.z);
transform.position = desiredPos;
}
}
I want to implement this but i don't understand how to do it .. When i put all code into update the player don't even move.. It only works for me when i copy paste all the code from docs, but how i can move the time from start method to update and always do the same to achieve to get smooth movement for player when going left and right i don't know really please help me guys..
This is the code that works but i don't know how to change it for my example..
https://docs.unity3d.com/ScriptReference/Vector3.Lerp.html
There are multiple ways. I would not use Translate as this gives you little control here but rather e.g. MoveTowards which makes sure you have no over shooting at the end. Use this for a linear movement with a given moveSpeed:
// set move speed in Units/seconds in the Inspector
public float moveSpeed = 1f;
private Vector3 desiredPos;
private bool isMoving;
private void Update()
{
if (!isMoving && Input.GetMouseButtonDown(0))
{
desiredPos = transform.position + Vector3.right * 1.5f;
isMoving = true;
}
if (!isMoving && Input.GetMouseButtonDown(1))
{
desiredPos = transform.position - Vector3.right * 1.5f;
isMoving = true;
}
if(isMoving)
{
transform.position = Vector3.MoveTowards(transform.position, desiredPos, moveSpeed * Time.deltaTime);
// this == is true if the difference between both
// vectors is smaller than 0.00001
if(transform.position == desiredPos)
{
isMoving = false;
// So in order to eliminate any remaining difference
// make sure to set it to the correct target position
transform.position = desiredPos;
}
}
}
Or as you asked use Vector3.Lerp like e.g.
// a factor between 0 and 1
[Range(0, 1)] public float lerpFactor;
...
transform.position = Vector3.Lerp(transform.position, desiredPos, lerpFactor);
lerpFactor has to be a value between 0 and 1 where in our case 0 would meen the object never moves and 1 it directly jumps to the target position. In other words the closer you set it to 0 the slower it will reach the target, the closer you set it to 1 the faster it will reach the target.
a lot of people do this to get "smooth" movements but what actually happens is e.g. if you set 0.5 for lerpFactor then every frame the object is placed in the middle between current position and target position.
That looks somehow smooth, moves very fast at the beginning and very very slow at the end ... but: It actually never really reaches the target position but just gets very slow.
For your case that is fine since anyway we compare the current and target position using == with a precision of 0.00001. One just has to have in mind how Lerp works.
But with this you won't have any control neither over the move speed nor the duration.
If you want overall more controll (as I do) I would recommend to use a Coroutine (it is not absolutely neccessary and you could do the same in Update as well but in my eyes Coroutines are better to maintain and keep track of).
Than you could also make a smooth eased-in and eased-out movement with an always fixed duration regardless how far the distance is
// set desired move duration in seconds
public float moveDuration = 1;
private bool isMoving;
privtae void Update()
{
if (!isMoving && Input.GetMouseButtonDown(0))
{
StartCoroutine(transform.position + Vector3.right * 1.5f, moveDuration);
}
if (!isMoving && Input.GetMouseButtonDown(1))
{
StartCoroutine(transform.position - Vector3.right * 1.5f, moveDuration);
}
}
private IEnumerator Move(Vector3 targetPosition, float duration)
{
if(isMoving) yield break;
isMoving = true;
var startPosition = transform.position;
var passedTime = 0f;
do
{
// This would move the object with a linear speed
var lerpfactor = passedTime / duration;
// This is a cool trick to simply ease-in and ease-out
// the move speed
var smoothLerpfactor = Mathf.SmoothStep(0, 1, lerpfactor);
transform.position = Vector3.Lerp(startPosition, targetPosition, smoothLerpfactor);
// increase the passedTime by the time
// that passed since the last frame
passedTime += Time.deltaTime;
// Return to the main thread, render this frame and go on
// from here in the next frame
yield return null;
} while (passedTime < duration);
// just like before set the target position just to avoid any
// under shooting
transform.position = targetPosition;
isMoving = false;
}
and you could still extend this example to also take the dtsnace to move into account like e.g.
var actualDuration = duration * Vector3.Distance(startPosition, targetPosition);
and then later everywhere use actualDuration.
Use transform.Translate instead:
public float moveSpeed = 3f;
void Update ()
{
//Moves Left and right along x Axis
transform.Translate(Vector3.right * Time.deltaTime * Input.GetAxis("Horizontal")* moveSpeed);
}

2D game platform camera goes up when player touches edges

I'm doing a 2D platform game where the player only climbs (y-axis).
What I'm trying to do is when I reach near to the top edge, my camera goes up a little bit so I can see more of the level.
I have this, but it's not working:
Public Transform player;
Void Start() { }
Void Update()
{
if (player.transform.position > Screen.height - 50)
{
transform.position.y += speed * Time.deltaTime;
}
}
Other Way is like this But not working more than once, i might need to set move = false; but dont know how without interupting Lerp:
float moveTime = 1f; // In seconds
float moveTimer = 0.0f; // Used for keeping track of time
bool moving = false; // Flag letting us know if we're moving
public float heightChange = 7.0f; // This is the delta
// These will be assigned when a collision occurs
Vector3 target; // our target position
Vector3 startPos; // our starting position
void Start()
{
}
void Update()
{
// If we're currently moving and the movement hasn't finished
if (moving && moveTimer < moveTime)
{
// Accumulate the frame time, making the timer tick up
moveTimer += Time.deltaTime;
// calculate our ratio ("t")
float t = moveTimer / moveTime;
transform.position = Vector3.Lerp(startPos, target, t);
}
else
{
// We either haven't started moving, or have finished moving
}
}
void OnTriggerEnter2D(Collider2D other)
{
if (!moving)
{
// We set the target to be ten units above our current position
target = transform.position + Vector3.up * heightChange;
// And save our start position (because our actual position will be changing)
startPos = transform.position;
// Set the flag so that the movement starts
moving = true;
}
}
}
you are comparing two different things here. Screen.height is in pixels while player position is in world units. so lets assume your screen is 1024x768 you are checking if your player is above 718 which is a huge amount in world units.
What you have to do is convert one to the others unit and compare then for example with http://docs.unity3d.com/ScriptReference/Camera.ScreenToWorldPoint.html
Another thing i noticed is that this script is named player so i assume this is controlling your player object in which case
transform.position.y += speed * Time.deltaTime;
would only change your players position. To change the position of the main camera you could use:
Camera.main.transform.position += new Vector3(0f, speed * Time.deltaTime, 0f);
And lastly, always post the errors you get when asking questions, explain what you expect to happen and what actually happens.
You could try lerping the between positions eg.
transform.position = new trasform.lerp(transform.position, player.transform.position, speed*Time.DeltaTime);
just use the if statement to trigger a bool that then does the lerp, so the camera will move to what you need it to and not just when the player hits a certain point. Then when the camera is finished mooving, reset the bool ready for next time

'Smoother' looking teleportation with Vector3.Lerp

Short version: Can I use Vector3.Lerp in transform.Translate(new Vector3(-6,0)); (without calculating a speed variable into the movement)?
I have a very simple game where the Player can move from 1 lane to another (max. 3 lanes (like Temple Run)). Right now my character is just teleporting to the other lanes and I want to make it look more smooth with Vector3.Lerp. My problem now is, I cant figure out how to implement it into my code. I don't calculate my movement with a speed variable, because my character starts moving uneven distances, which I don't want. I have 3 set lanes to move to.
if (Input.GetKeyDown("a"))
{
// Do I need a speed variable here?
transform.Translate(new Vector3(-6, 0));
}
If the code is not in Update(), you can use StartCoroutine
if (Input.GetKeyDown("a")) {
changing = true;
StartCoroutine(ChangeLane(int n));
}
IEnumerator ChangeLane(int n) {
float changeProgress = 0f;
float changeDuration = 1f;
originPosition = tranform.position;
//the speed should be run speed
targetPosition = originPosition + new Vector3(n * lanWidth, speed * changeDuration);
while(changeProgress < changeDuration) {
tranform.position = Vector3.Lerp(originPosition, targetPosition, changeProgress / changeDuration);
changeProgress += Time.DeltaTime;
yield return 0;
}
tranform.position = targetPosition;
changing = false;
}
If the code is in Update(), you can also use this, but while changing lane, you should not start a new change

Categories

Resources