How to transition smoothly from AddForce() to MovePosition() in Unity? - c#

I have a character that dashes using AddForce(), after dashing, i want him to auto walk in set speed using MoovePosition().
This project was made using the 3D engine and it work flawlessly. But I've changed it to 2D and after the AddForce, the character abruptly changes to the speed that MovePosition() set, it looks bad.
I thought about checking the velocity after the dash and when it reaches the same velocity as when auto-walk is happening, it calls for MovePosition. But it doesn't seem a good approach.. I'd love to receive some tips on this issue.
if(moveFactor!= 0)
{
Vector2 appliedForce = transform.position + transform.up * moveFactor;
Debug.Log("appliedForce: " + appliedForce.magnitude);
playerRigidbody.AddForce(appliedForce);
autoWalk = true;
moveFactor = 0;
}
if (autoWalk) playerRigidbody.MovePosition(walkForce);
moveFactor is a float value that a swipe detector script returns. The code is in FixedUpdate as well.

It would be helpful to see the problem instead of trying to reconstruct the picture from your description, but I'll try my best.
The Problem
The same frame you start the dash, you set autoWalk to true. As soon as the if statement is exited, the if (autoWalk) is evaluated and you start manually moving the RigidBody using MovePosition() — before the force has even been added.
I'm not sure why that ever worked in 3D, though.
Solution #1
Instead of using MovePosition(), move with AddForce(), with the default ForceMode2D; or with playerRigidbody.velocity, where-in you set it once, not every frame.
Solution #2
Instead of using AddForce(), for the dash, use the same old MovePosition() you use inside if (autoWalk).
Just increase the magnitude of walkForce several times for the duration of the dash.
Solution #3
The problem with using AddForce() for it is that you don't know when it's "finished", so you don't know when to transition to MovePosition().
If the above solutions don't work for you, implement the dash manually. You can do this with Vector2.Lerp() (or Vector3) inside a coroutine.
if (moveFactor != 0)
{
Vector2 endPosition = transform.position + transform.up * moveFactor;
StartCoroutine(Dash(endPosition));
moveFactor = 0;
}
Where Dash() is:
public float dashDuration = 1f;
private IEnumerator Dash(Vector2 endPosition)
{
autoWalk = false;
// Record the original position before you begin moving the object.
float startingPosition = transform.position;
float elapsed = 0f;
while (elapsed < dashDuration )
{
elapsed += Time.deltaTime;
Vector2 nextPosition = Vector2.Lerp(startingPosition, endPosition, elapsed / dashDuration );
playerRigidbody.MovePosition(nextPosition);
// Note: if the RigidBody is kinematic (isKinematic == true), this will simply teleport it to the given position.
// Otherwise, it will comply with its interpolation settings.
// You could directly modify playerRigidbody.position or transform.position (though the former is faster) for the same teleportation.
yield return null;
}
autoWalk = true;
}
appliedForce might not immediately work as endPosition — modify it so it does, in that case. dashDuration is in seconds.
Make sure Dash() is started only once, until it is finished. You can do this by saving the Coroutine returned by StartCoroutine() in a private field member and comparing it to null before you start, but you shouldn't have to. If the code inside the if (moveFactor != 0) statement is executed something like every frame, change your code accordingly.
I've not tested this, so I'm not sure it will check for collisions. If it doesn't but you need it to, you can add that funcionality by raycasting (or box-casting, sphere-casting, or whatever kind of Collider you use):
private IEnumerator Dash(Vector2 endPosition)
{
float startingPosition = transform.position;
float elapsed = 0f;
while (elapsed < dashDuration )
{
elapsed += Time.deltaTime;
Vector2 nextPosition = Vector2.Lerp(startingPosition, endPosition, elapsed / dashDuration );
// Add the raycast here and only execute the MovePosition() if the returned RaycastHit2D's collider property is null.
// (The raycast's should extend to nextPosition.)
playerRigidbody.MovePosition(nextPosition);
yield return null;
}
}

Related

Vector3.Lerp won't move my object despite my debugging saying it should

