Search Unity

Calculating a projectile's CLOSEST angle to hitting a target it can't reach?

Discussion in '2D' started by SullyTheStrange, Sep 30, 2016.

  1. SullyTheStrange

    SullyTheStrange

    Joined:
    May 17, 2013
    Posts:
    147
    The question of calculating a projectile's angle to hit a target has been answered countless times, but one thing I can't find an answer for is how to find the CLOSEST angle in case the projectile's initial velocity is insufficient for actually hitting the target. Most of the solutions I've seen just return 0 if it can't find an answer and call it a day.

    This is what we've been using for our game below. It works like most regular solutions do, except if it sees that both the plus and minus solution are both NaN, it re-calculates it with a slightly higher velocity and tries again until it finds a solution. It more or less works, but it produces pretty jittery behavior on the character's arm while aiming. So I'm trying to find something that DIRECTLY finds the closest angle, instead of our weird solution which finds a close one through trial and error.

    Code (CSharp):
    1.  
    2.    
    3.     //returns the angle to aim from a to b at the force of v and brought down by gravity g
    4.     public static float GetAimAngle(Vector2 a, Vector2 b, float v, float g) {
    5.         Vector2 c = b - a;
    6.         return GetAimAngle(c, v, g);
    7.     }
    8.    
    9.     public static float GetAimAngle(Vector2 dir, float v, float g) {
    10.  
    11.         float plus = Mathf.Atan((Mathf.Pow(v,2) + Mathf.Sqrt(Mathf.Pow(v,4) - (g*((g * Mathf.Pow(dir.x,2))+ (2*dir.y*Mathf.Pow(v,2)))))) / (g*dir.x));
    12.         float minus = Mathf.Atan((Mathf.Pow(v,2) - Mathf.Sqrt(Mathf.Pow(v,4) - (g*((g * Mathf.Pow(dir.x,2))+ (2*dir.y*Mathf.Pow(v,2)))))) / (g*dir.x));
    13.        
    14.         float angle = minus;
    15.        
    16.         if(float.IsNaN(minus))
    17.         {
    18.             angle = plus;
    19.             if(float.IsNaN(plus))
    20.             {
    21.                 angle = 0f;              
    22.                 angle = GetAimAngle(dir, v+5, g);
    23.             }
    24.         }
    25.         else
    26.         {
    27.             angle = angle * Mathf.Rad2Deg;
    28.             if (x < 0)
    29.                 angle = angle + 180f;
    30.         }      
    31.         return angle;
    32.     }
    Any ideas?
     
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I haven't worked through the math, but those NaNs are coming from Atan... often, Atan2 can be used instead to avoid singularities (usually division by zero) that occurs when using straight-up Atan. Could there be some way to rewrite your plus and minus formulas using Atan2?
     
  3. SullyTheStrange

    SullyTheStrange

    Joined:
    May 17, 2013
    Posts:
    147
    As I understand it, the NaN is unavoidable... This is the equation for finding the angle of trajectory with a given initial velocity. But because the initial velocity is insufficient to reach the target, there simply is no real solution -- not due to dividing by zero, but to taking the square root of a negative number. I could be wrong on all that, but, well, that's why I'm asking. :rolleyes: If there's a way to rewrite it, I don't know of it... (plus, I also just don't know how to properly use Atan2)



    EDIT: After taking a closer look at Atan2, I think the proper way to rewrite it would be this (I also got rid of the Mathf.Pow uses, which I've just read are really inefficient), but I get the same result:

    Code (CSharp):
    1. float plus = Mathf.Atan2(((v*v) + Mathf.Sqrt((v*v*v*v) - (g*((g * (dir.x*dir.x))+ (2*dir.y*(v*v)))))), (g*dir.x));
    2. float minus = Mathf.Atan2(((v*v) - Mathf.Sqrt((v*v*v*v) - (g*((g * (dir.x*dir.x))+ (2*dir.y*(v*v)))))), (g*dir.x));
    Instead of avoiding the NaN, I think what I'm really looking for is a separate equation to handle the case where there is no solution.
     
    Last edited: Sep 30, 2016
  4. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK, well, do I understand correctly that you're firing a projectile at the given angle, attempting to hit a target? But you want to handle the case where the target is too far away to hit at all?

    In that case, I would think you want to just fire your projectile as far as possible, which means firing at 45 degrees.
     
  5. SullyTheStrange

    SullyTheStrange

    Joined:
    May 17, 2013
    Posts:
    147
    That's correct. Unfortunately I can't just fall back on 45 degrees, because it's used for the player's aiming also, and the player expects it to go in the direction they're aiming for even if it falls short of the target. My goal isn't to launch the projectile as far as possible, but as close to the target as possible, which 45 degrees won't satisfy in most cases.

    I did at least find an okay alternative, which is still trial and error like my original idea but much less jittery. Instead of just adding 5 to the velocity and trying again like I originally did...

    Code (CSharp):
    1. angle = GetAimAngle(dir, v+5, g);
    ... I took the original target, moved it slightly towards the start point, and then tried again:

    Code (CSharp):
    1. angle = GetAimAngle(a, Vector2.MoveTowards(b, a, 1), v, g);
    Still trial and error, still definitely inefficient, but less jittery than before.

    I'm working on another solution which should result in WAY fewer GetAimAngle calls though, I'll post that too if it works out and if no one can figure out a better way before then.
     
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I still don't get it... eventually (with the approach above), the target will be at your maximum range, and this aim angle should return 45 degrees. No?
     
  7. SullyTheStrange

    SullyTheStrange

    Joined:
    May 17, 2013
    Posts:
    147
    The target can be anywhere on screen, including straight up above the player, below the player, behind the player... 45 degrees is only the best solution of the target is in front of the player at the same height, no? If it's straight above you, for example, you'll want to throw at 90 degrees.
     
  8. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Ah! That was the key bit I was missing. I was picturing some sort of cannon game, where the cannon and the target are at the same Y, but varying in X.

    OK, thanks for your patience. I think I get it now!
     
  9. SullyTheStrange

    SullyTheStrange

    Joined:
    May 17, 2013
    Posts:
    147
    Haha, no worries.

    So I got this new version working pretty great, virtually no jittering AND more efficient. I'll go ahead and explain it just in case someone ever finds this handy, since a pet peeve of mine is finding answers I can't understand. :p

    Code (CSharp):
    1. // Stores info in between attempts of calculating closest angle
    2.     Vector2 lowGuess;
    3.     Vector2 highGuess;
    4.     Vector2 currentGuess;
    5.     int attemptLimit = 15; // Number of times GetAimAngle can call itself before choosing an angle. 10-15 seems to be enough for a very accurate guess, in my case
    6.     // Declared outside function to be reused
    7.     float plus;
    8.     float minus;
    9.     float angle;
    10.     Vector2 dir;
    11.  
    12.     // This is called to start the process
    13.     // Returns the angle to aim from a to b at the force of v and brought down by gravity g
    14.     public float GetAimAngle(Vector2 a, Vector2 b, float v, float g) {
    15.         return GetAimAngle(a, b, v, g, 0);
    16.     }
    17.  
    18.     public float GetAimAngle(Vector2 a, Vector2 b, float v, float g, int attempts) {
    19.         //Debug.Log("Get aim angle: "+a+", "+b+", "+v+", "+g+", "+attempts+", "+Time.time);
    20.  
    21.         dir = (b-a);
    22.  
    23.         // Calculate both outcomes of formula for finding the angle of the projectile to hit the target. If NaN, that means the projectile has insufficient velocity to reach the target
    24.         plus = Mathf.Atan2(((v*v) + Mathf.Sqrt((v*v*v*v) - (g*((g * (dir.x*dir.x))+ (2*dir.y*(v*v)))))), (g*dir.x));
    25.         minus = Mathf.Atan2(((v*v) - Mathf.Sqrt((v*v*v*v) - (g*((g * (dir.x*dir.x))+ (2*dir.y*(v*v)))))), (g*dir.x));
    26.      
    27.         angle = 0;
    28.      
    29.         // If the minus outcome is NaN, check plus
    30.         if(float.IsNaN(minus))
    31.         {
    32.             // If plus outcome is NaN, start a-guessin'
    33.             if(float.IsNaN(plus))
    34.             {
    35.                 if (attempts == 0) {
    36.                     lowGuess = a;
    37.                     highGuess = b;
    38.                     currentGuess = b;
    39.                 } else {
    40.                     highGuess = currentGuess;
    41.                 }
    42.                 currentGuess = Vector2.MoveTowards(currentGuess, lowGuess, Vector2.Distance(currentGuess, lowGuess)*0.5f);
    43.                 angle = GetAimAngle(a, currentGuess, v, g, (attempts+1));
    44.             // If plus is NOT NaN
    45.             } else {
    46.                 // If this is the first try or if the attempt limit has been reached, pick this angle
    47.                 if (attempts == 0 || attempts >= attemptLimit) {
    48.                     angle = plus;
    49.                 // Otherwise, keep a-guessin'
    50.                 } else {                  
    51.                     lowGuess = currentGuess;
    52.                     currentGuess = Vector2.MoveTowards(currentGuess, highGuess, Vector2.Distance(currentGuess, highGuess)*0.5f);
    53.                     angle = GetAimAngle(a, currentGuess, v, g, (attempts+1));
    54.                 }
    55.             }
    56.         }
    57.         // If minus is NOT NaN
    58.         else
    59.         {
    60.             // If this is the first try or if the attempt limit has been reached, pick this angle
    61.             if (attempts == 0 || attempts >= attemptLimit) {
    62.                 angle = minus;
    63.             // Otherwise, keep a-guessin'
    64.             } else {  
    65.                 lowGuess = currentGuess;              
    66.                 currentGuess = Vector2.MoveTowards(currentGuess, highGuess, Vector2.Distance(currentGuess, highGuess)*0.5f);
    67.                 angle = GetAimAngle(a, currentGuess, v, g, (attempts+1));
    68.             }
    69.         }
    70.  
    71.         // Convert from radians to degrees at the end
    72.         if (attempts == 0) {
    73.             angle = angle * Mathf.Rad2Deg;
    74.         }
    75.      
    76.         return angle;
    77.     }
    To explain the problem a little better, we know where the player is, we know where the target is, and we know the speed of the projectile, but we don't know the best angle to launch it at, or how close it will get to the target, since that depends on the angle.

    What this method does is instead of just changing one of the factors (either velocity or target position, like I tried before) bit by bit and testing it out in small steps, it quickly converges on the best angle through checking halfway points. Imagine the player is at (0,0) and the target is at (100,0), but we don't know how close the projectile can get to the target (let's say it can reach (60,0), but we don't know that yet). First, we check (100,0) with the formula to find an angle, and get NaN, meaning it won't reach. Then we take the halfway point of 0 and 100: (50,0). We see that that DOES reach. Instead of settling for that, we then take the halfway point of 50 and 100, (75,0), and see that it does NOT reach, since the secret answer is (60,0). Then halfway between 50 and 75, (62.5,0), also doesn't reach. Halfway between 50 and 62.5, (56.25,0) does reach, and so on until you arbitrarily decide to stop and use what you have as the answer. For my use case it seems 10 to 15 attempts will give a very accurate answer, so I limit it at 15.

    And... it works, and stuff. Yeah. :D It's not perfect but unless anyone's got anything better, it gets the job done.
     
    JoeStrout likes this.