Search code examples
c#godot

How to move_and_slide to a target location and stop


How to use move_and_slide to move and object to a target location in godot 4

Looked up godot docs and some posts about the problem but they never seem to give a solution to moving an object to a target location and make it stop once it reaches the target.


Solution

  • The method move_and_slide (MoveAndSlide in C#) will move the character body according to its velocity property. The velocity, by the way, is defined in global units (world metres if you will).

    Now, remember that velocity is speed with a direction. And remember that speed is distance over time. This is high-school physics.

    So we need to figure out:

    • Direction
    • Distance
    • Time

    I'll be assuming we have a vector target that points to the position in global coordinates. If what you have a node, you would do this:

    GDScript

    var target := node.global_position
    

    Which is the same as

    var target := node.global_transform.origin
    

    C#

    var target = node.GlobalPosition;
    

    Which is the same as

    var target = node.GlobalTransform.Origin;
    

    Figuring out the direction

    GDScript

    var direction := global_transform.origin.direction_to(target)
    

    C#

    var direction = GlobalTransform.Origin.DirectionTo(target);
    

    Figuring out the distance

    GDScript

    var distance := global_transform.origin.distance_to(target)
    

    C#

    var distance = GlobalTransform.Origin.DistanceTo(target);
    

    Figuring out the time

    It's delta, it has always been delta. You are running your code in _physics_process (_PhysicsProcess in C#), right?


    Figuring out the velocity

    GDScript

    var max_speed := (distance / delta)
    velocity = direction * max_speed
    

    C#

    var max_speed = (distance / delta);
    Velocity = direction * max_speed;
    

    OK, here is the thing: what we computed is the velocity to reach the target in a frame, in other words it is the maximum velocity (which is why I called it max_speed).

    Presumably, you want to set a different speed at which the character body moves (it might, for example, be a property that you export so you can set it from the inspector), in which case you will make sure it is at most max_speed:

    GDScript

    var max_speed := (distance / delta)
    velocity = direction * minf(speed, max_speed)
    

    C#

    var max_speed = (distance / delta);
    Velocity = direction * Mathf.min(speed, max_speed);
    

    Why? Because if the character body were to move faster than that it would overshoot. And then the next frame it would try to move the opposite direction... And overshoot again. And the result is the character body moving back and forth across the target position (which would be perceived a jitter).

    So, we limit the speed because we do not what to overshoot the target.


    Put it all together

    GDScript

    var target := node.global_transform.origin
    var direction := global_transform.origin.direction_to(target)
    var distance := global_transform.origin.distance_to(target)
    
    var max_speed := (distance / delta)
    velocity = direction * minf(speed, max_speed)
    move_and_slide()
    

    C#

    var target = node.GlobalTransform.Origin;
    var direction = GlobalTransform.Origin.DirectionTo(target);
    var distance = GlobalTransform.Origin.DistanceTo(target);
    
    var max_speed = (distance / delta);
    Velocity = direction * Mathf.min(speed, max_speed);
    MoveAndSlide();
    

    And yes, the code should as it is. Although technically the character would always be moving, you should not see jittering. But I take you want it to actually stop, we will get to that.


    *Floating point and shenanigans.

    At this point I'm going to tell you that this is a little redundant (the code needs to compute the difference between the current position and the target twice), so I'm going to show you an slightly different way (which will also give insight on what is going on):

    GDScript

    var target := node.global_transform.origin
    var displacement := target - global_transform.origin
    var direction := displacement.normalized()
    var distance := displacement.length()
    
    var max_speed := (distance / delta)
    velocity = direction * minf(speed, max_speed)
    move_and_slide()
    

    Which is the same as:

    var target := node.global_transform.origin
    var displacement := target - global_transform.origin
    var distance := displacement.length()
    var direction := displacement / distance # With caveats, see below
    
    var max_speed := (distance / delta)
    velocity = direction * minf(speed, max_speed)
    move_and_slide()
    

    C#

    var target = node.GlobalTransform.Origin;
    var displacement = target - GlobalTransform.Origin;
    var direction = displacement.Normalized();
    var distance = displacement.Length();
    
    var max_speed = (distance / delta);
    Velocity = direction * Mathf.min(speed, max_speed);
    MoveAndSlide();
    

    Which is the same as:

    var target = node.GlobalTransform.Origin;
    var displacement = target - GlobalTransform.Origin;
    var distance := displacement.Length();
    var direction := displacement / distance; // With caveats, see below
    
    var max_speed = (distance / delta);
    Velocity = direction * Mathf.min(speed, max_speed);
    MoveAndSlide();
    

    Of course, as you might now, getting the length of the vector is done by a generalization of the Pythagoras theorem. Not that you would need to implement that, I mention that to satisfy curiosity.

    The transformation I did to the code should highlight something: If the character is at the target, we have a division by zero (because there would be zero distance). However, in practice:

    • When we ask Godot to normalize a zero vector, it gives us a zero vector.
    • However, normalizing a very small vector might result in unexpected results due to floating point error.
    • Speaking of floating point errors, we cannot rely on the character body reaching the target exactly, so we expect the distance to never be exactly zero.

    Thus it is in our best interest to check for this condition. Which is also the condition when you want it to stop.


    Checking if we reached the target

    Simple, if the distance is approximately zero (or if the displacement is approximately zero, if you prefer) we are at the destination. In which, you can stop moving the character body, and perhaps do something else (e.g. pick the next target).

    So we can check:

    GDScript

    if displacement.is_zero_approx():
        # don't move and slide (i.e. stop), perhaps do something else
    else:
        # continue to move and slide
    

    Or

    if is_zero_approx(distance):
        # don't move and slide (i.e. stop), perhaps do something else
    else:
        # continue to move and slide    
    

    C#

    if (displacement.IsZeroApprox())
    {
        // don't move and slide (i.e. stop), perhaps do something else
    }
    else
    {
        // continue to move and slide
    }
    

    Or

    if (Mathf.IsZeroApprox(distance))
    {
        // don't move and slide (i.e. stop), perhaps do something else
    }
    else
    {
        // continue to move and slide
    }
    

    Another common variation of this is the case where we want the character body to just go in a direction (defined by the target), and just keep going. This would, for example, be useful for a projectile. In that case what you would do is precompute the direction (instead of computing it every time), and forget about the target (you only use it for the initial computation).