Trigonometry is a scary topic for many game devs...
it certainly was for me when I started making games.
This brief tutorial *attempts* to
dispel some of the fear attached to vector math
through simple, concrete examples.

**Trigonometry and rendering**

It is important to underline that game logic, especially involving math, should *never*
be affected by how you draw stuff on the screen.
Unfortunately, most 2D game engines and rendering systems
typically work with the y-axis pointing South.
This is done out of convenience,
so that (0, 0) is the upper-left corner of the screen.

When using trig function like sine or cosine, we have to start with a few assumptions:

- the y-axis points North
- angles are measured in radians where Pi = 180 degrees = 3.14159 radians
- an angle of 0 points East and it rotates counter-clockwise as its value increases

Gray: projection where the x-axis points right, y-axis points up

Red: vector (3, 3) with an angle of 45 degrees or pi/4

**Vectors and points**

A point is simply a position in space whereas a vector is a combination of direction and magnitude.
Think of a car located at some point (100, 100) with a velocity vector of (0, 10).
The position of the car (100, 100) is a point relative to the origin of some coordinate system.
The velocity of the car (0, 10) is a vector that tells us the heading (angle) and the speed (magnitude).

When programming games we often start with a given heading and speed. These two values are sometimes called "polar" coordinates. It's fairly easy to convert the angle (or heading) and the length (speed) to the nice Cartesian coordinates that we all know and love:

function vpolar(a, l) l = l or 1 local x = math.cos(a)*l local y = math.sin(a)*l return x, y end

The previous function accepts the length as an optional parameter. If we didn't multiply both components by the length, we would end up with a unit vector (length = 1). It's important to remember that all trigonometric functions work in radians. If you prefer using degrees, you have to convert your angle (or heading) to radians using math.rad.

What if we already have an existing vector and want to find its angle? This is where the atan2 function comes into play: it returns a value in radians between -pi to pi. Just remember that atan2 assumes that the y-axis points up and rotates counter-clockwise. When the y value of the supplied vector is negative, atan2 would return a negative angle. atan2 in Lua accepts the y coordinate as the first parameter.

function vangle(x, y) assert(x ~= 0 or y ~= 0) return math.atan2(y, x) end

Red: Polar coordinates where the length of the vector equals: sqrt(3^2+4^2)=5 and the angle equals: atan2(4,3)=0.927295

Green: Cartesian coordinates where x equals: cos(0.927295)*5=3 and y equals: sin(0.927295)*5=4

**Angle between two points**

The previous function is used when dealing with *vectors*.
To find the angle from one point to another,
we have to modify the code slightly:

function angle(x, y, x2, y2) assert(x ~= x2 or x2 ~= y2) return math.atan2(y - y2, x - x2) endThe atan2 function should

**Rotating vectors**

Vectors can be rotated by 90 or 180 degrees with *no* computation at all, simply by swapping and negating their x and y values.

x, y = -y, x -- 90 degrees counterclockwise x, y = y, -x -- 90 degrees clockwise x, y = -x, -y -- 180 degreesThe following function can rotate a vector by an arbitrary angle in radians.

function rotate(x, y, a) local c = math.cos(a) local s = math.sin(a) return c*x - s*y, s*x + c*y end

**Angle between two vectors**

Finding the angle between two vectors is a common problem in games.
Note that the angle between two vectors is *not* the same as the direction from one point to another.
For example, if he have the vectors 1,0 and 0,1 then the angle between them is pi/2 or 90 degrees.
That is, the difference between the headings of the two vectors is 90 degrees.

One approach to finding the angle between vectors is using the arcsine function. The tricky part in the code involves checking the cross product (x*y2 - y*x2) to see if we should negate the angle. This is necessary because arcsine always returns a positive number.

function vangle(x, y, x2, y2) local arc = (x*x + y*y)*(x2*x2 + y2*y2) arc = sqrt(arc) if arc > 0 then arc = math.acos((x*x2 + y*y2)/arc) if x*y2 - y*x2 < 0 then arc = -arc end end return arc end

Another way of calculating the angle between two vectors is by using math.atan2.
Then, we can use the modulo operator ("%") to clamp the resulting angle.
Note that this method is *less* precise than arcsine
and assumes both vectors have non-zero length.

function vangle2(x, y, x2, y2) assert(x ~= 0 or y ~= 0) assert(x2 ~= 0 or y2 ~= 0) local a = math.atan2(y2, x2) - math.atan2(y, x) return (a + math.pi)%(math.pi*2) - math.pi end

It's not difficult to find the distance between two points using the Pythagorean theorem. Imagine that the two points represent the vertices of the hypotenuse on a right-angled triangle. All we have to do is get the square root of the sum of the two sides squared:

function distance(x, y, x2, y2) local dx, dy = x2 - x, y2 - y return math.sqrt(dx*dx + dy*dy) end

The same technique is used to find the length or magnitude of a vector:

