Coroutine call stacks - c#

I wanted to use my coroutine to smoothly interpolate the position and rotation of multiple GameObjects with the script attached to them. When I start the coroutine, the smooth part works fine, but all of my Objects get moved to the same position, which was not what I intended. I want to understand why that is the case, and if there is a smart way to deal with it.
This is what my coroutine looks like:
IEnumerator interpolate_Plate(Transform targ)
{
float passedTime = 0;
while (!transform.Equals(targ))
{
passedTime += Time.deltaTime;
transform.position = Vector3.Lerp(transform.position, targ.position, passedTime);
transform.rotation = Quaternion.Lerp(transform.rotation, targ.rotation,passedTime);
yield return null;
}
yield break;
}
I was thinking about creating a mastercoroutine with a list in it and then call the smoothing part.
Is the problem just, that the Reference of targ always gets reset for all coroutinecalls on the stack?
As requested by you the function that calls the coroutine:
public void move_Plate(Transform newPosition)
{
StartCoroutine(interpolate_Plate(newPosition));
}

Okay so I found the solution, since Unity or C# works with pointers and so on the problem was. that I continously changed the transforms of all Objects, since I used the pointer to the transform of the next Object. But I moved that Object to so it all ended up on the last Object I moved.
How to prevent this:
I created a new class which stores my values so position and rotation of the old one. I store an Instance of that in my Class where I move the plates.
I now changed from coroutine to the update method as suggested in the comments. With a flag I check if I should move the Object or not. Then I move it smoothly to the new Position.
Code:
private Trans MoveTo;
private bool move;
void Update()
{
if (move)
{
float passedTime = 0;
passedTime += Time.deltaTime;
transform.position = Vector3.Lerp(transform.position, MoveTo.position, passedTime);
transform.rotation = Quaternion.Lerp(transform.rotation, MoveTo.rotation, passedTime);
}
}
Seems to work.

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.

Game character is not running towards target in a straight line with MoveTowards

This is a turn based game where the enemy character runs and hits the player's characters and then returns to its start position. The enemy's target object (one of the player's characters) is updated after each turn. In the first turn everything is correct and the character runs in a straight line towards the target. However in the following turns when the target changes it runs towards the previous target's position first before moving to the correct target and then it doesn't come back to start position straight away. Like I said, everything is correct in the first turn but when the target changes these behaviours occur.
public Vector3 startPos;
public Vector3 targetPos;
void Start()
{
startPos = transform.position;
targetPos = playerCharacter1.transform.position;
}
void Update()
{
Move(targetPos);
}
public void Move(Vector3 targetPos)
{
StartCoroutine(MoveOverTime());
IEnumerator MoveOverTime()
{
while (transform.position != targetPos)
{
transform.position = Vector3.MoveTowards(transform.position, targetPos, 0.04f * Time.deltaTime);
animator.Play("Run");
yield return null;
}
animator.Play("Attack");
yield return new WaitForSeconds(0.2f);
if (transform.localScale.x > 0f)
{
transform.localScale = new Vector3(-0.175f, 0.175f, 1f);
}
else
{
transform.localScale = new Vector3(0.175f, 0.175f, 1f);
}
while (transform.position != startPos)
{
transform.position = Vector3.MoveTowards(transform.position, startPos, 0.04f * Time.deltaTime);
animator.Play("Run");
yield return null;
}
if (transform.localScale.x < 0f)
{
transform.localScale = new Vector3(0.175f, 0.175f, 1f);
}
else
{
transform.localScale = new Vector3(-0.175f, 0.175f, 1f);
}
yield return null;
}
}
Moving targetPos to a public global value alone is not a good solution!
With the code you provided you are indeed creating a new CoRoutine with every Update loop (as pointed out by #Fredrik Schon). The reason that making targetPos global seems to "work" is that all of those coroutines are using that new location as the target. However you are still creating and executing many coroutines that will only be stopped when they finally reach transform.position == targetPos.
There are a number of things you should fix here:
Don't compare Vector3s (transform.position) like you are as they are floating point numbers. Use instead:
if (Vector3.Distance(transform.position, targetPos) < 0.001f)
...
OR
if (Vector3.Distance(transform.position, targetPos) < Vector3.kEpsilon)
See Unity example on this page: https://docs.unity3d.com/ScriptReference/Vector3.MoveTowards.html
Create only 1 coroutine and have it do the work for you. StartCoRoutine returns a Coroutine value that can be used to stop that specific CoRoutine. (https://docs.unity3d.com/ScriptReference/MonoBehaviour.StopCoroutine.html)
Coroutine moveRoutine;
...
if (moveRoutine == null)
moveRoutine = StartCoroutine(MoveOverTime());
At the end of your MoveOverTime coroutine, just set moveRoutine = null; and then you will ensure that if another Coroutine is needed one will be started. Or alternatively you could keep track of if you have a coroutine using a boolean that you set. Same principle.
Better yet, only start 1 Coroutine during your Start() and modify it to not quit. There is a small overhead in creating coroutines. If you know you plan to have 1 running the whole time your MonoBehaviour is alive, why not just start one at the beginning and add conditions to have it sleep (yield return new WaitForSeconds(0.5f); when you don't want it do do anything. Or have it check if it must do any work at all and just yield return null; immediately if not.
Consider caching your WaitForSeconds. This is just an object like any other, so you can do the following:
WaitForSeconds attackPause;
void Start()
{
attackPause = new WaitForSeconds(0.2f);
}
Then in your coroutine instead of yield return new WaitForSeconds(0.2f); use yield return attackPause;

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

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;
}
}

How can I make the coroutine to start and keep working while the others are already in action?

The script is attached to 3 cubes.
Each cube with another tag.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class raytest : MonoBehaviour
{
public float duration;
public string tag;
private Vector3 originalpos;
private void Start()
{
originalpos = transform.position;
}
private void Update()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100))
{
if (hit.transform.tag == tag)
{
if (transform.position.z != originalpos.z - 1)
StartCoroutine(moveToX(transform, new Vector3(transform.position.x, transform.position.y, transform.position.z - 1), duration));
}
else
{
StartCoroutine(moveToX(transform, originalpos, duration));
}
}
else
{
//reset
StartCoroutine(moveToX(transform, originalpos, duration));
}
}
bool isMoving = false;
IEnumerator moveToX(Transform fromPosition, Vector3 toPosition, float duration)
{
//Make sure there is only one instance of this function running
if (isMoving)
{
yield break; ///exit if this is still running
}
isMoving = true;
float counter = 0;
//Get the current position of the object to be moved
Vector3 startPos = fromPosition.position;
while (counter < duration)
{
counter += Time.deltaTime;
fromPosition.position = Vector3.Lerp(startPos, toPosition, counter / duration);
yield return null;
}
isMoving = false;
}
}
When the mouse is over a gameobject and shoot a ray the object start moving.
When the ray is not hitting the object the object is moving back to it's original place.
But sometimes when I move the mouse over two or even three objects quick the next object is not moving until the first one finished moving.
Sometimes the objects are moving at the same time the first one move forward while the rest still moving back to original position.
I'm not sure why sometimes when hitting another object it's waiting first for the other to back to it's original position and only then start moving the hitting one ? And not moving them at the same time one forward and one back.
The idea is that if I hit a object and start moving forward once I'm hitting another object the first one should start moving back and the one that is hitting should start moving forward parallelly.
Sorry if I don't understand the question properly, but this is what I gather:
If the raycast is hitting an object then its moving one way, if the raycast is not hitting an object then its moving back to its original place.
If this is all you need - aren't coroutines over complicating the issue? For example, you could have a CheckIfRaycast.cs script attached to each of your boxes. Inside that scripts Update()method you could check if its being hit with the raycast or not, then do your desired movement.
Multiple coroutines can cause some strange behaviour, so make sure you stop them with StopCoroutine(coroutine name); or StopAllCoroutines();.
https://docs.unity3d.com/ScriptReference/MonoBehaviour.StopCoroutine.html
https://docs.unity3d.com/ScriptReference/MonoBehaviour.StopAllCoroutines.html
you should identify you coroutines that way:
you have to use different coroutines on different objects
Coroutine c1;
Coroutine c2;
void runCourotines()
{
c1 = StartCoroutine(MoveToX());
c2 = StartCoroutine(MoveToX());
}
void StopCoroutines()
{
StopCoroutine(c1);
}

