Basically I'm trying to create a rolling system for my top down RPG style game, but when I do the Vector3.Lerp it doesn't move the entire way
The code is:
public void CheckSpaceKey()
{
if (exhausted == false)
{
if (Input.GetKey(KeyCode.Space) && canRoll)
{
if (rollDir == RollDir.Up)
{
Vector3 rollStartPos = new Vector3(transform.position.x, transform.position.y, -1.0f);
Vector3 rollEndPos = new Vector3(transform.position.x, transform.position.y + 3, -1.0f);
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, Time.deltaTime);
canRoll = false;
playerStats.stamina -= 10;
Invoke("RollCooldown", 1.5f);
}
}
}
}
public void RollCooldown()
{
canRoll = true;
}
This should be making my player move upwards 3 units, but it instead is moving a random number of around 0.35 to 0.45.
Also CheckSpaceKey is being called in update
Lerp doesn't do what you think it does. Assuming Time.deltaTime is about 1/60, it's going to move the unit 1/60th of the way to the destination, and that's it.
Consider using a coroutine that updates the t parameter for Lerp with each frame:
public void CheckSpaceKey()
{
// by the way, this could just be `if (!exhausted)`
// although some coding guidelines require this format below
if (exhausted == false)
{
if (Input.GetKey(KeyCode.Space) && canRoll)
{
if (rollDir == RollDir.Up)
{
StartCoroutine(DoRoll(Vector3.up * 3));
}
}
}
}
IEnumerator DoRoll(Vector3 offset)
{
float duration = 1f;
canRoll = false;
playerStats.stamina -= 10;
Invoke("RollCooldown", 1.5f);
Vector3 rollStartPos = new Vector3(transform.position.x, transform.position.y, -1.0f);
Vector3 rollEndPos = rollStartPos + offset;
float t = 0;
while (t < 1f)
{
t = Mathf.Min(1f, t + Time.deltaTime/duration);
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, t);
yield return null;
}
}
public void RollCooldown()
{
canRoll = true;
}
This will not move 3 Units, you call
transform.position only inside the if case and access the if case only once every 1.5 seconds.
Lerp will not update your position every frame but only the frame it is called, if you the object to move constantly you have to update your position constantly you should crate rollStartPos and rollEndPos inside the first if and create add
else{
time += Time.deltaTime;
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, time);
}
under the 2nd if. This way it will lerp til rollEndPos unless the ball(?) is exhausted, if it should roll even when it's exhausted you need to update the position outside the first if
Related
I'm fairly new to unity, and after scraping through threads about rotations through frames, I managed to find code to help me get the look that I wanted for my game, here's the code for the movement:
public class Movement : MonoBehaviour
{
IEnumerator RotateOctagon(Vector3 byAngles, float inTime)
{
var fromAngle = transform.rotation;
var toAngle = Quaternion.Euler(transform.eulerAngles + byAngles);
for(var t = 0f; t <= 1; t+= Time.deltaTime / inTime)
{
transform.rotation = Quaternion.Lerp(fromAngle, toAngle, t);
yield return null;
}
transform.rotation = toAngle;
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown("left"))
{
StartCoroutine(RotateOctagon(Vector3.forward * 45, 0.1f));
}
if (Input.GetKeyDown("right"))
{
StartCoroutine(RotateOctagon(Vector3.forward * -45, 0.1f));
}
if (Input.GetKeyDown("up"))
{
StartCoroutine(RotateOctagon(Vector3.forward * 180, 0.1f));
}
}
}
And it generates desired results - putting the octagon to its next side, assuming you wait until the rotation is finished...
But The problem arises when you press left or right in the middle of the animation, in which the octagon incorrectly ends up on a point or weird angle (not a multiple of 45 degrees)...
My guess is that if the octagon is in the middle of the coroutine of rotating to the right say, at 30 degrees, any immediate key press will now rotate from that angle, ending up being 30+- 45 an unwanted angle.
I considered calculating the offset of the angle to the lower/higher 45 degree angle multiple, but I feel like there's a more correct approach and something I'm missing about coroutines that would help this.
Any help on how to get the octagon to rotate properly under the condition that multiple keys are pressed would be greatly appreciated.
Desired behavior: Say the current z axis is 0. After right is pressed and in the middle of rotating, left is pressed at 30 degrees. The octagon should rotate back to z=0
In order to simply not allow a new routine until the previous one has finished simply introduce a bool flag
private bool isRotating;
IEnumerator RotateOctagon(Vector3 byAngles, float inTime)
{
if(isRotating) yield break;
isRotating = true;
var fromAngle = transform.rotation;
var toAngle = Quaternion.Euler(transform.eulerAngles + byAngles);
for(var t = 0f; t <= 1; t+= Time.deltaTime / inTime)
{
transform.rotation = Quaternion.Lerp(fromAngle, toAngle, t);
yield return null;
}
transform.rotation = toAngle;
isRotating = false;
}
This way your input is basically ignored until the previous routine finished.
In order to be more efficient you can then also add
void Update()
{
if(isRotating) return;
...
}
Or the "fancy" alternative without additional flag: Make Start itself a Coroutine and do
IEnumerator Start ()
{
while(true)
{
if (Input.GetKeyDown("left"))
{
// This now rotates and waits at the same time until the rotation is finished
yield return RotateOctagon(Vector3.forward * 45, 0.1f));
}
else if (Input.GetKeyDown("right"))
{
yield return RotateOctagon(Vector3.forward * -45, 0.1f));
}
else if (Input.GetKeyDown("up"))
{
yield return RotateOctagon(Vector3.forward * 180, 0.1f));
}
else
{
// If none of the previous is true wait at least one frame
yield return null;
}
}
}
Alternatively since now we know you want to rather interrupt the current rotation and start a new one store it in a field like
private float rotation;
void Awake()
{
rotation = transform.localEulerAngles.z;
}
void Update()
{
if (Input.GetKeyDown("left"))
{
RotateAbout (45);
}
if (Input.GetKeyDown("right"))
{
RotateAbout (-45);
}
if (Input.GetKeyDown("up"))
{
RotateAbout (180);
}
}
private void RotateAbout(float angle)
{
StopAllCoroutines ();
rotation += angle;
StartCoroutine(RotateOctagon(rotation, 0.1f));
}
and then rather do
IEnumerator RotateOctagon(float toAngle, float inTime)
{
var fromRotation = transform.localRotation;
var toRotation = Quaternion.Euler(Vector3.forward * toAngle);
for(var t = 0f; t <= 1; t+= Time.deltaTime / inTime)
{
transform.localRotation = Quaternion.Lerp(fromRotation, toRotation, t);
yield return null;
}
transform.localRotation = toRotation;
}
I've got a setup right now where I have a 3D object with an empty object parented to it inside the object. When you press one button, an empty object that the camera is following rotates 90 degrees. If you press the other button, it rotates the other direction in 90 degrees. The result is that the camera can spin around the object 4 times before it makes a complete rotation.
Currently it works well, but I'm trying to figure out how I add some easing to the animation so it doesn't look so rough. I know a little about working with curves in animation but I'm not sure how I can apply that to code (or if it's even the best way to do things)
public InputMaster controls;
private bool isSpinning;
private float rotationSpeed = 0.3f;
IEnumerator RotateMe(Vector3 byangles, float intime)
{
var fromangle = transform.rotation;
var toangle = Quaternion.Euler(transform.eulerAngles + byangles);
for (var t = 0f; t < 1; t += Time.deltaTime / intime)
{
transform.rotation = Quaternion.Slerp(fromangle, toangle, t);
yield return null;
transform.rotation = toangle;
}
Debug.Log("finished rotation");
isSpinning = false;
Debug.Log("isSpinning now false");
}
code above is where I create a coroutine that says how it is going to transform. One thing that confuses me here a little is that if I don't have the line that says transform.rotation = toangle; , the rotation comes out at like 89.5 degrees or something, and if you do it multiple times it goes several degrees off. Not sure why that happens.
void Rotation(float amount)
{
Debug.Log("rotation number is " + amount);
float holdingRotate = controls.Player.Camera.ReadValue<float>();
if (holdingRotate == 1 && isSpinning == false)
{
isSpinning = true;
Debug.Log("isSpinning now true");
StartCoroutine(RotateMe(Vector3.up * 90, rotationSpeed));
}
else if (holdingRotate == -1 && isSpinning == false)
{
isSpinning = true;
Debug.Log("isSpinning now true");
StartCoroutine(RotateMe(Vector3.up * -90, rotationSpeed));
}
}
and this part is where the animation gets called up. Any help much appreciated.
Have a look at Animation Curves like #immersive said.
You can easily define your own curves in inspector
simply by adding this to your code:
public AnimationCurve curve;
You can then use AnimationCurve.Evaluate
to sample the curve.
The alternative is to use a premade library from the AssetStore/Package Manager like DOTween or LeanTween to name some.
Comments inline:
public class SmoothRotator : MonoBehaviour
{
// Animation curve holds the 'lerp factor' from starting angle to final angle over time
// (Y axis is blend factor, X axis is normalized 'time' factor)
public AnimationCurve Ease = AnimationCurve.EaseInOut(0, 0, 1, 1);
public float Duration = 1f;
private IEnumerator ActiveCoroutine = null;
public void RotateToward(Quaternion targetAngle) {
// If there's a Coroutine to stop, stop it.
if (ActiveCoroutine != null)
StopCoroutine(ActiveCoroutine);
// Start new Coroutine and cache the IEnumerator in case we need to interrupt it.
StartCoroutine(ActiveCoroutine = Rotator(targetAngle));
}
public IEnumerator Rotator(Quaternion targetAngle) {
// Store starting angle
var fromAngle = transform.rotation;
// Accumulator for Time.deltaTime
var age = 0f;
while (age < 1f) {
// normalize time (scale to "percentage complete") and clamp it
var normalisedTime = Mathf.Clamp01(age / Duration);
// Pull lerp factor from AnimationCurve
var lerpFactor = Ease.Evaluate(normalisedTime);
// Set rotation
transform.rotation = Quaternion.Slerp(fromAngle, targetAngle, lerpFactor);
// Wait for next frame
yield return null;
// Update age with new frame's deltaTime
age += Time.deltaTime;
}
// Animation complete, force true value
transform.rotation = targetAngle;
// Housekeeping, clear ActiveCoroutine
ActiveCoroutine = null;
}
}
for (var t = 0f; t < 1; t += Time.deltaTime / intime)
{
transform.rotation = Quaternion.Slerp(fromangle, toangle, t);
yield return null;
}
transform.rotation = toangle;
Because t won't equal to 1.
You just need to set the rotation to the target after the loop.
I'm trying to achieve the simplest thing. I have code for moving an object smoothly, and I want it to stop at a specific distance in relation to where the object was when the button was clicked. So I can't store the position inside any function that updates every frame. Currently the object simply starts moving and doesn't stop.
void Update()
{
Vector3 targetPosition = new Vector3(transform.position.x - 1, transform.position.y, transform.position.z);
if (movement == true)
{
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, targetPosition, step);
}
}
public void coroutineStarter()
{
float targetPosition = transform.position.x - 1;
StartCoroutine(OnClick(targetPosition));
}
IEnumerator OnClick(float targetPosition)
{
if (transform.position.x != targetPosition)
{
movement = true;
}
else
{
movement = false;
yield return null;
}
}
}
I should specify that pressing the button starts the coroutineStarter. I tried to use a while loop instead of the if statement inside OnClick but apparently using while loops anywhere freezes unity if it's infinite, and if it isn't it freezes unity for the duration of the loop.
I wouldn't mix up a Coroutine with code running in Update. A Coroutine already is like a temporary Update method so you could simply do
public void coroutineStarter()
{
var targetPosition = transform.position + Vector3.left;
StartCoroutine(OnClick(targetPosition));
}
IEnumerator OnClick(Vector3 targetPosition)
{
while (transform.position.x != targetPosition)
{
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, targetPosition, step);
// Important! Tells Unity to pause here, render this frame
// and continue from here in the next frame
yield return null;
}
// When done set the position fixed once
// since == has a precision of 0.00001 so it never reaches an exact position
transform.position = targetPosition;
}
This moves the object to the target position with a constant speed.
A slightly different approach is moving the object within a certain time but allows to add ease-in and ease-out at start and end of the movement using Vector3.Lerp and SmoothStep
IEnumerator OnClick(Vector3 targetPosition)
{
var startPosition = transform.position;
var distance = Vector3.Distance(startPosition, targetPosition);
if(distance <= 0) yield break;
var duration = distance / speed;
var timePassed = 0f;
while (timePassed < duration)
{
var factor = timePassed / duration;
// Optionally add easing
factor = Mathf.SmoothStep(0,1,factor);
transform.position = Vector3.Lerp(startPosition, targetPosition, factor);
// increase by time since last frame avoiding overshooting
timePassed += Mathf.Min(Time.deltaTime, duration - timePassed);
// Important! Tells Unity to pause here, render this frame
// and continue from here in the next frame
yield return null;
}
// When done set the position fixed once
// this time just in case you never know ;)
transform.position = targetPosition;
}
(Once it reaches point B, it goes to point A and back to point B in a smooth and orderly fashion). For some reason, the platform refuses to move and stays put. I have tried many things such as using vector3.movetowards and much more but nothing makes it move.
Here is the code. (Point A and Point B are empty game objects that are not parented to the platform)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveTwoTransforms : MonoBehaviour
{
public Transform pointA;
public Transform pointB;
bool HeadingtowardsB;
bool HeadingtowardsA;
public float speed = 10;
// Start is called before the first frame update
void Start()
{
transform.position = pointA.position;
HeadingtowardsB = true;
HeadingtowardsA = false;
GlideAround();
}
// Update is called once per frame
void Update()
{
}
public IEnumerator GlideAround()
{
while (true)
{
while ((Mathf.Abs((pointB.position.x - transform.position.x) + (pointB.position.y - transform.position.y)) > 0.05f) && HeadingtowardsB == true && HeadingtowardsA==false )
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointB.position, speed * Time.deltaTime);
if(Mathf.Abs((pointB.position.x - transform.position.x) + (pointB.position.y - transform.position.y)) > 0.05f)
{
HeadingtowardsB = false;
HeadingtowardsA = true;
}
}
HeadingtowardsB = false;
HeadingtowardsA = true;
while (Mathf.Abs((pointA.position.x - transform.position.x) + (pointA.position.y - transform.position.y)) > 0.05f && HeadingtowardsA==true && HeadingtowardsB==false)
{
yield return new WaitForEndOfFrame();
transform.position=transform.position=Vector3.Lerp(transform.position, pointA.position, speed*Time.deltaTime);
}
}
}
}
There are no error messages, the platform won't move. The platform is still colliding and it seems to behave like a normal platform.
GlideAround() is an IEnumerator and can not be called like a method. You have to start it using StartCoroutine
StartCoroutine(GlideAround());
Also note that speed * Time.deltaTime makes little sense for usage in Lerp. You usually would want a constant value between 0-1 in your case (since you re-use the current position as first parameter).
E.g. a value of 0.5 means: Every frame set the new position to the center between the current and the target position.
Since you catch it using a threashold of 0.05f this should be fine but in general I wouldn't use Lerp like this ... with very small values you might never really reach the target position.
I would therefore prefer to either control the constant speed and use
bool isHeadingA = true;
while(true)
{
// if it was intended you can ofourse also again use
// Vector2.Distance(transform.position, isHeadingA ? pointA.position : pointB.position) <= 0.05f)
while (transform.position != (isHeadingA ? pointA.position : pointB.position))
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.MoveTowards(transform.position, isHeadingA ? pointA.position : pointB.position, speed * Time.deltaTime);
}
// flip the direction
isHeadingA = !isHeadingA;
}
!= has a precision of 0.00001 and is fine here since MoveTowards avoids overshooting so at some point it will surely reach the position if speed != 0.
Or alternatively you can use Lerp if you rather want to control the duration of the movement with a smoothed in and out speed using e.g. Mathf.PingPong as factor and Mathf.SmoothStep for easing in and out like
while(true)
{
yield return new WaitForEndOfFrame();
// linear pingpong between 0 and 1
var factor = Mathf.PingPong(Time.time, 1);
// add easing at the ends
factor = Mathf.SmoothStep(0, 1, factor);
// optionally add even more easing ;)
//factor = Mathf.SmoothStep(0, 1, factor);
transform.position = Vector2.Lerp(pointA.position, pointB.position, factor);
}
Try this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveTwoTransforms : MonoBehaviour
{
public Transform pointA;
public Transform pointB;
bool HeadingtowardsB;
bool HeadingtowardsA;
public float speed = 10;
// Start is called before the first frame update
void Start()
{
transform.position = pointA.position;
HeadingtowardsB = true;
HeadingtowardsA = false;
StartCoroutine(GlideAround());
}
// Update is called once per frame
void Update()
{
}
public IEnumerator GlideAround()
{
//Because we want a specific speed, the % between the two points
//that we should be at will be equal to (time * speed) / distance
//with an adjustment for going backwards.
float distance = Vector3.Distance(pointA, pointB) * 2;
float lapTime = distance / speed;
float startTime = Time.time;
Debug.Log("The platform speed is: " + speed.ToString());
Debug.Log("The distance for one full lap is: " + distance.ToString());
Debug.Log("One lap will take: " + lapTime.ToString() + " seconds");
while (true)
{
yield return new WaitForEndOfFrame();
float elapsedTime = (Time.time - startTime) % lapTime;
float progress = elapsedTime / (lapTime / 2);
if (progress > 1){
progress = 2 - progress;
}
Debug.Log("The platform speed is currently: " + progress.ToString() + "% between pointA and pointB");
transform.position = Vector2.Lerp(pointA.position, pointB.position, progress);
}
}
As I mentioned in the comments, you Lerp uses a percentage between two points as the return value. You need to give it a percent, not a speed.
Here is an implementation that uses speed, like you wanted, but #derHugo's answer with PingPong is much simpler!
The main issue is that you are not using coroutines properly, as pointed out in derHugo's answer.
However, I'll provide my own answer, seeing that you are making the rookie mistake of way over-engineering this problem.
I think teaching by example might be the most appropriate in this case, so here it is:
If the points dictating the platform's movement are static, you should do this with animation. I won't explain it here. Tutorials like this one can easily be found all over the unity tutorials, unity forums, other StackOverflow Q&As, and youtube.
If your points are dynamic, this is more than enough:
public class MoveTwoTransforms : MonoBehaviour {
public Transform pointA;
public Transform pointB;
public float speed = 10;
void Start() {
transform.position = pointA.position;
StartCoroutine(GlideAround());
}
private IEnumerator MoveTowards(Vector3 targetPosition) {
while (transform.position != targetPosition) {
transform.position = Vector3.MoveTowards(transform.position, targetPosition, speed * Time.deltaTime);
yield return null;
}
}
private IEnumerator GlideAround() {
while(true) {
yield return StartCoroutine(MoveTowards(pointA));
yield return StartCoroutine(MoveTowards(pointB));
}
}
}
Just a final note:
If the platform should have physics or a collider, it is preferable to add a Rigidbody, set it to be kinematic, and to the movement by setting the Rigidbody.position instead of the transform. This is because that is updated on the physics loop (FixedUpdate) rather than the frame loop (Update), and avoids some bugs related to asyncrony between the physics system and moving objects through transform's position.
rigidbody.position = Vector3.MoveTowards(rigidbody.position, //...
Here is the code that worked.
public IEnumerator GlideAround()
{
while (true)
{
while (HasReachedA == false)
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointA.position, 0.01f);
if ((Mathf.Abs(Vector2.Distance(pointA.position, transform.position)) < 0.01f))
{
HasReacedB = false;
HasReachedA = true;
}
}
while (HasReacedB == false)
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointB.position, 0.01f);
if ((Mathf.Abs(Vector2.Distance(pointB.position, transform.position)) < 0.01f))
{
HasReacedB = true;
HasReachedA = false;
}
}
}
}
void Fire(float firingRate)
{
TimePerFrame += Time.deltaTime;
if(TimePerFrame >= firingRate)
{
Vector3 ProjectileDistance = new Vector3(0, 30, 0); //distance between center of the campion and it's head
GameObject beam = Instantiate(projectile, transform.position + ProjectileDistance, Quaternion.identity) as GameObject;
beam.GetComponent<Rigidbody2D>().velocity = new Vector3(0, projectileSpeed, 0);
// AudioSource.PlayClipAtPoint(fireSound, transform.position);
TimePerFrame = 0;
}
}
void Update ()
{
if (freezePosition == false)
{
Fire(firingRate);
PositionChaning();
firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, 0.1f);
Debug.Log(firingRate);
}
}
I want my firerate to be flexible, i want it to start by shooting fast and let it automatically lower it's fire rate. (the bigger the firingRate float is the slower the speed is)
The problem is that firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, 0.1f);
triggers once and only once. It doesn't seem to change it's value every frame.
Debug.Log(firingRate); tells the value every frame but it seems to remain a constant.
Why does this happen?
The update triggers every Frame, and so does your Mathf.Lerp However you are not changing the interpolation, which in your case is defined as 0.1f.
By changing this interpolation, you will be able to achieve the 'shifting' of fire rate.
In your case you could define a variable t outside the scope of your update, and update it inside the Update() through t += 0.5f * Time.deltaTime;
The Mathf.Lerp documentation has a pretty good sample of how todo so as well
void Update()
{
// animate the position of the game object...
transform.position = new Vector3(Mathf.Lerp(minimum, maximum, t), 0, 0);
// .. and increate the t interpolater
t += 0.5f * Time.deltaTime;
// now check if the interpolator has reached 1.0
// and swap maximum and minimum so game object moves
// in the opposite direction.
if (t > 1.0f)
{
float temp = maximum;
maximum = minimum;
minimum = temp;
t = 0.0f;
}
}
Your problem is here:
firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, 0.1f);
As you can see here your t has to be calculated every frame.
public static float Lerp(float a, float b, float t);
You can change it like this:
private float fireTimer = 1.0f;
public float fireLimiter = 0.05f;
void Fire(float firingRate)
{
TimePerFrame += Time.deltaTime;
if(TimePerFrame >= firingRate)
{
Vector3 ProjectileDistance = new Vector3(0, 30, 0); //distance between center of the campion and it's head
GameObject beam = Instantiate(projectile, transform.position + ProjectileDistance, Quaternion.identity) as GameObject;
beam.GetComponent<Rigidbody2D>().velocity = new Vector3(0, projectileSpeed, 0);
// AudioSource.PlayClipAtPoint(fireSound, transform.position);
TimePerFrame = 0;
}
}
void Update ()
{
if (freezePosition == false)
{
if(fireTimer > 0.0f){
fireTimer -= Time.deltaTime * fireLimiter;
}
Fire(firingRate);
PositionChaning();
firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, fireTimer);
Debug.Log(firingRate);
}
}
Do not forget to reset fireTimer to 1.0f after shooting!
The delay of the firerate can be controlled by fireLimiter