function length(x, y) return math.sqrt(x*x + y*y) endNote that to improve efficiency, both code examples use multiplication instead of the more formal: x^2 + y^2.

**Normalizing vectors**

Normalizing a vector means changing its length to 1 while retaining its angle.
You have to be careful with this function since when the vector's x and y values are both 0 a division by 0 will occur.
There shouldn't be a case in your program where you are trying to normalize a zero-length vector.

function normalize(x, y) local d = math.sqrt(x*x + y*y) assert(d > 0) return x/d, y/d end

**Scaling vectors**

Scaling a vector means multiplying its x and y components by a number.
The result is another vector with the same heading, but different length.

function scale(x, y, s) return x*s, y*s endIf we scale a vector by its inverse length (1/length(x, y)) we get a normalized vector whose length is 1.

The dot product is an operation between two vectors whose result is a single number. The dot product is commutative so dot(a,b) == dot(b,a).

function dot(x, y, x2, y2) return x*x2 + y*y2 endThe dot product tells us about the angle (R) between the two vectors. It is positive, when the angle is acute and negative if it's obtuse. When the dot product is zero, the two vectors are perpendicular. For two normalized vectors:

cos(R) = dot(ax, ay, bx, by) R = acos(dot(ax, ay, bx, by)For vectors that may not be normalized:

cos(R) = dot(ax, ay, bx, by)/(length(ax, ay)*length(bx, by) R = acos(dot(ax, ay, bx, by)/(length(ax, ay)*length(bx, by)))

Suppose that we have a moving car P with a heading of h. Using the dot product, we can find if the car is facing a target point T. When the dot product of the h and d (where: d = T - P) is positive, point T is in front the moving car and when negative it must be behind it.

We can take this example one step further by rotating the heading vector by 90 degrees before calculating the dot product. In that case, the sign of the dot product shows us if the target is on the left or right hand side of the car. When the dot product is 0, the heading vector must be pointing directly at the target point T.

**Cross product**

In 3D, the cross product of two vectors produces a third vector perpendicular to the original two.
In 2D, the result is a number equal to the area of the parallelogram spanned by the two vectors.

function cross(x, y, x2, y2) return x*y2 - y*x2 endThe cross product is zero if the two vectors are parallel. Notice that the cross product is not commutative, therefore cross(a,b) == -cross(b,a).

**Vector projection**

Suppose we have two vectors a and b.
If we normalize vector a (a_{n}) and multiply it by the dot product of a_{n} and b we get vector b_{p}.
The resulting vector b_{p} is basically vector b projected onto the axis defined by a.

aThis technique allows us to project vectors to any arbitrary axis. Here is an example with three points P_{n}= normalize(a) b_{p}= a_{n}*dot(a_{n}, b)

a = P_{1}- P_{0}b = P_{2}- P_{0}a_{n}= normalize(a) b_{p}= a_{n}*dot(a_{n}, b)

-- find the sprite's travel distance for this frame local speed = 100 local step = speed*delta -- find the distance to target local dist = distance(sprite.x, sprite.y, target.x, target.y) if dist <= step then -- we have arrived sprite.x = target.x sprite.y = target.y else -- get the normalized vector between the target and sprite local nx = target.x - sprite.x local ny = target.y - sprite.y nx, ny = normalize(nx, ny) -- find the movement vector for this frame local dx = nx*step local dy = ny*step -- keep moving sprite.x = sprite.x + dx sprite.y = sprite.y + dy endThe code above could be simplified, but it's hopefully easier to understand in this more descriptive form.

The formula for finding the desired angle is:

target = atan2(d.y, d.x) heading = atan2(h.y, h.x) angle = (heading - target + pi)%(2*pi) - piWhere h is the heading vector and d is the vector between points P and T. Notice that the resulting angle is in radians.

-- speed of rotation in degrees per second local speed = 180 -- normalized vector to the target local dx = mouse.xaxis - sprite.x local dy = mouse.yaxis - sprite.y local target = math.atan2(dy, dx) -- heading angle local heading = math.rad(-sprite.rotation + 90) -- arc to target local arc = (heading - target + math.pi)%(2*math.pi) - math.pi arc = math.deg(arc) -- calculate the estimated time of arrival local eta = math.abs(arc/speed) if eta < delta then -- target angle reached sprite.rotation = sprite.rotation + arc else -- keep rotating local step = speed*delta if arc < 0 then step = -step end sprite.rotation = sprite.rotation + step end

In the example code, we are producing the heading angle from the current rotation of the sprite. We need to negate and add 90 to the sprite's rotation. This is because the "arrow" drawn on the sprite points North and turns clockwise as its rotation increases. On the other hand, the atan2 function returns 0 for vectors pointing East and this value increases counterclockwise. Notice that to make the example interactive, we are using the mouse coordinates as the target point.

Download: | vector.lua |

References:

N Tutorial by Raigan Burns and Mare Sheppard

Programming Game AI by Example by Matt Buckland

Wolfram Alpha