19

I'm making a game and in it is a computer controlled gun turret. The gun turret can rotate 360 degrees.

It uses trig to find out the angle it needs to aim the gun (objdeg) and the current angle of the gun is stored in (gundeg)

the following code rotates the gun at a set speed

if (objdeg > gundeg)
{
    gundeg++;
}
if (objdeg < gundeg)
{
    gundeg--;
}

The problem is that if there is an object at 10 degrees, the gun rotates, shoots and destroys it, if another target appears at 320 degrees, the gun will rotate 310 degrees anticlockwise instead of just rotating 60 degrees clockwise to hit it.

How can I fix my code so it won't act stupidly?

RCIX
  • 38,647
  • 50
  • 150
  • 207
Verroq
  • 210
  • 1
  • 2
  • 7
  • 7
    I'm slightly bemused by seeing 10 upvotes and 3 favourites for this one.... is there a subtly to this question that I'm missing? – skaffman Jun 26 '09 at 14:00
  • I think it's a fun problem to solve and has gotten a lot of interest, a lot of wrong answers a lot of right answers. – JoshBerke Jun 26 '09 at 14:11
  • 1
    Well Josh's solution was quite nicely done as well, only if i had two slots for accepted answers – Verroq Jun 26 '09 at 14:15

15 Answers15

25

You can avoid division (and mod) entirely if you represent your angles in something referred to as 'BAMS', which stands for Binary Angle Measurement System. The idea is that if you store your angles in an N bit integer, you use the entire range of that integer to represent the angle. That way, there's no need to worry about overflow past 360, because the natural modulo-2^N properties of your representation take care of it for you.

For example, lets say you use 8 bits. This cuts your circle into 256 possible orientations. (You may choose more bits, but 8 is convenient for the example's sake). Let 0x00 stand for 0 degrees, 0x40 means 90 degrees, 0x80 is 180 degrees, and 0xC0 is 270 degrees. Don't worry about the 'sign' bit, again, BAMS is a natural for angles. If you interpret 0xC0 as 'unsigned' and scaled to 360/256 degrees per count, your angle is (+192)(360/256) = +270; but if you interpret 0xC0 as 'signed', your angle is (-64)(360/256)= -90. Notice that -90 and +270 mean the same thing in angular terms.

If you want to apply trig functions to your BAMS angles, you can pre-compute tables. There are tricks to smallen the tables but you can see that the tables aren't all that large. To store an entire sine and cosine table of double precision values for 8-bit BAMS doesn't take more than 4K of memory, chicken feed in today's environment.

Since you mention using this in a game, you probably could get away with 8-bit or 10-bit representations. Any time you add or subtract angles, you can force the result into N bits using a logical AND operation, e.g., angle &= 0x00FF for 8 bits.

FORGOT THE BEST PART (edit)

The turn-right vs turn-left problem is easily solved in a BAMS system. Just take the difference, and make sure to only keep the N meaningful bits. Interpreting the MSB as a sign bit indicates which way you should turn. If the difference is negative, turn the opposite way by the abs() of the difference.

This ugly little C program demonstrates. Try giving it input like 20 10 and 20 30 at first. Then try to fool it by wrapping around the zero point. Give it 20 -10, it will turn left. Give it 20 350, it still turns left. Note that since it's done in 8 bits, that 181 is indistinguishable from 180, so don't be surprised if you feed it 20 201 and it turns right instead of left - in the resolution afforded by eight bits, turning left and turning right in this case are the same. Put in 20 205 and it will go the shorter way.

#include <stdio.h>
#include <math.h>

#define TOBAMS(x)    (((x)/360.0) * 256)
#define TODEGS(b)    (((b)/256.0) * 360)

int main(void)
{
    double a1, a2;     // "real" angles
    int b1, b2, b3;    // BAMS angles


    // get some input
    printf("Start Angle ? ");
    scanf("%lf", &a1);

    printf("Goal Angle ? ");
    scanf("%lf", &a2);

    b1 = TOBAMS(a1);
    b2 = TOBAMS(a2);

    // difference increases with increasing goal angle
    // difference decreases with increasing start angle
    b3 = b2 - b1;
    b3 &= 0xff;

    printf("Start at %7.2lf deg and go to %7.2lf deg\n", a1, a2);
    printf("BAMS   are 0x%02X and 0x%02X\n", b1, b2);
    printf("BAMS diff is 0x%02X\n", b3);

    // check what would be the 'sign bit' of the difference
    // negative (msb set) means turn one way, positive the other
    if( b3 & 0x80 )
    {
        // difference is negative; negate to recover the
        // DISTANCE to move, since the negative-ness just
        // indicates direction.

        // cheap 2's complement on an N-bit value:
        // invert, increment, trim
        b3 ^= -1;       // XOR -1 inverts all the bits
        b3 += 1;        // "add 1 to x" :P
        b3 &= 0xFF;     // retain only N bits

        // difference is already positive, can just use it
        printf("Turn left %lf degrees\n", TODEGS(b3));
        printf("Turn left %d counts\n", b3);
    }
    else
    {
        printf("Turn right %lf degrees\n", TODEGS(b3));
        printf("Turn right %d counts\n", b3);
    }

    return 0;
}
JustJeff
  • 12,640
  • 5
  • 49
  • 63
  • 2
    Premature optimisation is the root of all evil. – finnw Jun 26 '09 at 14:02
  • 4
    @finnw - I do not disagree about premature optimization. Yes, cutting out division may smack of optimization, but the point of BAMS isn't so much optimization, as it is to leverage the natural modulo-2^N properties of integers. – JustJeff Jun 26 '09 at 14:16
  • 1
    more on 'optimization' - actually, he did say 'game'. About half of game programming is optimizing that 20% of the code that takes 80% of the CPU. I would suspect 'aiming the gun' is going to be near that 20% =) – JustJeff Jun 26 '09 at 14:19
  • Is there a reference for where this comes from? A paper thrown up by googling indicates it's from ship tactical systems. – Pete Kirkham Jun 27 '09 at 11:42
  • sadly, no, i don't have a reference, and its curiously hard to find good info on it, probably since all the words are so generic. anyway i suppose it could be used for that. it's good for almost any kind of system that has rotating components. some optical shaft encoders, the absolute kind, basically output a BAMS value directly. also, if you interpret the number as a binary *fraction*, it easily extends to counting whole revolutions as well. – JustJeff Jun 27 '09 at 12:36
  • This stuff is excellent. I have had experience modding StarCraft and can tell you that it uses a 0-255 angling system throughout, but I never realised what a clever idea that was until I read this! – Ray Hidayat Dec 04 '09 at 02:01
  • LMAO about the ship tactical systems. Every time I Google around about game making I can feel my name getting added to the short list. Thank you for this post! – Tegra Detra Sep 29 '12 at 19:34
  • I love this approach, and used it in RRobots (fun programming tanks game). To find out more references to this approach, you can read up on "Binary Angles" https://en.wikipedia.org/wiki/Binary_scaling – Greg Edwards Aug 06 '15 at 16:13
  • This is by far the best explanation. Even better than the BAMS wiki. – newguy Apr 30 '17 at 06:13
  • @GregEdwards this answer is much better than the wiki IMHO. I think the 8 bit integer scaling (use a factor of 256) is widely used in a lot of games. And this is the only place that explains how it is done in details. – newguy Apr 30 '17 at 06:19
