3
\$\begingroup\$

I'm using this coroutine to move my player to a new position. The problem is that it takes the duration as an argument instead of the speed of the object. Which makes it hard for me to make sure the object always moves at the same speed.

How can I create a coroutine that takes the source position, target position and movement speed to move my character?

This is my current code:

IEnumerator MoveObject(Vector3 source, Vector3 target, float duration)
{
    float startTime = Time.time;
    while(Time.time < startTime + duration)
    {
        player.transform.position = Vector3.Lerp(source, target, (Time.time - startTime) / overTime);
        yield return null;
    }
    player.transform.position = target;
}
\$\endgroup\$
5
+50
\$\begingroup\$

You can use something like this:

 IEnumerator MoveFromTo(Transform objectToMove, Vector3 a, Vector3 b, float speed) {
         float step = (speed / (a - b).magnitude) * Time.fixedDeltaTime;
         float t = 0;
         while (t <= 1.0f) {
             t += step; // Goes from 0 to 1, incrementing by step each time
             objectToMove.position = Vector3.Lerp(a, b, t); // Move objectToMove closer to b
             yield return new WaitForFixedUpdate();         // Leave the routine and return here in the next frame
         }
         objectToMove.position = b;
 }

The main thing to note is the step calculation. It first takes the input speed (in m/s) and projects its value on the distance between the source (a) and the target (b). Then it is multiplied by Time.fixedDeltaTime (Look at @DMGregory's comments) to get the projected distance.

\$\endgroup\$
  • 1
    \$\begingroup\$ The risk here is that Time.deltaTime is evaluated only once, when the coroutine starts. Since Unity's update loop uses a variable time step, this could give a mismatched step value for future frames, making the object appear to move at an inconsistent speed. (Especially if Time.timeScale changes in the meantime, such as for slow-mo effects or pausing) You can fix this by moving deltaTime inside the while loop, or by yielding return new WaitForFixedUpdate() and using Time.fixedDeltaTime instead. \$\endgroup\$ – DMGregory May 9 '15 at 3:47
  • 1
    \$\begingroup\$ Also, this coroutine can exit with objectToMove almost at position b, but not exactly there (since adding step might jump from t < 1 to t > 1). Adding a final objectToMove.position = b after the while loop will fix this. \$\endgroup\$ – DMGregory May 9 '15 at 4:11
1
\$\begingroup\$

When talking about 'speed' there are a couple of ways of thinking about it. You can think about it one way, like 'meters per frame' (ie: for each frame, I want the player to move X meters). For that type of functionality, see this:

public IEnumerator MoveAtSpeedCoroutine(Vector3 end, float speed){
    //while you are far enough away to move
    while (Vector3.Distance(this.transform.position,end)>speed){
        //MoveTowards the end position by a given distance
        this.transform.position = Vector3.MoveTowards(this.transform.position, end, speed);
        //wait for a frame and repeat
        yield return 0;
    }
    //Since you are really really close, now you can just go to the final position.
    this.transform.position = end;
}

The problem with this way is it can look weird if the framerate is not perfectly constant.

The other way (and the best way) would be to keep track of 'meters per second'. This way, if the game lags out for a couple of frames, it should still look just fine (ie: if you want to move X meters over 1 second, and if you get a constant 100 frames per second, each frame the object will be moved X/100th of a meter; Whereas, lets say I get to a really choppy part of my game (heaven forbid) that the game was lagging for the whole second, and only ran one frame in one second, then the object would be moved X/1). In the first way, if I ran the same game on two computers, and the first computer was laggy and the second one wasn't, and I played my game (lets say a racing game), then the laggy computer would always lose and it will always move slower than the non-laggy computer. Whereas with the second way (code below), even if one computer laggs a bunch, they both should keep neck and neck (even though on the non-laggy computer will look a bunch better :D) The code for the second way is as follows:

public IEnumerator MoveAtSpeedCoroutine(Vector3 end, float speed){
    while (Vector3.Distance(this.transform.position,end)>speed*Time.deltaTime){
        this.transform.position = Vector3.MoveTowards(this.transform.position, end, speed*Time.deltaTime);
        yield return 0;
    }
    this.transform.position = end;
}

Note, the only change is multiplying speed by Time.deltaTime - this converts it from 'meters per frame' to 'meters per second'. Why this works is Time.deltaTime is the amount of time that passed last frame (a percent out of 1 second). If the computer lagged out for a whole second, Time.deltaTime would be 1, and if your game rendered at 200 FPS (5ms total render time) for this frame, then Time.deltaTime would be 0.005. If you multiply your speed by this number, then no matter your framerate is, that speed is the amount of meters the object will move in 1 second.

With that said, you could do this with lerp as well (I don't care for it in this instance myself... but this is an option). You can do it like so:

public IEnumerator MoveAtSpeedCoroutine(Vector3 end, float speed){
    Vector3 start = this.transform.position;
    //total time this has been running
    float runningTime = 0;
    //the longest it would take to get to the destination at this speed
    float totalRunningTime = Vector3.Distance(start,end) / speed;
    //for the length of time it takes to get to the end position
    while (runningTime<totalRunningTime){
        //keep track of the time each frame
        runningTime += Time.deltaTime;
        //lerp between start and end, based on the current amount of time that has passed
        // and the total amount of time it would take to get there at this speed.
        this.transform.position = Vector3.Lerp(start, end, runningTime/totalRunningTime);
        yield return 0;
    }
    this.transform.position = end;
}

And even more still, one way to look at speed is using physics. So... you got a lot of choices :D

Hope that helps and good luck! :D

ps. Scale...

I would do that as follows:

public IEnumerator MoveAtSpeedCoroutine(Vector3 end, float speed){
    //figure out the scale you want to use - here I will just take the average scale
    float scale = (this.transform.localScale.x+this.transform.localScale.y+this.transform.localScale.z)/3;
    //lets now use the scale to make a new speed. The new speed will be based off of the old speed
    //and also the scale.
    float speedAtScale = scale * speed;
    while (Vector3.Distance(this.transform.position,end)>speedAtScale*Time.deltaTime){
        this.transform.position = Vector3.MoveTowards(this.transform.position, end, speedAtScale*Time.deltaTime);
        yield return 0;
    }
    this.transform.position = end;
}
\$\endgroup\$
  • \$\begingroup\$ It is meters, not pixels. Unity's default unit both in 2D and 3D is one unit (corresponding to one meter). You have written pixels as the unit everywhere ;) not a major mistake, but will help to clear some misunderstanding. \$\endgroup\$ – EvilTak May 11 '15 at 6:05
0
\$\begingroup\$

You can do this:

void moveObjectBySpeed(Vector3 source, Vector3 target, float speed)
{
  pathLength = (target - source).magnitude;
  duration = pathLength / speed;
  moveObject(source, target, duration);
}

IEnumerator moveObject(Vector3 source, Vector3 target, float duration)
{
    float startTime = Time.time;
    while(Time.time < startTime + duration)
    {
        player.transform.position = Vector3.Lerp(source, target, (Time.time - startTime) / overTime);
        yield return null;
    }
    player.transform.position = target;
}
\$\endgroup\$
  • \$\begingroup\$ Why are you using Time.time - startTime? Keeping track of the elapsed time would be easier and less confusing. \$\endgroup\$ – JPtheK9 May 9 '15 at 23:32
0
\$\begingroup\$

Perhaps something like this:

[RequireComponent(typeof(Rigidbody))]
public class MovementTowardsTarget : MonoBehaviour
{
    private void Start()
    {
        var target = GameObject.Find("Target");
        StartCoroutine(MoveObject(target, 1.0f));
    }

    private IEnumerator MoveObject(GameObject target, float speed)
    {
        var direction = target.transform.position - transform.position;
        GetComponent<Rigidbody>().velocity = direction * speed;
        yield return null;
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject == GameObject.Find("Target"))
        {
            GetComponent<Rigidbody>().velocity = Vector3.zero;
        }
    }
}

If you are looking for something more verbose, you may be able to tweak a script I wrote for 2D games. It would be the MovePositionInLine() method. Source Code here.

Also this script could be exactly what you are looking for. Not sure what you are trying to accomplish, but its worth a look.

\$\endgroup\$
  • \$\begingroup\$ Hi @Evorlor your soltuion is simple, yet I don't understand how the character will stop when he reaches the target position. \$\endgroup\$ – Lisandro Vaccaro May 9 '15 at 1:46
  • \$\begingroup\$ @LisandroVaccaro oops. It sure doesn't. Sorry about that. You can write an if statement checking to see if it's within some small distance of the target, and set its velocity to 0 when it is. Or not set it to 0 and it will swing around a bit. I'm not at my computer at the moment, but I can update the code tomorrowish. Sorry about that! \$\endgroup\$ – Evorlor May 9 '15 at 1:49
  • \$\begingroup\$ I think I might add a while at the end to check if it passes the target position and if has stop it. I'm not sure how to check if it has passed the spot though, Vectors aren't really my thing, I'll try to do it tomorrow though. \$\endgroup\$ – Lisandro Vaccaro May 9 '15 at 1:58
  • \$\begingroup\$ @LisandroVaccaro In pseudocode: dot(direction, (position - source)) >= dot(direction, (target - source)) will evaluate true when the target is reached. The second half is equivalent to length(target - source) but it's slightly more efficient if you already know the normalized direction. Alternatively you could calculate the distance from source to target and use it to manually normalize the direction. Either way, you want to avoid as many redundant sqrt operations as possible. \$\endgroup\$ – bcrist May 9 '15 at 2:59
  • \$\begingroup\$ @LisandroVaccaro I updated my script. As is, it effectively finds the direction it needs to go, moves in that direction until it hits the object, and then stops. It does not take into account the target moving, and it assumes both objects are physical objects that can be hit. I will update if this does not suit your needs. Just lemme know. :) \$\endgroup\$ – Evorlor May 9 '15 at 19:47