So this is for an aim-down sight motion in a FPS game. When the player right clicks, this coroutine is fired up which should move the player's right hand from its "idle" position to its "aiming" position.
Once the aiming position is reached, hitbox is activated, and an animation plays.
public float ChargeTime;
public Vector3 TargetPosition;
public override IEnumerator Attack(PlayerController player)
{
float elapsedTime = 0f;
Vector3 currentPos = player.RightHand.transform.localPosition;
while (elapsedTime < ChargeTime)
{
elapsedTime += Time.deltaTime;
player.RightHand.transform.localPosition = Vector3.Lerp(currentPos, TargetPosition, (elapsedTime / ChargeTime));
yield return null;
}
player.RightHand.transform.localPosition = TargetPosition;
player.Hitbox.SetActive(true);
player.RightHandAnimator.SetBool("IsHolding", true);
yield return null;
}
I've been going over and over this code and everything should be working as intended. Debug.Logs will show me I've reached this code exactly once, that I'm looping through this while() function properly, that the t variable of the Lerp does increase properly from 0 to 1, and that the hitbox properly triggers at the end of while()... but the hand simply refuses to move.
I've tripled and quadrupled checked that the RightHand variable is indeed referencing the proper object, and that no other code ever interferes (there is nothing else that makes that specific object move).
Am I not understanding how Vector3.Lerp works? What am I missing?
Thanks!
EDIT: The "override" is because this class derives from "Weapon" which has a virtual Attack coroutine, which gets overriden by different types of Weapon subclasses.
Looks to me like there is an Animator on your player that affects the right hand position - at least player.RightHandAnimator leads me to that thought ;)
The Animator is one of the last things to be executed within a frame and as soon as it holds any keyframe in any state on the player.RightHand.transform.localPosition it will always overrule whatever changes you make in code!
you could in the meantime turn it off and do e.g.
public override IEnumerator Attack(PlayerController player)
{
player.RightHandAnimator.enabled = false;
var rightHand = player.RightHand.transform;
var currentPos = rightHand.localPosition;
for(var elapsedTime = 0f; elapsedTime < ChargeTime; elapsedTime += Time.deltaTime)
{
rightHand.localPosition = Vector3.Lerp(currentPos, TargetPosition, elapsedTime / ChargeTime);
yield return null;
}
rightHand.localPosition = TargetPosition;
player.Hitbox.SetActive(true);
player.RightHandAnimator.enabled = true;
player.RightHandAnimator.SetBool("IsHolding", true);
}
What actually happens? Does the hand not move at all?
Your code looks ok to me. Does the hand move at the end of the loop at this line..?
player.RightHand.transform.localPosition = TargetPosition;
If not then, the problem is that the field TargetPosition is wrong. You can ignore the lerp and just try and get that one line working properly, and once it is, your lerp should also work fine.
You could try changing it from simply from a public Vector3 to a Transform and drag a reference to a blank gameobject into it, and use the transform.position field of it in the lerp (the gameobject will be at the position you want the hand to goto) That would make more sense to me, but maybe not for your game. I dunno.

Unity 2D. How to rotate an object smoothly inside a Coroutine?

I'm newbie here, but will try to describe as clear as I can. (it's not that important, just to clarify) So I want to rotate an object for 405deg in 1 second only once, when space is pressed. I've thought it should be done using coroutines, so I've wrote something like this:
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
StopAllCoroutines(); //to prevent overlapping
StartCoroutine(Rotate());
}
}
IEnumerator Rotate()
{
//where final finalRotation is a rotation i want to get in a result
Quaternion finalRotation = Quaternion.Euler(0, 0, 405) * transform.rotation;
/*
using do..while to make sure that object will rotate even in case it's in same rotation as finalRotation
*/
do
{
//should smoothly rotate an object for 405deg in 1 sec but it doesnt
transform.Rotate(Vector3.forward, 405f * Time.deltaTime / 1f);
} while (transform.rotation != finalRotation);
yield return 1;
}
The code above works fine and everything is ok except one, it rotates an object instantly and not smoothly. I mean, if I use that line of code in Update, everything go smoothly
transform.Rotate(Vector3.forward, 405f * Time.deltaTime / 1f);
So my questions:
Why does it rotate smoothly in Update, but instantly in the coroutine;
and how can that be fixed?
Thanks in advance, once again, sorry if I made some mistakes when making a topic.
It updates instantly in the coroutine because you only yield when the rotation is complete. The frame wont be rendered until every pending/running coroutine yields or completes, so here your rotation completes and then finally the frame is rendered, making it an instant rotation.
Instead, you should yield in the loop.
You should also be aware that Quaternion finalRotation = Quaternion.Euler(0, 0, 405) * transform.rotation; and transform.Rotate(Vector3.forward, 405f); can have different results depending on parent object rotations, because they rotate in different spaces, the first rotating in global and the second rotating in local. This may result in the loop as written actually never ending.
Another problem is that there is no guarantee that summing up Time.deltaTime values will ever add up to 1f. Suppose on the very first frame, lag occurs, causing Time.deltaTime to be huge, 2f. You can see how it might never end as a result. As an alternative, you should remember the start rotation, then on each frame calcuate the rotation from that frame, then apply it that frame.
Altogether:
IEnumerator Rotate()
{
Quaternion startRotation = transform.rotation;
float endZRot = 405f;
float duration = 1f;
float t = 0;
while (t < 1f)
{
time = Mathf.Min(1f, t + Time.deltaTime/duration);
Vector3 newEulerOffset = Vector3.forward * (endZRot * t);
// global z rotation
transform.rotation = Quaternion.Euler(newEulerOffset) * startRotation;
// local z rotation
// transform.rotation = startRotation * Quaternion.Euler(newEulerOffset);
yield return null;
}
}
yield return null will wait one frame inside a coroutine. Right now you have placed the yield return 1; outside the while loop, so it wont do anything. In this situation I whould lerp between the two rotations in a while loop that goes from 0->1
or (if you dont want to do any of this stuff and dont care for a bit of bloat) use leantween, https://assetstore.unity.com/packages/tools/animation/leantween-3595
In the place where you rotate it one increment you have to wait for some time. Try
yield return new WaitForSeconds(0.1);
Also while I was typing this someone else answered.

