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
- Sines and cosines
- Moving at an angle
- Computing 90° angles
- Rotating around a point
- Get angle between two points
- Bring angle towards another
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:
- The most common unit is degrees (a full circle is 360°)
- Another common unit is radians (a full circle is 2×π)
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:
sin(angle)is how much you move up
cos(angle)is how much you move right
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,
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:
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:
|0°||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
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:
- Determine which range we're willing to store (e.g. 32 or 64 units per axis)
- Compute all the angles for every possible X/Y value in that range.
- When computing angles at larger distances, we divide them by 2 until both fit (make sure to always divide both even if only one is out of range)
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.