Arc Projectiles: Computing Angles
Given a target
In this post, I'll go over the step-by-step process in attempting to solve this problem. I'll focus mainly on math, but since I initially encountered this problem while working on a video game, I will include some code at the end!
Some basic experience with physics and calculus is recommended for this post.
Derivation
Getting to a solution was much harder than I expected, and involved lots of failed attempts. In the following sections, I will highlight the main parts of the solution, including some of the things I tried that didn't work. If you just want the solution, skip ahead to Final Steps.
Kinematics
My first instinct was to use kinematics to solve for the launch angle.
I'm going to assume air resistance is negligible, so once the projectile is
launched, it will have an acceleration of
We have two unknowns and two equations, so we should be good to go to solve it!
I used the second equation to express
Plugging
Hmm. The equation we land on doesn't seem to have an algebraic solution for
Approximation
In order to perform numeric approximation, we're first going to make a single-variable function:
Thus, when
As
f resulted in this:
With this, I wrote up a quick Python script to approximate
The red line is the graph of my
Secant Method
So, I attempted a different method of approximation, called the secant method. It's very similar to Newton's method, but does not rely on knowing the derivative of the function.
After typing up an implementation in Python, I started to get correct angles!
Issues
But there's a huge problem with approximation that I ignored at the beginning:
since we're working with a trigonometric function, there are 2 distinct roots of
All numeric approximation methods take an initial guess. Depending on the guess, I'd get one of the solutions on the graph, and have absolutely no information about where the other solutions are.2
Because of this, numeric approximation wasn't going to cut it.
Substitution
Let's take a look at our original equation again:
The reason this is algebraically unsolvable stems from the
It's possible to express
Using this, we have:
Let
And suddenly, we're left with a dead-simple quadratic equation for
Final Steps
For good measure, here's the final expression for
We also have a handy physical meaning for the discriminant: if there are no solutions (i.e. the discriminant < 0), then our target is too far for our initial speed.
Wrap-Up
In this post, I walked through my trial and error process for deriving a solution to a classic physics problem. Although it took me a while, it was fun to figure it out on my own.
I hope you learned something from this post! Below, I put two code implementations: one with Python, and one with GDScript.3
Code
Python
import math
def firing_angle(v: float, dx: float, dy: float) -> float | None:
GRAVITY = 9.81
a = -((GRAVITY * dx**2) / (2 * v**2))
b = dx
c = dy + a
discriminant = b**2 - (4 * a * c)
if discriminant < 0:
return None
root = math.sqrt(discriminant)
angle1 = math.atan((-b - root) / (2 * a))
angle2 = math.atan((-b + root) / (2 * a))
if dx > 0:
return angle1
else:
return angle2
GDScript
Godot's coordinate system is a bit strange: the y-axis points downwards, which should change our answers. The fix isn't too bad, we just have to mess around with the quadrants a little!
static func firing_angle(v: float, dx: float, dy: float) -> float:
# Units are in pixels, so use 980
const GRAVITY = 980.0
var a := -((GRAVITY * pow(dx, 2)) / (2 * pow(v, 2)))
var b := dx
var c := dy + a
var discriminant := pow(b, 2) - (4 * a * c)
# This is an assertion, but you can change it to whatever you want
assert(discriminant > 0)
var root := sqrt(discriminant)
var angle1 := atan((-b - root) / (2 * a))
var angle2 := atan((-b + root) / (2 * a))
if dx > 0:
# angle1 should be in the 1st quadrant
assert(angle1 > 0)
# Negate to put into Godot's coordinate system (-y points upwards)
return -angle1
else:
# angle2 should be in the 4th quadrant
assert(angle2 < 0)
# Translate to the 2nd quadrant (for Godot's coordinate system)
return -PI - angle2
I checked my answers using this simulator! ↩︎
I needed both the distinct roots of
because one of them will actually be the angle I want, but I don't always know which without more information. ↩︎I used the GDScript version for the game I'm developing. ↩︎