Rotation of a group of objects in Unity is too fast

I'm trying to rotate a group of objects 90 degrees in unity, so I set all of the objects to the same parent, then rotated the parent, it rotated just fine but it's too fast that I can't see, how can I slow it down? I tried both of codes below and both are the same :\ this code appears in a function that is called by update when the user presses on a button
float totalRotation = 0;
while (Mathf.Abs(totalRotation) < 90){
totalRotation += Time.deltaTime;
Parent.transform.RotateAround(temp.transform.position, flag*Vector3.right, Time.deltaTime);
}
and this one
Parent.transform.RotateAround(temp.transform.position, flag*Vector3.right, 90f);
thanks in advance!
Use a factor for the angle, something like
Parent.transform.RotateAround(temp.transform.position, flag*Vector3.right, Time.deltaTime * 0.5f);
Based on the while I would guess that you actually don't do this in Update. Either way this will not work since update is one frame. You don't want a while to do anything time related like this. Make your while an if instead. That way the rotation will probably be too slow so make the factor bigger. Currently your rotation is instant.
Edit:
Something like this would work:
public bool isRotating = false;
public float speed = 20.0f;
public Vector3 targetRotation;
void Update()
{
if(isRotating)
{
if(Vector3.Distance(transform.eulerAngles, targetRotation) > 1.0f)
{
transform.Rotate(Vector3.up * Time.deltaTime * speed);
}
else
{
transform.eulerAngles = targetRotation;
isRotating = false;
}
}
}
This would be around y only.
This part of the answer doesn't work. Please see the update
Actually you did it in the wrong way. Movements in Unity should be done slowly through updates, not just once. Like ChristophKn, I suggest using Coroutine.
const float speed = 180;
IEnumerator Rotate () {
float rotated = 0;
while (rotated < 90) {
float rotation = speed*Time.fixedDeltaTime;
rotated += rotation;
Parent.transform.RotateAround(temp.transform.position, flag*Vector3.right, rotation);
//wait for the next fixed update to continue.
yield return new WaitForFixedUpdate ();
}
}
To start rotating, call StartCoroutine(Rotate);
EDIT
I have another idea to do this, not using script but animation:
First, add a rotating animation to your parent objects, which rotates the angle you want.
To rotate the group of objects, set them to that parent like you did in your question, then start the animation.
At the end of the animation (you can call your methods at that point using animation events), call SetParent(null,true) for all your objects in the group. This will remove the parent but keep the world position.
Finally, set your parent's rotation to the original value.
Hope this will help.

Categories