20

If you need to rotate more than 180 degrees in one direction to aim the turret, then it would be quicker to rotate the other direction.

I would just check for this and then rotate in the appropriate direction

if (objdeg != gundeg)
{
    if ((gundeg - objdeg) > 180)
       gundeg++;
    else
       gundeg--;
}

EDIT: New Solution

I have refined my solution based on the feedback in the comments. This determines whether the target is to the 'left or right' of the turret and decides which way to turn. It then inverts this direction if the target is more than 180 degrees away.

if (objdeg != gundeg)
{
  int change = 0;
  int diff = (gundeg - objdeg)%360;
  if (diff < 0)
     change = 1;
  else
     change = -1;

  if (Math.Abs(diff) > 180)
     change = 0 - change;

  gundeg += change;
 }
Moff Kalast
  • 1,024
  • 1
  • 12
  • 22
Kirschstein
  • 14,570
  • 14
  • 61
  • 79
  • one slight oversight, this makes the turret get stuck on 350 something degress, and unable to hit anything from 0 to 180 degress. – Verroq Jun 26 '09 at 13:24
  • This assumes objdeg and gundeg are both integers. If the two are not equal, the turret will ALWAYS move. How are your angles being calculated? – Kirschstein Jun 26 '09 at 13:31
  • no... When gundeg is equal to 359 degress, objdeg is equal to 10 degress if((359-10) > 180) -> yes gundeg++ -> gundeg now equal to 360 modulus - > gundeg now equal to 0 if((0-10)>180)) -> NO gundeg -1 -> gundeg now equal to -1 modulus -> gundeg now equal to 359 rinse and repeat – Verroq Jun 26 '09 at 13:38
  • What happens with integer objdeg=350 and gundeg=10? You decrement to 0 and then turn negative (-1 % 360 will not help). Soon, you are shooting in the other (negative) dimension and eventually rolling over backwards. I changed this answer to add in the negative delta handling. – nik Jun 26 '09 at 13:41
  • let me retype that, 1. Gundeg = 359, objdeg = 10 <><><> 2. if((359-10) > 180) -> yes <><><> 3. Gundeg = 360. <><><> 4. Modulus, gundeg = 0 <><><> 5. if(0-10)>180 -> NO <><><> 6. gundeg-=1 <><><> 7. Modulus <><><> 8. gundeg = 359 <><><> rinse and repeat – Verroq Jun 26 '09 at 13:42
  • 1
    Working solution. May I say Josh's one is also prefect, http://stackoverflow.com/questions/1048945/getting-the-computer-to-realise-360-degrees-0-degrees-rotating-a-gun-turret/1049214#1049214 but I cant select two as accepted answers – Verroq Jun 26 '09 at 14:10
  • @Verroq: Thanks this one is more concise better answer but good to know mine works as well – JoshBerke Jun 26 '09 at 14:14
  • 1
    `diff` needs to be modulo `% 360`, otherwise `diff` can reach values of *more* than 360 and cause a double-rotation. – a paid nerd Jan 17 '12 at 03:06
