Introduction
The Rigidbody2D.AddForce in Unity applies force to a 2D gameobject with the Rigidbody component attached to it. The method takes two arguments, the first argument is a Vector2 that represents the force we want to apply to our object and the second argument is a ForceMode2D enumeration that can have one of the following two values: Force or Impulse. Both of those values take the object’s mass into account, the difference is that Force applies the force in each FixedUpdate over time and Impulse applies the force instantly.
Let’s suppose that we have the following scenario: A circle that represents a ball that we want to apply force, but we want this ball to move between two points, its starting location and its destination with a constant speed. If we apply the force there are two problems we have to solve:
- When the ball is going to stop moving: If our ball is moving in a straight line, this is simple, we just have to check when our ball is at the destination and then stop the movement, but if our ball collides with another object then we want a way to calculate the length of the movement and stop the ball when it has traveled that distance.
- The speed of the ball: We want to be able to provide a value for the speed and not for the force as this is more intuitive, then calculate the force we need to apply based on the values of speed and distance
Finally there is one more problem that we may need to solve. It is better for the player to be able to visualize the end of movement, based on where the edge of the ball will be after the movement has stopped, instead of the center of the ball. Something like this:
Calculating the end point
Let’s start by calculating the destination based on the edge of the ball. This can be done by subtracting from the final position (where the center would be) the vector that has a starting point the center of the ball at the final destination and the end point is the center of the ball at the final position.
The outer circle represent the range, the straight line is a line formed between the center of the ball at its starting position and the mouse pointer. By subtracting the ball position from the pointer position, normalizing the resulting vector and multiplying by our range, we get a vector that has the length we need but has its starting point at (0,0). Then we add to that vector the ball position.
In C# code this look like this:
var ballPos = rb.position;
var vectorOfEndPoint = worldMousePos - ballPos;
var centerEndPoint = vectorOfEndPoint.normalized * range + ballPos;
The worldMousePos variable is the current mouse position in world space. This means that after we get the mouse position we have to translate it to world space coordinates like this:
Vector2 mousePos = Mouse.current.position.ReadValue();
Vector2 worldMousePos = Camera.main.ScreenToWorldPoint(mousePos);
The above will give us the coordinates that the straight line intersects with the big circle in the image. This would put our ball’s center there, but to be able to have the edge of the ball at that point, we need to subtract the vector that spans between the final point and our preferred location.
As we can see in our image, that vector has the coordinates where the x value is the cos of the angle formed between our vectorOfEndPoint and the x axis multiplied by the ball radius and the y value is the sin of that angle multiplied by the radius.
In code first we have to find what the angle is, this is easy by writing the following code:
var xAngle = Mathf.Atan2(vectorOfEndPoint.y, vectorOfEndPoint.x);
then the final position would be:
var xCos = Mathf.Cos(xAngle);
var ySin = Mathf.Sin(xAngle);
var edgeEndPoint = centerEndPoint - new Vector2(transform.localScale.x/2f * xCos, transform.localScale.x/2f * ySin);
The transform.localScale.x/2f gives us the radius of the circle.
Calculating the force
Now that we have the coordinates for the final destination (the edgeEndPoint variable) we need to calculate the vector between that point and our ball.
var distance = edgeEndPoint - ballPos;
To calculate the force, we need the mass of our ball, which we can get from the rigidbody, the distance that the ball has to travel that we just calculated it and stored it in the distance variable and the time that we want for the ball to travel to get there.
Because we know the speed that we want the ball to have, the time is distance/speed or in code:
var _travelTime = distance.magnitude / speed;
notice that we take the magnitude of our vector, because that is the length of the vector and then we divide by speed.
Now we can calculate the force:
var force = mass * distance / _travelTime;
and finally call the AddForce method:
rb.AddForce(force,ForceMode2D.Impulse);
Adding check for stopping the movement
We just made our ball to start moving, but without any friction, the ball will keep going on forever. Let’s fix that.
We start by making a variable that represents the movement time, which at first is equal to our travelTime:
_movementTimer = _travelTime;
then in our Update we decrease that variable by the time that has passed:
if(_movementTimer > 0f)
_movementTimer -= Time.deltaTime;
finally in our FixedUpdate, we make a check to stop the movement when the timer is zero:
if (_movementTimer <= 0f)
{
_movementTimer = 0f;
rb.velocity = Vector2.zero;
}
Conclusion
This is the full code including Gizmos in Unity’s OnDrawGizmos event function for better visualization:
public class Ball : MonoBehaviour
{
[SerializeField] private Rigidbody2D rb;
[SerializeField] private float range = 10f;
[SerializeField] private float speed = 5f;
private float _movementTimer;
private void Update()
{
if (Mouse.current.scroll.ReadValue().y > 0f)
range++;
if (Mouse.current.scroll.ReadValue().y < 0f)
range--;
if(Mouse.current.leftButton.wasPressedThisFrame)
Move(Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue()));
if(_movementTimer > 0f)
_movementTimer -= Time.deltaTime;
}
private void FixedUpdate()
{
if (_movementTimer <= 0f)
{
_movementTimer = 0f;
rb.velocity = Vector2.zero;
}
}
private void OnDrawGizmos()
{
Vector2 mousePos = Mouse.current.position.ReadValue();
Vector2 worldMousePos = Camera.main.ScreenToWorldPoint(mousePos);
Gizmos.DrawLine(transform.position, worldMousePos);
Gizmos.DrawWireSphere(transform.position, range);
var ballPos = rb.position;
var vectorOfEndPoint = worldMousePos - ballPos;
var centerEndPoint = vectorOfEndPoint.normalized * range + ballPos;
var xAngle = Mathf.Atan2(vectorOfEndPoint.y, vectorOfEndPoint.x);
var xCos = Mathf.Cos(xAngle);
var ySin = Mathf.Sin(xAngle);
var edgeEndPoint = centerEndPoint - new Vector2(transform.localScale.x/2f * xCos, transform.localScale.x/2f * ySin);
Gizmos.DrawWireSphere(edgeEndPoint, transform.localScale.x/2f);
}
public void Move(Vector2 pos)
{
var mass = rb.mass;
var ballPos = rb.position;
var vectorOfEndPoint = pos - ballPos;
var centerEndPoint = vectorOfEndPoint.normalized * range + ballPos;
var xAngle = Mathf.Atan2(vectorOfEndPoint.y, vectorOfEndPoint.x);
var xCos = Mathf.Cos(xAngle);
var ySin = Mathf.Sin(xAngle);
var edgeEndPoint = centerEndPoint - new Vector2(transform.localScale.x/2f * xCos, transform.localScale.x/2f * ySin);
var distance = edgeEndPoint - ballPos;
var _travelTime = distance.magnitude / speed;
var force = mass * distance / _travelTime;
rb.AddForce(force,ForceMode2D.Impulse);
_movementTimer = _travelTime;
}
}
It is using the new Input system, but can easily be converted to be using the Legacy input. You can also get the project from github.
Thank you for reading and as always if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email, also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.