0
\$\begingroup\$

I'm adding deceleration to my player character, decreasing their velocity over time when the player releases the direction keys.

With the code below, at non-perfect angles, the character seems to re-adjust or curve their direction. This is a video that shows the issue.

# Handle movement
var input_dir := Input.get_vector("Left", "Right", "Forward", "Back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
    if direction:
        $PlayerAnims.play("Headbob")
        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
    else:
        var DEACC : float = SPEED * 0.01
        velocity.x = move_toward(velocity.x, 0, DEACC)
        velocity.z = move_toward(velocity.z, 0, DEACC)
        $PlayerAnims.play("Idle")

How can I slow the player to a stop without changing their direction?

New contributor
MyNamesRubber is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
3
  • \$\begingroup\$ Hey, I watched your video twice and I can not see what you mean by "re-adjust or curve" the direction. For me it looks that he always walks straight where he is heading. I would recommend that you move around and point your crosshair at the exact edge of the green box on your playground and walk forward. If the crosshair moves away from the edge you have indeed a "curve" but I would be surprised to see that :) \$\endgroup\$ Commented Nov 27 at 8:21
  • \$\begingroup\$ @WarrenFaith This should better show the issue \$\endgroup\$ Commented Nov 27 at 21:18
  • \$\begingroup\$ Aye, now I see it too. Thanks! \$\endgroup\$ Commented Nov 28 at 14:11

1 Answer 1

2
\$\begingroup\$

The Source of the Problem

Using move_toward separately on each axis will reduce both x and z toward 0 at the same rate until one of them reaches the destination, leaving the remaining velocity pointing parallel to the remaining axis.

Say we started with velocity.x = 4 and velocity.z = 6, and DEACC = 1. Here's what happens after each tick of deceleration, noting our remaining speed, the change in speed compared to the previous step, and the bearing angle our velocity makes relative to the z axis:

step velocity.x velocity.z speed change bearing
0 4 6 7.2 --- 33.7°
1 3 5 5.8 -1.4 31.0°
2 2 4 4.5 -1.4 26.6°
3 1 3 3.2 -1.3 18.4°
4 0 2 2.0 -1.2 0.0°
5 0 1 1.0 -1.0 0.0°
6 0 0 0.0 -1.0 ---

You can see how the direction of the remaining velocity shifts each tick until only one axis still has a non-zero value. That's the curve you're observing.

The "change" column also shows that our rate of deceleration is not constant: we decelerate more sharply when travelling diagonally, and let off the brakes a little as the velocity aligns with a coordinate axis. This is because decelerating by DEACC on each of two axes separately applies a delta-V with magnitude:

$$\|\Delta v\ \| = \sqrt{\text{DEACC}^2 + \text{DEACC}^2} = \sqrt{2}\cdot\text{DEACC}$$.

That's 41.4% more than the delta-V we get when only one axis is changing.

How to Fix It

If you want to take the behaviour you get when the velocity is pointing directly along the x or z axis, and make it consistent at all angles, then you need to apply the deceleration to both components together, rather than separately:

# Isolate horizontal components (so we don't slow a fall)
var vel2d := Vector2(velocity.x, velocity.z)

# Step the whole vector DEACC units toward (0, 0)
vel2d = vel2d.move_toward(Vector2.ZERO, DEACC)

# Unpack back into the original 3D velocity (preserving y)
velocity.x = vel2d.x
velocity.z = vel2d.y

This is equivalent to applying the scalar move_toward method to the vector's length:

# Magnitude of horizontal components = ground speed
var oldSpeed := sqrt(velocity.x*velocity.x + velocity.z*velocity.z)

# If not moving, we're done (avoids division by zero)
if oldSpeed > 0:
    # Decelerate the speed
    var newSpeed := move_toward(oldSpeed, 0, DEACC)

    # Apply the same scale factor to both x and z
    var scale := newSpeed / oldSpeed
    velocity.x *= scale
    velocity.z *= scale

Because x and z change in the same ratio each tick, we preserve the direction velocity is pointing in, eliminating the curving you were observing. These solutions also ensure your deceleration is consistent in every direction, rather than decelerating more sharply when moving diagonally.


Alternative Solution

The solutions above preserve the linear deceleration used in your question. This operates as though the braking force was coming from a cord attached to a weight on a pulley, applying a constant pull backward until we come to a stop and we let go of the cord (to avoid continuing to accelerate backward toward the pulley). That means there's a jerk right at the end as we come to a stop and release the cord: our acceleration changes from the constant DEACC per tick to nothing in a discontinuous jump.

Many real systems will instead exhibit non-linear deceleration, where the decelerating force is proportional to the current velocity (because a faster-moving object creates more friction with the ground/air, shedding more energy in the same timespan). You might find this feels more natural, because it avoids the sharp jerk at the end.

A common way this is done is with a simple "exponential ease-out":

# Tune inertia to your liking; 0 = instant stop, 1 = no speed loss
var inertia := 0.9
velocity.x *= inertia
velocity.z *= inertia

The downside with this is that it mathematically never reaches zero - it just keeps getting closer. To avoid an annoying slight drift, you can clamp the speed to zero once the ground speed is "small enough".

The ease-out approach is appropriate for something like a puck sliding on a surface or a rolling vehicle, but the linear approach may arguably be better for a character with legs walking / running, or for making the controls feel tight and predictable - try each one and see what feels best for your game.

\$\endgroup\$
0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.