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.
CodePudding user response:
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;
}
}