2D vector math hero - polar movement

Vector math for polar movement in 2D games (Unity)

Polar movement, i.e. moving objects at an angle, is something that people starting out in games programming often have trouble with. Coordinate systems are easy to understand, and so is moving things left and right or up and down; but what if you want to move at angles that are not parallel to an axis, like 30° upwards, or towards a target? How do you get a vector that represents that direction of movement?

This article requires some basic knowledge about vector math, so it will be helpful to read up Vector math basics for games programming first if you are not familiar with the topic.

Distance between two objects

It’s not exactly “movement”, but understanding how to find the distance between 2 objects is essential to what we are going to explore further down in the article. Unity has a Vector2.Distance() method that removes the need for us to do any vector math:

float distance = Vector2.Distance( transform.position, target.position );

But we’re more interested in understanding the math behind it here. Using Vector2.Distance() is the same as doing the following:

Vector2 difference = transform.position - target.position;
float distance = difference.magnitude;

Essentially, we are:

  1. Getting the difference vector between the 2 objects, and;
  2. Getting the magnitude (length) of this difference vector
Distance between two objects
The difference vector between the two objects.

The difference vector is the line drawn between the two objects in the image above. It can point in either direction, because there are 2 possible difference vectors:

  • A – B: which gives you a vector that points towards A
  • B – A: which gives you a vector that points towards B, in the opposite direction of A – B.

For those looking to impress people at the dinner table, here is how you write it mathematically:

d is the distance vector between A and B , d = A B OR d = B A Distance between A and B = | d | = d x d y

Article continues after the advertisement:


Moving towards a target (linearly)

To move an object towards a target at a particular speed (expressed by moveSpeed in the snippet below), you can use the following line using Vector2.MoveTowards() in your script’s Update() method. In the script below, target is the Transform component of the target you are following, assigned to the same script. You have to declare it as a property of your script and assign it to make it work.

transform.position = Vector2.MoveTowards( transform.position, target.position, moveSpeed * Time.deltaTime );

This will move your object’s position by moveSpeed * Time.deltaTime units at every frame until it reaches its destination. moveSpeed, of course, should be the distance your object can travel in 1 second.

How do you move towards a target?
Moving towards a target.

Here’s how to do it if you do the vector math yourself — the code below gives almost the same result.

Vector2 difference = target.position - transform.position;
transform.position += difference.normalized * moveSpeed * Time.deltaTime;

The difference vector is back, except this time, it has to be pointing towards the target. Hence, only target.position - transform.position can work.

What does difference.normalized do?

It normalises the difference vector, giving us a unit vector that points in the same direction as the difference vector. A unit vector is a vector with a magnitude (i.e. length) of 1 unit.

As the image notes, in a unit vector, only the length of the vector is 1. The x and y values are actually the cosine and sine of the vector’s angle, respectively. This is because:

cos θ = adjacent hypotenuse = x length sin θ = opposite hypotenuse = y length

What’s special about a unit vector is that you can set its length very easily by multiplying it with a scalar value, since its length becomes whatever value you multiply it by.

Almost the same as Vector2.MoveTowards

Our code snippet doesn’t actually do the same thing as Vector2.MoveTowards(), because it is possible for our object to move past its target in certain situations. We need an additional line in the snippet of code to prevent it from doing so.

Vector2 difference = target.position - transform.position;
float travelDistance = Mathf.Min( difference.magnitude, moveSpeed * Time.deltaTime );
transform.position += difference.normalized * travelDistance moveSpeed * Time.deltaTime

The additional line of code takes the smaller of the two values between difference.magnitude and moveSpeed * Time.deltaTime. This makes it so that when difference.magnitude is smaller than moveSpeed * Time.deltaTime, we will not overshoot our destination.

Tracks a moving target

Note that both Vector2.MoveTowards and our own written ones follow a moving target if placed in Update(). Your following object will change its trajectory to continue following the target.

Our code naturally causes our object to track the target.

Moving towards a target (easing out)

Besides Vector2.MoveTowards(), we can also use Vector2.Lerp() to move towards a particular target. Again, this goes into your script’s Update() method, and target is a property containing the Transform component of the target being followed.

transform.position = Vector2.Lerp( transform.position, target.position, lerpFactor * Time.deltaTime ); 