Vector3.Lerp inside a Coroutine is not working as it should on Unity3D

I am having a strange issue when using a Vector3.Lerp inside a coroutine and it makes no sense because I have many Coroutines in my game and they are all working fine.
WHAT I AM TRYING TO ACHIEVE
I am simply trying to move an object from a starting height to a final height with a coroutine called InVolo() and a simple Vector3.Lerp to change the position of the object.
MY CODE
float elapsedTime = 0f;
while (elapsedTime < TempoSalita)
{
transform.position = Vector3.Lerp(transform.position,
new Vector3(transform.position.x, maxHight, transform.position.z), (elapsedTime / TempoSalita));
elapsedTime += Time.deltaTime;
yield return null;
}
transform.position = new Vector3(transform.position.x, maxHight, transform.position.z);
yield return null;
The maxHight is simply 4f and TempoSalita is 2f. The coroutine is started in an OnTriggerEnter and it is working fine, the object is reaching the y of 4f and it exits the while in 2 seconds.
THE PROBLEM
Basically, elapsedTime / TempoSalita is becoming 1 after 2 seconds, as it should be, but the object reaches the ending position after like 0.3 seconds, when elapsed/TempoSalita is 0.2 and it makes no sense to me. Vector3.Lerp should go from startpos to endpos in the time of the t value to go from 0 to 1. But it goes to the final position in 0.2 seconds and I don't have a clue why.
WHAT I TRIED
I have tried to Debug.Log the elapsedTime and it is changing fine, tried to use only Mathf.Lerp between the y values and it does the same thing. There is nothing else in that script that can affect it, this is how the Coroutine starts:
void OnTriggerEnter(Collider other)
{
if(other.CompareTag("Saltino"))
{
StartCoroutine(InVolo());
}
}
It starts only one time. Do you know what can cause this strange problem?
It could be the condition you are using which seems a bit suspect to me.
Generally I would write this a bit differently, something like..
var t = 0f;
var start = transform.position;
var target = new Vector3(transform.position.x, maxHight, transform.position.z);
while (t < 1)
{
t += Time.deltaTime / TempoSalita;
if (t > 1) t = 1;
transform.position = Vector3.Lerp(start, target, t);
yield return null;
}
First you have your start and target values created before altering the position. The t value will increase until it reaches (or passes) 1. Since we do not want t to go beyond 1 we do a check before applying the lerp. Our t value is calculated outside of the lerp to make it clear and easy to read/modify.
The above will run for time specified and does not require the extra lines at the end since t will eventually be exactly 1.

While infinite loop with moving unity transform