12

To Normalised to [0,360):

(I.e. a half open range)

Use the modulus operator to perform "get division remainder":

361 % 360

will be 1.

In C/C++/... style languages this would be

gundeg %= 360

Note (thanks to a comment): if gundeg is a floating point type you will need to either use a library function, in C/C++: fmod, or do it yourself (.NET):

double FMod(double a, double b) {
  return a - Math.floor(a / b) * b;
}

Which Way To Turn?

Which ever way is shorter (and if turn is 180°, then the answer is arbitrary), in C#, and assuming direction is measured anti-clockwise

TurnDirection WhichWayToTurn(double currentDirection, double targetDirection) {
  Debug.Assert(currentDirection >= 0.0 && currentDirection < 360.0
               && targetDirection >= 0.0 && targetDirection < 360.0);

  var diff = targetDirection - currentDirection ;
  if (Math.Abs(diff) <= FloatEpsilon) {
    return TurnDirection.None;
  } else if (diff > 0.0) {
    return TurnDirection.AntiClockwise;
  } else {
    return TurnDirection.Clockwise;
  }
}

NB. This requires testing.

Note use of assert to confirm pre-condition of normalised angles, and I use an assert because this is an internal function that should not be receiving unverified data. If this were a generally reusable function the argument check should throw an exception or return an error (depending on language).