Vector2.Lerp() is different from Vector2.MoveTowards() because it moves faster the further you are from the target, and slows down the nearer it gets. lerpFactor controls how fast the object moves, though it should be greater than 0 and less than 10. If it is less than 0, the object moves backwards; and if it is more than 10, the movement is so fast it will almost look like its teleporting.

Move towards with Lerp
The moving object eases into its destination.

Without using Vector2.Lerp(), this is how to achieve a similar result using plain ol’ vector math:

Vector2 difference = target.position - transform.position;
transform.position += difference * lerpFactor * Time.deltaTime;

As Time.deltaTime (i.e. time between 2 frames) will almost always be hovering between 0.02 to 0.1, lerpFactor * Time.deltaTime will always be less than 1 unless your lerpFactor is very high. This means that the distance moved will always be a percentage of the total distance, which also means the distance moved gets smaller as you travel because the total distance decreases.

Lerp distance frame by frame
How an object using Vector2.Lerp moves.

To make our code function the same as Vector2.Lerp(), we have to cap lerpFactor * Time.deltaTime to be between 0 and 1. This prevents it from overshooting its destination or moving backwards when lerpFactor is below 0.

Vector2 difference = target.position - transform.position;
transform.position += difference * Mathf.Min( 1, Mathf.Max( 0, lerpFactor * Time.deltaTime ) );

Tracking also included

If you put either piece of code in Update(), it will cause our object to track the target too. Our object will maintain its non-linear movement though.


Article continues after the advertisement:


Moving at an angle

To move an object at a specified angle, we’ll have to use some trigonometry. Remember how the x and y values of a unit vector can be found by using cosine and sine respectively? So if you want to get a vector pointing 30° upwards (and to the right):

float angleRadians = 30 * Mathf.Deg2Rad;
Vector2 direction = new Vector2( Mathf.Cos(angleRadians), Mathf.Sin(angleRadians) );

Note that Mathf.Cos() and Mathf.Sin() takes angles in radians, so you have to remember to convert the angles with Mathf.Deg2Rad.

You can then multiply the direction vector — which is a unit vector — with a float value to set the length of the vector.

transform.position += direction * distance;
transform.position += direction * moveSpeed * Time.deltaTime

Alternatively, you can also set the length of the vector when creating the vector:

float angleRadians = 30 * Mathf.Deg2Rad;
Vector2 direction = new Vector2( Mathf.Cos(angleRadians) * distance, Mathf.Sin(angleRadians) * distance );

Unfortunately, Unity doesn’t seem to have helper methods for this. At least, not that I know of.

Should we use Unity’s shortcuts?

With convenience methods like Vector2.MoveTowards() and Vector2.Distance() already made for us, you might be wondering why we should bother to learn vector math behind them. In some cases, it can actually optimise our code. Consider the example below:

void Update() {
    // Only start moving towards the target if it is within our acquisition range.
    if ( Vector2.Distance( target.position, transform.position ) < acquisitionRange ) {
        transform.position = Vector2.MoveTowards( transform.position, target.position, moveSpeed * Time.deltaTime );
    }
}

If we avoided using Unity’s helper methods, we could have done it this way.

 void Update() {
    // Only start moving towards the target if it is within our acquisition range.
    Vector2 difference = target.position - transform.position;
    if ( difference.sqrMagnitude < acquisitionRange * acquisitionRange ) {
        transform.position += difference.normalized * travelDistance;
    }
} 

This new piece of code has 2 main advantages over the previous piece of code:

  1. The same difference vector is used to measure distance and move us towards the target. If we used Vector2.Distance() and Vector2.MoveTowards, the same difference vector would’ve been computed twice separately.
  2. By not using Vector2.Distance(), we had the advantage of comparing the squares of distances instead. Recall that to find a vector’s length, we need to get the square root of x2 + y2. Since square roots are relatively more expensive to compute than multiplication, this optimises the code.

So there are benefits to not using Unity’s math helper methods. It may be time-consuming to learn the vector math initially, but in the long run, you will have greater control over your code. This not only opens up opportunities for optimisation, but also gives you the capability to really make your game the way you want it.


Article continues after the advertisement:


Leave a Reply

Your email address will not be published. Required fields are marked *

Note: You can use Markdown to format your comments.

This site uses Akismet to reduce spam. Learn how your comment data is processed.