views:

1545

answers:

12

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

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 degress, the gun rotates, shoots and destories it, if another target appears at 320 degress, the gun will rotate 310 degress anticlockwise instead of just rotating 60 degress clockwise to hit it.

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

+5  A: 

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
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
+14  A: 

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
This seems like best solution. :)
Arnis L.
But how do you use this to determine which way to turn?
JoshBerke
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
Even if i take the modulus, how do I know which way to turn?
Verroq
This illustrates the worst of StackOverflow's fastest-gun-in-the-west problem, where a correct-looking answer gets voted higher than correct answers. The original question was about determining which way to turn (so it would involve checking whether x is smaller or 360-x, where x is the difference of the angles mod 360), but this answer is at 11 votes and rising despite not answering the question at all.
ShreevatsaR
just vote it down then...
wds
People who don't read the question thoroughly will keep upvoting, thus fastest gun in the west problem where answer looks correct but doesn't answer anything.
yx
Richard
So you first answer with something that looks alright but isn't, getting quick upvotes, then read the correct answers, incorporate them in your post et voila, free rep. Brilliant!
wds
What is FloatEpsilon? Shouldn't you just compare if diff==0 then return Direction of None?
JoshBerke
Who cares? Listen to you chimps complaining about this... Richard gave the most helpful answer, so get over it.
Josh Stodola
How does the currentdirection and targetdirection, does the angle go in there? And what is FloatEpsilon?
Verroq
@Josh, no he did not. He has since then revised his answer to be more helpful, most likely due to the comments on his original answer. I am fine with that and have withdrawn my downvote. However, his original answer did not answer the question Verroq originally asked at all.
yx
"" -- Should that 36.0 be 360.0?
finnw
i figured that was a typo\
Verroq
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
@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
@finnw Yes, will correct.
Richard
Richard
@wds: Listen to some of the SO poscasts, the idea is that questions /and/ answers are improved by the community to stand the test of time. And that Joel specifically suggest the get a quick answer in and then revise to fill out. (In retrospect I should have noted the initial version was only a partial answer to the easy part of the question.)
Richard
@Richard: Ahh ok your missing a period there: float.Epsilon is what you meant. Thanks
JoshBerke
@Richard: I hope no offense was taken. I didn't even downvote your answer; I was only making a general observation about the nature of StackOverflow (or its voting users) to reward speed and appearances over accuracy, thus often leading to unhelpful replies at the top obscuring genuinely thoughtful answers. (Not in this case.) This system design seems suboptimal for a website that intends questions and answers to "stand the test of time", as you said.
ShreevatsaR
+3  A: 

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;
John Pirie
successfully shoots target at 10 degress, faces back of the gun to the target at 310 degress.
Verroq
@Verroq: Edited to allow for gundeg declining to < 0. Don't really have facility to test, but I think that should do it.
John Pirie
+18  A: 

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;
  if (diff < 0)
     change = 1;
  else
     change = -1;

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

  gundeg += change;
 }
Kirschstein
one slight oversight, this makes the turret get stuck on 350 something degress, and unable to hit anything from 0 to 180 degress.
Verroq
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
needs a gundeg % 360 then
wds
no...When gundeg is equal to 359 degress, objdeg is equal to 10 degressif((359-10) > 180) -> yesgundeg++ -> gundeg now equal to 360modulus - > gundeg now equal to 0if((0-10)>180)) -> NOgundeg -1 -> gundeg now equal to -1modulus -> gundeg now equal to 359rinse and repeat
Verroq
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
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
curses! back to the drawing board...
Kirschstein
Ok - attempt #2!
Kirschstein
That's better, but why leave the wrong solution at the top?
finnw
+1 for 180 degrees relation with orientation
Eric
@finnw, so all these comments make sense!
Kirschstein
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#1049214but I cant select two as accepted answers
Verroq
@Verroq: Thanks this one is more concise better answer but good to know mine works as well
JoshBerke
+1  A: 

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)

yx
A: 

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
+4  A: 

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
This also works perfect, except I cant seem to select both as accepted answers.
Verroq
A: 

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
+12  A: 

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
+1 because this is such an refreshing approach!
Marc
Premature optimisation is the root of all evil.
finnw
@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
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
Is there a reference for where this comes from? A paper thrown up by googling indicates it's from ship tactical systems.
Pete Kirkham
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
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
+3  A: 

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

Krypes
+5  A: 

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.

erichui
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
A: 

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