- Introduction

- Angles and rotation

- Distance and length

- Products and projection

- Real-time applications

- References

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

Example 1: The coordinate system is projected so that the x-axis points right and the y-axis points up. The green arc segment represent an angle (measured in radians).

radians = math.rad(degrees) degrees = math.deg(radians)As you already know, PI or 3.14159265359 radians is equivalent to 180 degrees. It's quite simple to convert between the two different units.

radians = degrees*(math.pi/180) degrees = radians*(180/math.pi)Just remember that radians are numbers and they can be negative too!

function vpolar(a, d) d = d or 1 local x = math.cos(a)*d local y = math.sin(a)*d return x, y endThe 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().

Example 2: Polar vs Cartesian coordinates

What if we already have an existing vector and want to find its angle? This is where the atan2 function comes into play. Just remember that atan2 assumes that the y-axis points up and rotates counter-clockwise.

function vangle(x, y) return math.atan2(y, x) endPlease note that this function accepts the y coordinate as its first parameter. When the y value of the supplied vector is negative, atan2 returns a negative angle. In fact, this function

positive = radians%(math.pi*2)

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

Example 3: Angle from one point to another, measured using atan2 (from the right in counter-clockwise direction)

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

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. This method works when dealing strictly with angles too. The only requirement is that both vectors have non-zero length.

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

Example 4: Angle between vectors

function length(x, y) return math.sqrt(x*x + y*y) endFor efficiency purposes, the code examples use multiplication instead of the more formal: x^2 + y^2.

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

Example 5: Distance between two points

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

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.

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 = math.acos(dot(ax, ay, bx, by)For vectors that may not be normalized:

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

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 is positive (where: d = T - P), point T is in front the moving car and when negative it must be behind it. If the dot product is zero, the car's heading is perpendicular to point T.

Example 6: Dot product

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.

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).

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) P_{3}= b_{p}+ P_{0}

Example 7: Projecting the point P

local sprite = { x = 0, y = 0, speed = 100 } function sprite.updatePosition(dt) -- vector from sprite to target local mx, my = love.mouse.getPosition() local dx = mx - sprite.x local dy = my - sprite.y local dist = math.sqrt(dx*dx + dy*dy) local step = sprite.speed*dt if dist <= step then -- we have arrived sprite.x = mx sprite.y = my else -- normalize vector (between the target and sprite) local nx = dx/dist local ny = dy/dist -- keep moving sprite.x = sprite.x + nx*step sprite.y = sprite.y + ny*step end endThe code above could be simplified, but it's hopefully easier to understand in this more descriptive form.

Example 8: Moving at constant speed toward the mouse cursor.

angle = (target - heading + pi)%(2*pi) - piNotice that the resulting angle measures the arc from the current heading to the target.

local sprite = { x = 0, y = 0, angle = 0, turnrate = math.pi } function sprite.updateAngle(dt) -- angle from sprite to target local mx, my = love.mouse.getPosition() local dx = mx - sprite.x local dy = my - sprite.y local target = math.atan2(my - dy, mx - dx) -- arc between heading and target local dist = target - sprite.angle dist = (dist + math.pi)%(math.pi*2) - math.pi local step = sprite.turnrate*dt if math.abs(dist) <= step then -- target angle reached sprite.angle = target else -- keep rotating if dist < 0 then step = -step end sprite.angle = sprite.angle + step end end

Example 9: Rotating at constant speed toward the mouse cursor (T).

- N Tutorial by Raigan Burns and Mare Sheppard
- Programming Game AI by Example by Matt Buckland
- Wolfram Alpha