Angle math

Sometimes you want to move at an angle, or are making a boss with jointed sprites and need to spin a limb, or you want an enemy that chases the player at an arbitrary angle, etc. For all this stuff, you need to learn how to do basic math with angles. This page will show you how to do some common stuff.

This page does not assume you have a good grasp of trigonometry and will explain whatever concepts are needed. These concepts are good for games in general, but some remarks are specific to 68000 (or other old CPUs).

Different angle units

An angle measures in which direction something is rotated. Angles go from 0 to a "full circle", and any values beyond that just repeat the directions (e.g. one and quarter circles is the same as a quarter circle).

Angles can be measured in different units:

But there's no reason why you have to stick with those! A common way to measure angles in old games is by picking a power of two as a full circle e.g. 256, 1024 or 65536. This has the advantage that you can use an AND operation to "normalize" an angle into the 0…full circle range (important when you're going to use the angle e.g. with a table).

We recommend using a power of two as a full circle in Mega Drive games.

Converting between angle units

Converting from one unit to another is simple:

angle ÷ circle in old unit × circle in new unit

E.g. to convert from degrees to an unit where a circle is 256:

angle ÷ 360 × 256

Sines and cosines

First of all two concepts you absolutely need to know: sines and cosines. Don't worry, we won't show the hard to understand triangle stuff from trigonometry class, but rather you just need to remember this:

In other words:

x = cos(angle)
y = -sin(angle)

The above assumes that angles are counterclockwise and that 0° points to the right. We need to flip Y's sign because in our games Y goes down instead of up (if you don't do this, angles will be clockwise instead, you decide!).

Sines look-up table

The 68000 can't do floating point at a reasonable speed, let alone computing sines on the fly. Instead, what we do is use a "sines look-up table", which is a table that has the sine values for every possible angle (or if that'd make the table too large, at least enough angles to make it feel smooth).

Whip up some quick program or script and run it in your computer, then use the resulting table in your game. Generating the table is simple, just compute sin(angle) for every angle (using whatever the standard library provides):

for angle = 0 to MAX_ANGLE
  table[angle] = sin(angle)
end

However as we just said, the 68000 can't do floating point, and you're likely working with fixed point instead. As such, you'll want to "pre-multiply" all the values by whatever amount you feel comfortable (e.g. Sonic games premultiply by 256):

for angle = 0 to MAX_ANGLE
  table[angle] = sin(angle) * premultiply
end

You don't need a separate table for cosines: cos(angle) is the same thing as sin(angle+90°), so you can use the same table for both. If you still want to have a separate cosines table or not is up to you.

Moving at an angle

Now let's learn the most common thing we'll want to do with this: moving a certain distance at an angle. We've seen before that sine and cosine are the same as moving in each axis: this gives us the amount to move by 1 unit. So multiply them by the distance you want to move and you're set:

new_x = x + (cos(angle) * distance)
new_y = y - (sin(angle) * distance)

Ideally, you'll pick powers of two so you can use bit shifts instead (which are much faster than multiply). Also, since the 68000 can't do floating point, you're likely using premultiplied values (e.g. by 256), so when you need the result as an integer remember to divide it by that amount!

E.g. if your tables are premultiplied by 256 and you want to advance by 16px:

new_x = x + (cos(angle) >> 4)
new_y = y - (sin(angle) >> 4)

That's because we multiply by 16 (<< 4) and divide by 256 (>> 8) and we merged the result of both shifts into a single shift (while also avoiding the risk of overflow). Or you could think of it as multiplying by 16/256 instead. Either works.

Computing 90° angles

In many cases, you need several angles at 90° steps. In this situation, you can reuse the same sine and cosine values so you only need to retrieve them once. Flipping both signs is the same as a 180° flip. Swapping them and flipping one sign is the same as a 90° flip.

The following table shows all the 90°-step rotations:

RotationXY
x = cos(angle)y = -sin(angle)
90°x = -sin(angle)y = -cos(angle)
180°x = -cos(angle)y = sin(angle)
270°x = sin(angle)y = cos(angle)

Rotating around a point

Another thing we may need to do is rotate a point around a certain point (think stuff like the Sonic 1 special stage). This looks more complex to do, but in the end all we're doing is advance the point twice (first at the angle we want, then again 270° away from it).

First we need to know the distance between the point we're gonna move and the point where it rotates around (i.e. the origin):

distance_x = x - origin_x
distance_y = y - origin_y

Then we advance twice. The first two lines set the starting point (which is the origin), the next two lines advance in the "X axis" of the original point and the last two in the "Y axis":

new_x = origin_x
new_y = origin_y
new_x += cos(angle) * distance_x
new_y -= sin(angle) * distance_x
new_x += sin(angle) * distance_y
new_y += cos(angle) * distance_y

Or if we put them together:

new_x = origin_x + (cos(angle)*distance_x) + (sin(angle)*distance_y)
new_y = origin_y - (sin(angle)*distance_x) + (cos(angle)*distance_y)

Get angle between two points

To determine the angle between two points, first we need their distance in each axis (without removing the sign, which we need to know which way the angle goes), we'll work off these values from here on:

dx = x2 - x1
dy = y2 - y1

Anyway, to determine the angle you need to compute the arc tangent of dy divided dx (i.e. atan(dy/dx)) and then remember to adjust the quadrant based on their signs… This is a pain, so most programming libraries instead provide an atan2 function that takes the values separately and does all that for you:

angle = atan2(dy,dx)

That returns the value in radians, so convert it to whatever you're using instead. And note that they usually return -180° to 180° instead of 0° to 360° (i.e. mind the sign). Also remember to cope with the case where dx and dy are both 0 (which has no valid angle).

Angle look-up table

The problem with the above is that the 68000 isn't good enough for this, so much like with sines, we're gonna need to resort to look-up tables. The method suggested here may not be ideal but it's easy to understand. In practice, you don't need 100% accuracy, just "close enough", and in the worst case you can correct course every few steps.

The idea here is:

The above will lose accuracy the larger the distances are, so make sure to pick a reasonable trade-off between range and table size (and for cases where long range accuracy is important, to recompute the angle every few units).

And the table in question (remember to convert from radians to whatever unit you're using for angles):

for x = MIN_X to MAX_X and y = MIN_Y to MAX_Y
   if x == 0 and y == 0
      table[x][y] = 0 or some other safe value
   else
      table[x][y] = atan2(y,x)
   end
end

Bring angle towards another

When you make something that chases after a target, you usually don't want to point to its angle directly but rather want it to have to turn around towards it. The problem is making sure it takes the shortest direction.

If you're using a power of two as a full circle, there's a neat trick here: substract the current angle from the target angle, then AND it (remove the excess bits). If the result is below 180° then you need to increment the angle, while if it's above then you need to decrement the angle.

temp = (target_angle - angle) & ANGLE_MASK

if temp = 0°
   do nothing
else if temp < 180°
   increment angle
else
   decrement angle
end

While the same concept works with non-power of two units, normalizing is a more complex process on those, which is the main reason why it's preferrable to use units based on a power of two.