Also note. to work out things like this there is nothing better than a pencil and paper (my initial version was wrong because I was mixing up using (-180,180] and [0,360).

Richard
  • 106,783
  • 21
  • 203
  • 265
  • 7
    But how do you use this to determine which way to turn? – JoshBerke Jun 26 '09 at 12:56
  • 1
    Note that if you're using floating point angles (as most people do) you need to use a floating point modulus. In C/C++ this is the fmod() function, or in C# it's Math.IEEERemainder(). – Ron Warholic Jun 26 '09 at 12:59
  • Even if i take the modulus, how do I know which way to turn? – Verroq Jun 26 '09 at 12:59
  • 1
    What is FloatEpsilon? Shouldn't you just compare if diff==0 then return Direction of None? – JoshBerke Jun 26 '09 at 13:43
  • How does the currentdirection and targetdirection, does the angle go in there? And what is FloatEpsilon? – Verroq Jun 26 '09 at 13:47
  • "&& targetDirection >= 0.0 && targetDirection < 36.0);" -- Should that 36.0 be 360.0? – finnw Jun 26 '09 at 13:53
  • I still don't think this works. I tried to plug it in, and it does it work some cases but not most, if starting point is 0 and item is at 10, then diff is >0 so it will go counter clockwise, and end up at 359 – JoshBerke Jun 26 '09 at 14:01
  • @Josh: target = 10, current = 0, then target-current = 10, which is >10 so move anti-clockwise which is correct given angels are measure by anti-clockwise rotation from the positive x axis (this is the assumption). However if you are measuring angles in a clockwise direction, (e.g. if 0 is 12-o'clock then 10 is halfway between 12 and 1-o'clock) then you need to reverse the return values. – Richard Jun 26 '09 at 14:55
  • @Josh & @Verroq: FloatEpsilon is the smallest difference that is significant in floating numbers. Comparing floating point for equality will give false negatives due to the inherent rounding and errors in the least significant bits. – Richard Jun 26 '09 at 14:59
  • @Richard: Ahh ok your missing a period there: float.Epsilon is what you meant. Thanks – JoshBerke Jun 26 '09 at 17:48
10

I tend to favor a solution that

  • does not have lots of nested if statements
  • does not assume that either of the two angles are in a particular range, e.g. [0, 360] or [-180, 180]
  • has a constant execution time

The cross product solution proposed by Krypes meets this criteria, however it is necessary to generate the vectors from the angles first. I believe that JustJeff's BAMS technique also satisfies this criteria. I'll offer another ...

As discussed on Why is modulus different in different programming languages? which refers to the excellent Wikipedia Article, there are many ways to perform the modulo operation. Common implementations round the quotient towards zero or negative infinity.

If however, you round to the nearest integer:

double ModNearestInt(double a, double b) {
    return a - b * round(a / b);
}

The has the nice property that the remainder returned is

  • always in the interval [-b/2, +b/2]
  • always the shortest distance to zero

So,

double angleToTarget = ModNearestInt(objdeg - gundeg, 360.0);

will be the smallest angle between objdeg and gundeg and the sign will indicate the direction.

Note that (C#) Math.IEEERemainder(objdeg - gundeg, 360.0) or (C++) fmod(objdeg - gundeg, 360.0) does that for you already, i.e. ModNearestInt already exists in the associated math libraries.

Ian Macintosh
  • 369
  • 4
  • 13
erichui
  • 2,761
  • 2
  • 20
  • 20
  • that looked so cool i ran it through the ol' compiler and yes, it works! Also wanted to say, nice list of qualities there, glad to see that not everyone is crazy for nests of if-else! =) – JustJeff Jun 27 '09 at 13:00
6

Just compare the following:

gundeg - objdeg
objdeg - gundeg 
gundeg - objdeg + 360
objdeg - gundeg + 360

and choose the one with minimum absolute value.

J-16 SDiZ
  • 26,473
  • 4
  • 65
  • 84
  • 1
    do it between the first and third in your list. then for that number check it's sign - that would be the direction to turn – yairchu Jun 26 '09 at 15:00
5

Here's a workign C# sample, this will turn the right way. :

public class Rotater
{
    int _position;
    public Rotater()
    {

    }
    public int Position
    {
        get
        {
            return _position;
        }
        set            
        {
            if (value < 0)
            {
                _position = 360 + value;
            }
            else
            {
                _position = value;
            }
            _position %= 360;
        }
    }
    public bool RotateTowardsEx(int item)
    {
        if (item > Position)
        {
            if (item - Position < 180)
            {
                Position++;
            }
            else
            {
                Position--;
            }
            return false;
        }
        else if (Position > item)
        {
            if (Position - item < 180)
            {
                Position--;
            }
            else
            {
                Position++;
            }
            return false;
        }
        else
        {
            return true;
        }
    }
}

    static void Main(string[] args)
    {


        do
        {
            Rotater rot = new Rotater();
            Console.Write("Enter Starting Point: ");
            var startingPoint = int.Parse(Console.ReadLine());
            rot.Position = startingPoint;
            int turns = 0;

            Console.Write("Enter Item Point: ");
            var item = int.Parse(Console.ReadLine());
            while (!rot.RotateTowardsEx(item))
            {
                turns++;
            }
            Console.WriteLine(string.Format("{0} turns to go from {1} to {2}", turns, startingPoint, item));
        } while (Console.ReadLine() != "q");


    }

Credit to John Pirie for inspiration

Edit: I wasn't happy with my Position setter, so I cleaned it up

JoshBerke
  • 66,142
  • 25
  • 126
  • 164
3

You need to decide whether to rotate left or right, based on which is the shorter distance. Then you'll need to take modulus:

if (objdeg > gundeg)
{
    if (objdeg - gundeg < 180)
    {
        gundeg++;
    }
    else
    {
        gundeg--;
    }
}
if (objdeg < gundeg)
{
    if (gundeg - objdeg < 180)
    {
        gundeg--;
    }
    else
    {
        gundeg++;
    }
}
if (gundeg < 0)
{
    gundeg += 360;
}
gundeg = gundeg % 360;
  • successfully shoots target at 10 degress, faces back of the gun to the target at 310 degress. – Verroq Jun 26 '09 at 13:33
  • @Verroq: Edited to allow for gundeg declining to < 0. Don't really have facility to test, but I think that should do it. –  Jun 26 '09 at 14:00
3

Actually, theres an easier way to approach this problem. Cross product of two vectors gives you a vector representing the normal (eg. perpendicular). As an artifact of this, given two vectors a, b, which lie on the xy-plane, a x b = c implies c = (0,0, +-1).

Sign of the z component of c (eg. whether it comes out of, or goes into the xy- plane) depends on whether its a left or right turn around z axis for a to be equal to b.

Vector3d turret
Vector3d enemy

if turret.equals(enemy) return;
Vector3d normal = turret.Cross(enemy);
gundeg += normal.z > 0 ? 1 : -1; // counter clockwise = +ve
John
  • 5,735
  • 3
  • 46
  • 62
Krypes
  • 561
  • 2
  • 10
1

Try dividing by 180 using integer division and turning based on even/odd outcome?

749/180 = 4 So you turn clockwise by 29 degrees (749%180)

719/180 = 3 So you turn counterclockwise by 1 degree (180 - 719%180)

z -
  • 7,130
  • 3
  • 40
  • 68
0

The problem is about finding the direction that will give the shortest distance.

However, subtraction can result in negative numbers and that needs to be accounted for.
If you are moving the gun one step at each check, I don't know when you will do the modulus.
And, if you want to move the gun in one step, you would just add/subtract the delta correctly.

To this end Kirschstein seems to be thinking nearest to me.
I am working with an integer in this simple psudo-code.

if (objdeg != gundeg)
{
    // we still need to move the gun
    delta = gundeg - objdeg
    if (delta > 0)
        if (unsigned(delta) > 180)
           gundeg++;
        else
           gundeg--;
    else // delta < 0
        if (unsigned(delta) > 180)
           gundeg--;        
        else
           gundeg++;

    if (gundeg == 360)
        gundeg = 0;
    else if (gundeg == -1)
        gundeg = 359;
}

Try to work this incrementally with gundeg=10 and objdeg=350 to see how the gundeg will be moved from 10 down to 0 and then 359 down to 350.

nik
  • 13,254
  • 3
  • 41
  • 57
0

Here's how I implemented something similar in a game recently:

double gundeg;

// ...

double normalizeAngle(double angle)
{
    while (angle >= 180.0)
    {
        angle -= 360.0;
    }
    while (angle < -180.0)
    {
       angle += 360.0;
    }
    return angle;
}

double aimAt(double objAngle)
{
    double difference = normalizeAngle(objdeg - gundeg);
    gundeg = normalizeAngle(gundeg + signum(difference));
}

All angle variables are restricted to -180..+180, which makes this kind of calculation easier.

finnw
  • 47,861
  • 24
  • 143
  • 221
0

At the risk of bikeshedding, storing degrees as an integer rather than as its own class might be a case of "primitive obsession". If I recall correctly, the book "The pragmatic programmer" suggested creating a class for storing degrees and doing operations on them.

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
0

This might be a bit late... Probably very late... But I recently had a similar issue and found that this worked just fine in GML.

var diff = angle_difference(gundeg, objdeg)

if (sign(diff)>0){
   gundeg --;
}else{
   gundeg ++;
}
0

I had a similar problem in python. I have a current rotation in degrees and a target rotation in degrees. The two rotations could be arbitrarily big so I had three goals with my function:

  1. Keep both angles small
  2. Keep the difference between the angles <= 180°
  3. The returned angles must be equivalent to the input angles

I came up with the following:

def rotation_improver(c,t):
    """
    c is current rotation, t is target rotation. \n
    returns two values that are equivalent to c and t but have values between -360 and 360
    """
    ci = c%360
    if ci > 180:
        ci -= 360
    ti = t%360
    if not abs(ci-ti) <= 180:
        ti -= 360
    return ci,ti

It should run flawlessly in c++ with a few syntax changes. The return values of this general solution can then easily be used to solve any specific problem like using subtraction to get the relative rotation.

I know that this question is very old and has sufficient specific answers but I hope that someone with a similar problem stumbling through the internet can draw inspiration from from my general solution.

Astus
  • 1
  • 2
0

Here's the short-test pseudo code sample I can think of that answers the problem. It works in your domain of positive angles 0..359 and it handles the edge conditions first prior to handling the 'normal' ones.

if (objdeg >= 180 and gundeg < 180)
    gundeg = (gundeg + 359) % 360;
else if (objdeg < 180 and gundeg >= 180)
    gundeg = (gundeg + 1) % 360;
else if (objdeg > gundeg)
    gundeg = (gundeg + 1) % 360;
else if (objdeg < gundeg)
    gundeg = (gundeg + 359) % 360;
else
    shootitnow();
Stephen Quan
  • 21,481
  • 4
  • 88
  • 75