This post is about making a simple problem actually simple: dealing with angles in 2d. As part of writing this post, I implemented a library for working with angles. The source code is available on github, or you can reference the nuget package with visual studio’s package manager. Lets start with a motivating problem. You have a tank, it has a turret with a restricted range of motion, and you want the tank and turret to rotate towards the mouse pointer. Like this: This is not a complicated task, but the angle math makes it tricky. If you approach it in the wrong way then you will end up with code containing a lot of cases, meaning lots of opportunities for sign errors and off-by-one errors. As a result, you’re likely to see bugs isolated to one quadrant or bugs on the transitions between quadrants (particularly -1° to +1° and +179° to +181°). What is it that makes angle code tricky to write? I can think of three good reasons. Math Trivia: A thing where you distinguish between absolute position and relative deltas is called an Affine Space. Another example of an affine space is the combination of points and vectors. Modular arithmetic is the arithmetic of ‘looping back around numbers’ like angles. Unfortunately, although there’s lots of online resources about integer modular arithmetic, there’s doesn’t seem to be much for real/continuous modular arithmetic. A proper angle library needs to make it natural to solve common tasks correctly. Examples of common tasks are: To make solving these tasks easy, my approach is to divide the space into four types of thing: Basis, Dir, Turn, and Range. A “Basis” is a system of angles, defining how to convert from raw angle values into encapsulated directions/rotations (and back). For example, the natural angle basis pairs the angle value 0 with the rightward direction and the angle value pi/2 with the upward direction. The natural basis is very common, so it makes sense to have it included by default and even to have specialized methods that use it. The Basis type is also a nice place to define constants like the number of radians/degrees/gradians per rotation. Note that users sometimes want signed angles [-180° to +180°) and sometimes want unsigned angles [0° to 360°), so it’s important to support both cases. A “Dir” is a direction, equivalent to a line starting at the origin and passing through a point on the unit circle. Directions have both exact and approximate equality that respects that 1° = 361° and that 359.999° is close to 0.001°. Being able to convert to and from X/Y vector components allows work independent of any angle basis. Arithmetic operators are also useful: adding/subtracting a turn from a direction rotates the direction, and taking the difference between to directions gives the turn that rotates between them. The Dir type is a great place to define constants for the cardinal directions. A “Turn” is a non-normalized rotation. The lack of normalization (meaning -180° != +180° != +540°) is a trade-off to allow intuitive comparisons and scaling (normalizing would create ambiguities in those cases, as I mentioned earlier). Instead of automatic normalization, turns have manual normalization as well as specialized method that nicely handle “congruence” (a turn of +15° is congruent to a turn of -345°). Another trade-off made by Turn is encapsulation of the sign of clockwise-ness. Some systems of angles pick counter-clockwise to be the “positive” direction and some pick clockwise, so that information should be part of the angle basis and not redundantly specified in Turn. As a consequence, the user must specify which direction is positive in order to compare turns. Turn arithmetic is almost identical to real arithmetic: summing, scaling, and dividing are all allowed and behave nicely thanks to the lack of automatic normalization. The Turn type is the place for common rotation constants (1°, 1 radian, 360°). A “Range” is a contiguous set of directions. The Range type is probably the hardest to implement correctly (see the ‘example awful code’ at the end of the post). That also makes it the most valuable to users. Providing several safe ways to create it, and implementing the all-important clamp method, basically solves the toy problem I opened with. The Range type is a good place to define constants for the four quadrants and four half-planes. With all of these types implemented, creating a tank with a constraint turret is easy. I spent significantly more time tweaking the art positioning and movement constants than this code: The example project in the source code on github includes the above code. Angles can be tricky to work with, but we can reduce the problem by encapsulating them into a library. Also, you can make an adequate animated gif by writing an example program, recording it with CamStudio, editing the video with Windows Movie Maker, uploading the video to benderconverter.com, cropping the result with Gimp, then uploading the result to imgur. I wouldn’t recommend this process, though. What does it look like when a programmer who doesn’t really know what they’re doing tries to implement angle math? Well, when I started programming (14 years ago) I had only QBasic’s built-in documentation to guide me. I didn’t know about variables, loops, arrays, indentation. Nothing. In the process of teaching myself, I naturally wrote some truly bad code. Now I get to put that bad code to use as an example of what not to do when trying to clamp an angle: As you can see: lots of cases, lots of constants, and lots of fiddly bits. I was probably “adjusting” it constantly, because it’s a breeding ground for bugs. There’s even still some serious ones in there! For starters, I see a mistakenly semi-bitwise comparison, a potentially infinite loop, and a stupid typo. — Embarrassing… Although, not quite as brain-dead as this code, from the seems-fine-otherwise HydraMF package, that I came across when looking through other math libraries on NuGet: I’m guessing these two particular methods have never been used. They pass degrees to a function expecting radians, unnecessarily round that input, and then confusingly transform the output to be from -0.001 to +0.001 instead of -1 to +1. Why? Who knows. — —
Encapsulating Angles
Problems and Obstacles
Encapsulating the Problems
Example Usage
// --- move the tank ---
// measurements
var dx = this._lastMousePosition.X - tankPos.X;
var dy = this._lastMousePosition.Y - tankPos.Y;
var dist = Math.Sqrt(dx * dx + dy * dy);
var dirTowardsMouseFromTank = Dir.FromVector(dx, dy);
// rotate tank towards target
tankDir +=
// determine the turn necessary to rotate the tank to face the target
(dirTowardsMouseFromTank - tankDir)
// force the turn to be within the allowed tank rotation rate
.ClampMagnitude(MaxTankTurnPerSec * dt.TotalSeconds);
// rotate turret towards target
turretTurn +=
// determine the amount of turning necessary to rotate the turret to face the target
(dirTowardsMouseFromTank - tankDir - turretTurn)
// force the turn to be within the allowed turret rotation rate
.ClampMagnitude(MaxTurretTurnPerSec * dt.TotalSeconds);
// force turret to stay within its allowed turning radius
turretTurn = turretTurn.ClampMagnitude(MaxTurretTurn);
Summary
Bonus: Example Awful Code
//[Hindsight: this is an inlined atan2 function]
Horizontal = ABS(StartX - DestX)
Vertical = ABS(StartY - DestY)
IF Horizontal AND Vertical <> 0 THEN
Diagonal = SQR((Horizontal ^ 2) + (Vertical ^ 2))
Angle = (Arcsin(Vertical / Diagonal))
IF DestX > StartX AND DestY > StartY THEN Angle = 0 - Angle: Angle = Angle + 90
IF DestX > StartX AND DestY < StartY THEN Angle = Angle + 90
IF DestX < StartX AND DestY > StartY THEN Angle = Angle + 270
IF DestX < StartX AND DestY < StartY THEN Angle = 180 - Angle: Angle = Angle + 90
ELSE
IF Horizontal = 0 AND DestY > StartY THEN Vector = 0
IF Horizontal = 0 AND DestY < StartY THEN Vector = 180
IF Vertical = 0 AND DestX > StartX THEN Vector = 90
IF Vertical = 0 AND DestX < StartX THEN Vector = 270
IF Vertical = 0 AND Horizontal = 0 THEN Vector = INT(RND * 360)
END IF
//[Hindsight: an awkward modulus, forcing the remainder to be in -180 to +180]
DO UNTIL Angle + 180 > Star AND Angle - 180 < Start
IF Angle + 180 < Start THEN Angle = Angle + 360
IF Angle - 180 > Start THEN Angle = Angle - 360
LOOP
//[Hindsight: clamping the angle towards the start]'
IF Angle > Start + Limit THEN Angle = Start + Limit
IF Angle < Start - Limit THEN Angle = Start - Limit
//[Hindsight: another awkward modulus, forcing the remainder to be in 0 to 360]
DO UNTIL Angle <= 360 AND Angle >= 0
IF Angle < 0 THEN Angle = Angle + 360
IF Angle > 360 THEN Angle = Angle - 360
LOOP
public static float Sin(AngleSingle angle) {
return (float)Math.Sin((int)angle.Degrees) / 1000f;
}
public static float Cos(AngleSingle angle) {
return (float)Math.Cos((int)angle.Degrees) / 1000f;
}
Discuss on Reddit.
Twisted Oak Studios offers consulting and development on high-tech interactive projects. Check out our portfolio, or Give us a shout if you have anything you think some really rad engineers should help you with.
Archive