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