-1
\$\begingroup\$

Get the velocity to move at based on the object's size and the speed. Move with that velocity. Then keep checking if the object has passed the target.

IEnumerator MoveObject (Vector3 source, Vector3 target, float speed)
{
    int Dot1 = (int)Mathf.Sign(Vector3.Dot(source, target)); //Get the original orientation
    int Dot2 = Dot1; //This is the current orientation 
    Vector3 velocity = (target - source).normalized * speed * player.Size; //Get the velocity to travel at
    while (Dot1 == Dot2) //If the original orientation and current orientation are the same, we haven't passed the target yet
    {
        player.transform.position += velocity * Time.deltaTime;
        Dot2 = (int)Mathf.Sign(Vector3.Dot(player.transform.position,target)); //Get and set the current orientation
        yield return null;
    }
}
\$\endgroup\$
  • \$\begingroup\$ This doesn't address the asker's request to be able to specify a speed parameter. \$\endgroup\$ – DMGregory May 9 '15 at 23:40
  • \$\begingroup\$ Wait... why would a duration be applied in the first place then? I'll change it up a bit. This question is really confusing lol. \$\endgroup\$ – JPtheK9 May 9 '15 at 23:40
  • \$\begingroup\$ Okay, satisfied? \$\endgroup\$ – JPtheK9 May 9 '15 at 23:47
  • \$\begingroup\$ Honestly, no. Dot(source, target) never changes, so this loop will run forever. \$\endgroup\$ – DMGregory May 10 '15 at 14:43
  • \$\begingroup\$ Ugh, yeah. Wrote this kinda hurridely lol. Thanks for catching me on that. \$\endgroup\$ – JPtheK9 May 10 '15 at 20:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.