while (transform.position != new Vector3(desX, desY))
{
// 2 - Movement
Vector3 movement = new Vector3(
0.1f * desX,
0.1f * desY,
0);
//movement *= Time.deltaTime;
transform.Translate(movement);
}
This part of my program crashes the Unity engine and I'm pretty sure it's an infinite loop but I can't figure out why or how to fix it.
It's freezing your application because you're are not giving other scripts chance to run when the condition in the while loop is not met.
To fix that put that code in a coroutine function then add yield return null; to the while loop. This makes Unity to wait for a frame after each loop therefore given other scripts the opportunity to run every frame. This should fix the freeze issue whether the while loop exits or not. I would also suggest you use Vector3.Distance to determine when you are close to the destination.
public float reachThreshold = 0.2f;
void Start()
{
StartCoroutine(MoveBject());
}
IEnumerator MoveBject()
{
float distance = Vector3.Distance(transform.position, new Vector3(desX, desY));
while (distance > reachThreshold)
{
// 2 - Movement
Vector3 movement = new Vector3(
0.1f * desX,
0.1f * desY,
0);
//movement *= Time.deltaTime;
transform.Translate(movement);
//Wait a frame
yield return null;
}
}
If you really want to move GameObject to another position over time, see this post.
In general don't do things in while that are meant to happen in a per frame base! (Thanks Ron)
The result of while in the best case would be that your App stucks until eventually the vectors match, making the object "jump" in position. In the worst case they never match and your App freezes forever.
Instead you should use the Update() method, which is called each frame, and just move the object one step per frame.
To compare the Vectors you should better use Vector3.Distance. Using the == operator for Vector3 is basically ok but internally it does something equal to
Vector3.Distance(vectorA, vectorB) <= 0.00001f
which uses a very small thershold that might not match exactly. So Using Vector3.Distance you can set your own threshold e.g. to 0.1 to make it "easier" to match.
bool match = Vector3.Distance(transform.position, new Vector3(desX, desY) < 0.1f
To move an object towards another Unity actually already has a built-in method Vector3.MoveTowards e.g.
transform.position = Vector3.MoveTowards(transform.position, new Vector3 (desX, desY), 0.1f);
This takes care of Vectors3 comparing so you don't even need it anymore.
To make it smooth and framarate-save you were right already to use Time.deltaTime. Without this it would move 0.1 meters / frame but your framerate might not be stabil. Using Time.deltaTime makes it 0.1 meters / second which in almost all cases is what you actually want to achieve.
So to put it together your code should be something like
float desX;
float desY;
float moveSpeed = 0.1f;
void Update()
{
var targetPosition = new Vector3 (desX, desY);
transform.position = Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime);
}
Note that in some cases MoveTowards still is not relayable enough e.g. if you want to track collisions or something. In this case refer here

Change movement speed in Vector.Lerp Unity 5.6

I am using Vector3.Lerp in unity game to simply move a gameobject from one position to other smoothly. Below is my code:
public class playermovement : MonoBehaviour {
public float timeTakenDuringLerp = 2f;
private bool _isLerping;
private Vector3 _startPosition;
private Vector3 _endPosition;
private float _timeStartedLerping;
void StartLerping(int i)
{
_isLerping = true;
_timeStartedLerping = Time.time ; // adding 1 to time.time here makes it wait for 1 sec before starting
//We set the start position to the current position, and the finish to 10 spaces in the 'forward' direction
_startPosition = transform.position;
_endPosition = new Vector3(transform.position.x + i,transform.position.y,transform.position.z);
}
void Update()
{
//When the user hits the spacebar, we start lerping
if(Input.GetKey(KeyCode.Space))
{
int i = 65;
StartLerping(i);
}
}
//We do the actual interpolation in FixedUpdate(), since we're dealing with a rigidbody
void FixedUpdate()
{
if(_isLerping)
{
//We want percentage = 0.0 when Time.time = _timeStartedLerping
//and percentage = 1.0 when Time.time = _timeStartedLerping + timeTakenDuringLerp
//In other words, we want to know what percentage of "timeTakenDuringLerp" the value
//"Time.time - _timeStartedLerping" is.
float timeSinceStarted = Time.time - _timeStartedLerping;
float percentageComplete = timeSinceStarted / timeTakenDuringLerp;
//Perform the actual lerping. Notice that the first two parameters will always be the same
//throughout a single lerp-processs (ie. they won't change until we hit the space-bar again
//to start another lerp)
transform.position = Vector3.Lerp (_startPosition, _endPosition, percentageComplete);
//When we've completed the lerp, we set _isLerping to false
if(percentageComplete >= 1.0f)
{
_isLerping = false;
}
}
}
}
The code works fine and the gameobject moves smoothly between two points. But it takes about 1 sec to reach destination. I want to make it move faster. I have tried decreasing the value of float timeTakenDuringLerp but the speed isn't affected. I have followed this tutorial and the explanation there also says to change timeTakenDuringLerp variable in order to change speed but its not working here.
Any Suggestions please?
H℮y, thanks for linking to my blog!
Decreasing timeTakenDuringLerp is the correct solution. That reduces the time it takes for the object to move from start to finish, which is another way of saying "it increases the speed".
If there is a specific speed you want the object to move at, you'll need to make timeTakenDuringLerp a variable rather than a constant, and set it to distance/speed. Or better yet, don't use Lerp at all, and instead set the object's velocity and let Unity's physics engine take care of it.
Multiplying percentageComplete by a constant, as suggested by #Thalthanas, is incorrect. That causes the lerping updates to continue occurring after the lerping has completed. It also makes the code hard to understand because timeTakenDuringLerp is no longer the time taken during the lerp.
I've double-checked with my code and it does indeed work, so the problem you are experiencing must be elsewhere. Or maybe you accidentally increased the time, which would decrease the speed?
The solution is to
multiply percentageComplete value with a speed value like,
transform.position = Vector3.Lerp (_startPosition, _endPosition, percentageComplete*speed);
So when you increase speed, it will go faster.

Categories