Search code examples
algorithmtrigonometrygame-physics

Homing missile trouble with Math.atan2


I'm working on a homing missile for a horizontal-scrolling, space-shooter game in android. I'm having trouble getting the desired behavior from the algorithm I'm using. I would like for the missile to shoot out horizontally from the player's ship and then gradually home in on the target, with a chance to miss the target if the arc is too great. It works, except if the missile misses the target, in which case, it generally attempts to follow the path of a sine wave, until it runs off the right side of the screen. What I would like, is for the missile to attempt to keep circling around the target in its current curve (like the stereotypical homing missile) until it hits a sprite or runs off the edge of the screen. I may add a limit so that it will explode and not keep spinning around on the screen, but that would be a later addition if I get this working.

Here's an example of the code:

public class HomingMissile extends Shot
{
    Bitmap bitmap;
    Rect sourceRect;
    Rect destRect;

    private double heading;

    private Sprite target;

    private int frameNumber;
    private int currentFrame;
    private int frameDelta;

    public HomingMissile(Context context, ImageLoader imageLoader, int x,
        int y, int minX, int minY, int maxX, int maxY, int dx, int dy)
    {
        super(context, imageLoader, x, y, minX, minY, maxX, maxY, dx, dy);

        heading = 0;

        frameNumber = 3;
        currentFrame = 0;
        frameDelta = 1;

        target = new Sprite(context, imageLoader, 300, 50, minX, minY, 
            maxX, maxY, 0, 0);
    }

    @Override
    public void setBitmap(int id)
    {
        bitmap = imageLoader.GetBitmap(id);

        width = bitmap.getWidth();
        height = bitmap.getHeight() / frameNumber;

        sourceRect = new Rect(0, 0, width - 1, height);
        destRect = new Rect(X, Y, X + width - 1, Y + height - 1);
    }

    public void setTarget(Sprite sprite)
    {
        target = sprite;
    }

    @Override
    public void Move()
    {
        if (!visible)
            return;

        final double f = 0.03;
        double oldHeading = heading;

        double atanY = target.Y + (target.height / 2) - Y;
        double atanX = target.Y + target.X - X;

        heading = (1 - f) * oldHeading + f * Math.atan2(atanY, atanX);

        X += Math.cos(heading) * 10;
        Y += Math.sin(heading) * 10;

        UpdateBounds();

        if (currentFrame == frameNumber - 1)
            frameDelta = -frameDelta;

        if (currentFrame < 0)
        {
            frameDelta = -frameDelta;
            currentFrame += frameDelta;
        }

        sourceRect.top = height * currentFrame;
        sourceRect.bottom = sourceRect.top + height;

        currentFrame += frameDelta;

        if (target.Collide(destRect, bitmap))
        {
            visible = false;

            heading = 0;
        }

        if (OutOfBounds())
        {
            visible = false;

            heading = 0;
        }
    }

    @Override
    public void Draw(Canvas canvas)
    {
        if (visible)
        {
            canvas.save();
            canvas.rotate((float) (heading * 180 / Math.PI) * 1.5f, X + width
                / 2, Y + height / 2);
            canvas.drawBitmap(bitmap, sourceRect, destRect, paint);
                canvas.restore();
        }
    }
}

The homing algorithm occurs in Move(). A partial solution I found to the problem was to add this check:

if (atanY >= 0 && atanX < 0)
    atanY = -atanY;

before the heading calculation, but the missile still bugs out if it is fired from a Y position greater than the target's Y position.

I've been battling with this for several days now, and I'm not very good with trig, so I'm hoping someone can help me. I tried not to clutter up the question with code, but if anymore code or information is needed, I can provide it.

Thanks!

EDIT

I changed the line:

double atanX = target.Y + target.X - X;

to:

double atanX = target.X - X;

but, if the missile is fired from a Y position greater than the target's Y position, it still bugs out. It dives toward the target, but if it misses, it abruptly curves up as if it were going to do a loop-de-loop.


Solution

  • The problem is probably due to crossing the 360 degrees / 0 degrees boundary.

    Using h = heading i.e. the direction the missile is travelling in, and t = the direction from the missile to the target and f=0.1. Now consider the situation where t is 20 degrees clockwise to h.

    If h = 180 then t = 200 and h(final) = 0.9*180+0.1*200 = 182, so the missile has turned a small amount clockwise as expected.

    But if h = 350 then t = 370 = 10 (according to the atan formula), so now h(final) = 0.9*350+0.1*10=316 and the missile has turned a large distance in the wrong direction.

    You need to add a little logic to check for the zero crossing and avoid this problem.

    Added comment

    OK - atan is always a tricky thing, as b = tan(a) is valid for any a in the range (-inf < a < + inf), but a = atan(b) will always provide a b in some range (bmin <= b < bmin + 360), so as a vector rotates around in a circle, at some point there will be a discontinuity in the calculated angle as it rises past bmin + 360 and immediately drops to bmin or as it falls below bmin and suddenly jumps up to bmin + 360. If I am not making myself clear, then just consider what happens when a clock goes past 12:59 pm. It doesn't continue on to 13:00 (except in Europe), but instead drops back to 1:00 am.

    Blackbear's answer shows the correct idea. My equivalent 'fix' would be to replace your line

      heading = (1 - f) * oldHeading + f * Math.atan2(atanY, atanX);
    

    with something like

      TheAngle = Math.atan2(atanY, atanX);
      if Abs(TheAngle-oldheading) > 180 then
        TheAngle = TheAngle - Sign(TheAngle)*360;
      heading = (1 - f) * oldHeading + f * TheAngle;
    

    where Sign(x) = +1 for x >=0 and -1 for x < 0

    Note that my code is not quite in C++, Java, or whatever you are using, so you won't be able to do a direct cut-and-paste - but you should be able to translate it into something useful.