Search code examples
c#unity-game-enginegame-physicsrigid-bodies

Unity Physics - Calculate the deceleration needed to stop at a specified distance


I have been trying to find a solution to this problem for a while now, but I feel like I lack the understanding of how physics work to come to the right solution. Hopefully someone here can help me!

The Problem

Like the title states - what I want to accomplish is to decelerate a moving object to a complete stop and reach a specific distance.

Context

I am specifically trying to implement this to be used in a player controller, where once input is no longer provided I take the moving object's current velocity and slow it to a stop x units from its position at the point of release.

Current Approach

Currently I understand that I know (a) the initial velocity (b) the target velocity (c) the distance traveled over the change in velocity and (d) the target distance to reach.

I have been able to get this working at a specific speed of 5 units / second using this script:

public class DecelToStop : MonoBehaviour

{ Rigidbody2D rb;

public float speed = 5f; // velocity of object while input is pressed
public float stoppingDistance = 3f; // distance object should stop at on input released
public float stopTimeMultiplier = 3.5f; // multiplier applied to time step to reach desired stopping distance


bool inputIsReleased = false;
float decelerationNeeded = 0;

public float GetDeceleration(float initalVelocity, float targetVelocity)
{
    
    float travelTime = stoppingDistance / initalVelocity; // total time needed to reach a stop
    float velocityChange = targetVelocity - initalVelocity;// total change in velocity
    float decelTimeMultiplier = Mathf.Sqrt(stoppingDistance * stopTimeMultiplier); // how much to multiply the travel time by
    float deceleration = initalVelocity / (travelTime * decelTimeMultiplier); //amount of deceleration to apply each fixed update

    return deceleration;
}

private void FixedUpdate()
{
    // get deceleration needed on input release
    if (!inputIsReleased)
    {
        decelerationNeeded = GetDeceleration(speed, 0);
        inputIsReleased = true;
    }

    // apply total force needed by applying the inital speed and the deceleration neeed 
    if (rb.velocity.x != 0)
    {
        rb.AddForce(new Vector2(speed, 0)); 
        rb.AddForce(new Vector2(decelerationNeeded, 0));
    }
}

}

The problem with my current approach is once I change the speed variable the stopTimeMultipler becomes exactly what I am trying to avoid - which is a bunch of guess work to find the exact value needed for everything to work properly.

I'm sure there are multiple flaws in this approach - like I said I don't have a great understanding of physics calculations - so if you have a solution to this if you could explain it like you were talking to a 5 year old that would be great! The solution doesn't need to hit the exact stopping distance - there can be some variation as long as it is relatively close (within 0.2 units) and can scale with varying speeds and stopping distances.


Solution

  • Ok, after spending some more time on this I have been able to find a scalable solution.

    I have completely reworked my approach - and instead switched to accelerating and decelerating my rigidbody using the methods shown in these two videos: https://www.youtube.com/watch?v=uVKHllD-JZk https://www.youtube.com/watch?v=YskC8h3xFVQ&ab_channel=BoardToBitsGames (I recommend watching these to fully understand how acceleration is implemented)

    These videos allowed me to accelerate and decelerate an object to a target speed within a specified amount of time.

    From here I was able write a method that converted a provided distance value into the time variable needed to find the correct amount of acceleration to apply each update.

    I found the formula to do this here: https://physics.stackexchange.com/questions/18974/what-do-i-need-to-do-to-find-the-stopping-time-of-a-decelerating-car

    But for the c# implementation look to the ConvertDistanceToVelocityStep() method in the code below.

    All my testing so far shows that this approach allows for only a max speed and desired stopping distance to be provided to slow a moving object to a complete stop at a specified distance once input is no longer provided.

    Here is the full script with notes - if you have any optimizations or suggested improvements feel free to leave them below.

    public class Accelerator : MonoBehaviour
    

    { Rigidbody2D m_Body2D;

    // - Speed
    public float maxSpeed = 6f;
    
    // - Distance
    public float stoppingDistance = 1f;
    public float accelDistance = 1f;
    
    // - Time
    float timeZeroToMax = 2.5f;
    float timeMaxToZero = 6f;
    float accelRatePerSec;
    float decelRatePerSec;
    float xVel;
    
    public bool inputPressed = false;
    public bool allowInputs = true;
    Vector2 lastHeldDirection;
    
    // - get any needed references
    private void Awake()
    {
        m_Body2D = GetComponent<Rigidbody2D>();
    }
    
    // - convert distance values into an acceleration to apply each update
    void ConvertDistanceToVelocityStep()
    {
        //acceleration
        timeZeroToMax = (2 * accelDistance) / (maxSpeed - 0);
        accelRatePerSec = maxSpeed / timeZeroToMax;
        //deceleration
        timeMaxToZero = (2 * stoppingDistance) / (0 + maxSpeed);
        decelRatePerSec = -maxSpeed / timeMaxToZero;
    }
    
    private void Start()
    {
        ConvertDistanceToVelocityStep();
        xVel = 0;
    }
    
    private void Update()
    {
        // if inputs are allowed - check when horizontal buttons are pressed
        if (allowInputs)
        {
            if (Input.GetButtonDown("Horizontal"))
                inputPressed = true;
            else if (Input.GetButtonUp("Horizontal"))
                inputPressed = false;
        }
        else inputPressed = false;
    }
    
    private void FixedUpdate()
    {
        // if a valid input is provided
        if (inputPressed && allowInputs)
        {
            // get direction
            InputDirection();
            // get acceleration
            Accelerate(accelRatePerSec);
            // apply acceleration in desired direction
            m_Body2D.velocity = new Vector2(lastHeldDirection.x * xVel, m_Body2D.velocity.y);
        }
        // if input no longer pressed
        else
        {
            // while still moving
            if (Mathf.Abs(m_Body2D.velocity.x) > 0.01f)
            {
                // get deceleration
                Accelerate(decelRatePerSec);
                // apply deceleration in last held direction
                m_Body2D.velocity = new Vector2(lastHeldDirection.x * xVel, m_Body2D.velocity.y);
            }
            else
                // bring x velocity to zero
                m_Body2D.velocity = new Vector2(0, m_Body2D.velocity.y);
        }
    }
    
    // calculate x velocity to move rigidbody
    void Accelerate(float accelRate)
    {
        xVel += accelRate * Time.deltaTime;
        xVel = Mathf.Clamp(xVel, 0, maxSpeed);
    }
    
    Vector2 InputDirection()
    {
        // get both axis of input
        float hor = Input.GetAxis("Horizontal");
        float vert = Input.GetAxis("Vertical");
        // save to vector2
        Vector2 inputDir = new Vector2(hor, vert);
        // round last held direction to whole number
        if (Mathf.Abs(inputDir.x) > 0.25f)
        {
            if (inputDir.x > 0)
                lastHeldDirection.x = 1;
            else lastHeldDirection.x = -1;
        }
        //normalize diagonal inputs
        if (inputDir.magnitude > 1)
            inputDir.Normalize();
        // return input direction
        return inputDir;
    }
    

    }