2d game : fire at a moving target by predicting intersection of projectile and unit
Solution 1
I wrote an aiming subroutine for xtank a while back. I'll try to lay out how I did it.
Disclaimer: I may have made one or more silly mistakes anywhere in here; I'm just trying to reconstruct the reasoning with my rusty math skills. However, I'll cut to the chase first, since this is a programming Q&A instead of a math class :-)
How to do it
It boils down to solving a quadratic equation of the form:
a * sqr(x) + b * x + c == 0
Note that by sqr
I mean square, as opposed to square root. Use the following values:
a := sqr(target.velocityX) + sqr(target.velocityY) - sqr(projectile_speed)
b := 2 * (target.velocityX * (target.startX - cannon.X)
+ target.velocityY * (target.startY - cannon.Y))
c := sqr(target.startX - cannon.X) + sqr(target.startY - cannon.Y)
Now we can look at the discriminant to determine if we have a possible solution.
disc := sqr(b) - 4 * a * c
If the discriminant is less than 0, forget about hitting your target -- your projectile can never get there in time. Otherwise, look at two candidate solutions:
t1 := (-b + sqrt(disc)) / (2 * a)
t2 := (-b - sqrt(disc)) / (2 * a)
Note that if disc == 0
then t1
and t2
are equal.
If there are no other considerations such as intervening obstacles, simply choose the smaller positive value. (Negative t values would require firing backward in time to use!)
Substitute the chosen t
value back into the target's position equations to get the coordinates of the leading point you should be aiming at:
aim.X := t * target.velocityX + target.startX
aim.Y := t * target.velocityY + target.startY
Derivation
At time T, the projectile must be a (Euclidean) distance from the cannon equal to the elapsed time multiplied by the projectile speed. This gives an equation for a circle, parametric in elapsed time.
sqr(projectile.X - cannon.X) + sqr(projectile.Y - cannon.Y)
== sqr(t * projectile_speed)
Similarly, at time T, the target has moved along its vector by time multiplied by its velocity:
target.X == t * target.velocityX + target.startX
target.Y == t * target.velocityY + target.startY
The projectile can hit the target when its distance from the cannon matches the projectile's distance.
sqr(projectile.X - cannon.X) + sqr(projectile.Y - cannon.Y)
== sqr(target.X - cannon.X) + sqr(target.Y - cannon.Y)
Wonderful! Substituting the expressions for target.X and target.Y gives
sqr(projectile.X - cannon.X) + sqr(projectile.Y - cannon.Y)
== sqr((t * target.velocityX + target.startX) - cannon.X)
+ sqr((t * target.velocityY + target.startY) - cannon.Y)
Substituting the other side of the equation gives this:
sqr(t * projectile_speed)
== sqr((t * target.velocityX + target.startX) - cannon.X)
+ sqr((t * target.velocityY + target.startY) - cannon.Y)
... subtracting sqr(t * projectile_speed)
from both sides and flipping it around:
sqr((t * target.velocityX) + (target.startX - cannon.X))
+ sqr((t * target.velocityY) + (target.startY - cannon.Y))
- sqr(t * projectile_speed)
== 0
... now resolve the results of squaring the subexpressions ...
sqr(target.velocityX) * sqr(t)
+ 2 * t * target.velocityX * (target.startX - cannon.X)
+ sqr(target.startX - cannon.X)
+ sqr(target.velocityY) * sqr(t)
+ 2 * t * target.velocityY * (target.startY - cannon.Y)
+ sqr(target.startY - cannon.Y)
- sqr(projectile_speed) * sqr(t)
== 0
... and group similar terms ...
sqr(target.velocityX) * sqr(t)
+ sqr(target.velocityY) * sqr(t)
- sqr(projectile_speed) * sqr(t)
+ 2 * t * target.velocityX * (target.startX - cannon.X)
+ 2 * t * target.velocityY * (target.startY - cannon.Y)
+ sqr(target.startX - cannon.X)
+ sqr(target.startY - cannon.Y)
== 0
... then combine them ...
(sqr(target.velocityX) + sqr(target.velocityY) - sqr(projectile_speed)) * sqr(t)
+ 2 * (target.velocityX * (target.startX - cannon.X)
+ target.velocityY * (target.startY - cannon.Y)) * t
+ sqr(target.startX - cannon.X) + sqr(target.startY - cannon.Y)
== 0
... giving a standard quadratic equation in t. Finding the positive real zeros of this equation gives the (zero, one, or two) possible hit locations, which can be done with the quadratic formula:
a * sqr(x) + b * x + c == 0
x == (-b ± sqrt(sqr(b) - 4 * a * c)) / (2 * a)
Solution 2
+1 on Jeffrey Hantin's excellent answer here. I googled around and found solutions that were either too complex or not specifically about the case I was interested in (simple constant velocity projectile in 2D space.) His was exactly what I needed to produce the self-contained JavaScript solution below.
The one point I would add is that there are a couple special cases you have to watch for in addition to the discriminant being negative:
- "a == 0": occurs if target and projectile are traveling the same speed. (solution is linear, not quadratic)
- "a == 0 and b == 0": if both target and projectile are stationary. (no solution unless c == 0, i.e. src & dst are same point.)
Code:
/**
* Return the firing solution for a projectile starting at 'src' with
* velocity 'v', to hit a target, 'dst'.
*
* @param ({x, y}) src position of shooter
* @param ({x, y, vx, vy}) dst position & velocity of target
* @param (Number) v speed of projectile
*
* @return ({x, y}) Coordinate at which to fire (and where intercept occurs). Or `null` if target cannot be hit.
*/
function intercept(src, dst, v) {
const tx = dst.x - src.x;
const ty = dst.y - src.y;
const tvx = dst.vx;
const tvy = dst.vy;
// Get quadratic equation components
const a = tvx * tvx + tvy * tvy - v * v;
const b = 2 * (tvx * tx + tvy * ty);
const c = tx * tx + ty * ty;
// Solve quadratic
const ts = quad(a, b, c); // See quad(), below
// Find smallest positive solution
let sol = null;
if (ts) {
const t0 = ts[0];
const t1 = ts[1];
let t = Math.min(t0, t1);
if (t < 0) t = Math.max(t0, t1);
if (t > 0) {
sol = {
x: dst.x + dst.vx * t,
y: dst.y + dst.vy * t
};
}
}
return sol;
}
/**
* Return solutions for quadratic
*/
function quad(a, b, c) {
let sol = null;
if (Math.abs(a) < 1e-6) {
if (Math.abs(b) < 1e-6) {
sol = Math.abs(c) < 1e-6 ? [0, 0] : null;
} else {
sol = [-c / b, -c / b];
}
} else {
let disc = b * b - 4 * a * c;
if (disc >= 0) {
disc = Math.sqrt(disc);
a = 2 * a;
sol = [(-b - disc) / a, (-b + disc) / a];
}
}
return sol;
}
// For example ...
const sol = intercept(
{x:2, y:4}, // Starting coord
{x:5, y:7, vx: 2, vy:1}, // Target coord and velocity
5 // Projectile velocity
)
console.log('Fire at', sol)
Solution 3
Jeffrey Hantin has a nice solution for this problem, though his derivation is overly complicated. Here's a cleaner way of deriving it with some of the resultant code at the bottom.
I'll be using x.y to represent vector dot product, and if a vector quantity is squared, it means I am dotting it with itself.
origpos = initial position of shooter
origvel = initial velocity of shooter
targpos = initial position of target
targvel = initial velocity of target
projvel = velocity of the projectile relative to the origin (cause ur shooting from there)
speed = the magnitude of projvel
t = time
We know that the position of the projectile and target with respect to t
time can be described with some equations.
curprojpos(t) = origpos + t*origvel + t*projvel
curtargpos(t) = targpos + t*targvel
We want these to be equal to each other at some point (the point of intersection), so let's set them equal to each other and solve for the free variable, projvel
.
origpos + t*origvel + t*projvel = targpos + t*targvel
turns into ->
projvel = (targpos - origpos)/t + targvel - origvel
Let's forget about the notion of origin and target position/velocity. Instead, let's work in relative terms since motion of one thing is relative to another. In this case, what we now have is relpos = targetpos - originpos
and relvel = targetvel - originvel
projvel = relpos/t + relvel
We don't know what projvel
is, but we do know that we want projvel.projvel
to be equal to speed^2
, so we'll square both sides and we get
projvel^2 = (relpos/t + relvel)^2
expands into ->
speed^2 = relvel.relvel + 2*relpos.relvel/t + relpos.relpos/t^2
We can now see that the only free variable is time, t
, and then we'll use t
to solve for projvel
. We'll solve for t
with the quadratic formula. First separate it out into a
, b
and c
, then solve for the roots.
Before solving, though, remember that we want the best solution where t
is smallest, but we need to make sure that t
is not negative (you can't hit something in the past)
a = relvel.relvel - speed^2
b = 2*relpos.relvel
c = relpos.relpos
h = -b/(2*a)
k2 = h*h - c/a
if k2 < 0, then there are no roots and there is no solution
if k2 = 0, then there is one root at h
if 0 < h then t = h
else, no solution
if k2 > 0, then there are two roots at h - k and h + k, we also know r0 is less than r1.
k = sqrt(k2)
r0 = h - k
r1 = h + k
we have the roots, we must now solve for the smallest positive one
if 0<r0 then t = r0
elseif 0<r1 then t = r1
else, no solution
Now, if we have a t
value, we can plug t
back into the original equation and solve for the projvel
projvel = relpos/t + relvel
Now, to the shoot the projectile, the resultant global position and velocity for the projectile is
globalpos = origpos
globalvel = origvel + projvel
And you're done!
My implementation of my solution in Lua, where vec*vec represents vector dot product:
local function lineartrajectory(origpos,origvel,speed,targpos,targvel)
local relpos=targpos-origpos
local relvel=targvel-origvel
local a=relvel*relvel-speed*speed
local b=2*relpos*relvel
local c=relpos*relpos
if a*a<1e-32 then--code translation for a==0
if b*b<1e-32 then
return false,"no solution"
else
local h=-c/b
if 0<h then
return origpos,relpos/h+targvel,h
else
return false,"no solution"
end
end
else
local h=-b/(2*a)
local k2=h*h-c/a
if k2<-1e-16 then
return false,"no solution"
elseif k2<1e-16 then--code translation for k2==0
if 0<h then
return origpos,relpos/h+targvel,h
else
return false,"no solution"
end
else
local k=k2^0.5
if k<h then
return origpos,relpos/(h-k)+targvel,h-k
elseif -k<h then
return origpos,relpos/(h+k)+targvel,h+k
else
return false,"no solution"
end
end
end
end
Solution 4
Following is polar coordinate based aiming code in C++.
To use with rectangular coordinates you would need to first convert the targets relative coordinate to angle/distance, and the targets x/y velocity to angle/speed.
The "speed" input is the speed of the projectile. The units of the speed and targetSpeed are irrelevent, as only the ratio of the speeds are used in the calculation. The output is the angle the projectile should be fired at and the distance to the collision point.
The algorithm is from source code available at http://www.turtlewar.org/ .
// C++
static const double pi = 3.14159265358979323846;
inline double Sin(double a) { return sin(a*(pi/180)); }
inline double Asin(double y) { return asin(y)*(180/pi); }
bool/*ok*/ Rendezvous(double speed,double targetAngle,double targetRange,
double targetDirection,double targetSpeed,double* courseAngle,
double* courseRange)
{
// Use trig to calculate coordinate of future collision with target.
// c
//
// B A
//
// a C b
//
// Known:
// C = distance to target
// b = direction of target travel, relative to it's coordinate
// A/B = ratio of speed and target speed
//
// Use rule of sines to find unknowns.
// sin(a)/A = sin(b)/B = sin(c)/C
//
// a = asin((A/B)*sin(b))
// c = 180-a-b
// B = C*(sin(b)/sin(c))
bool ok = 0;
double b = 180-(targetDirection-targetAngle);
double A_div_B = targetSpeed/speed;
double C = targetRange;
double sin_b = Sin(b);
double sin_a = A_div_B*sin_b;
// If sin of a is greater than one it means a triangle cannot be
// constructed with the given angles that have sides with the given
// ratio.
if(fabs(sin_a) <= 1)
{
double a = Asin(sin_a);
double c = 180-a-b;
double sin_c = Sin(c);
double B;
if(fabs(sin_c) > .0001)
{
B = C*(sin_b/sin_c);
}
else
{
// Sin of small angles approach zero causing overflow in
// calculation. For nearly flat triangles just treat as
// flat.
B = C/(A_div_B+1);
}
// double A = C*(sin_a/sin_c);
ok = 1;
*courseAngle = targetAngle+a;
*courseRange = B;
}
return ok;
}
Solution 5
Here's an example where I devised and implemented a solution to the problem of predictive targeting using a recursive algorithm: http://www.newarteest.com/flash/targeting.html
I'll have to try out some of the other solutions presented because it seems more efficient to calculate it in one step, but the solution I came up with was to estimate the target position and feed that result back into the algorithm to make a new more accurate estimate, repeating several times.
For the first estimate I "fire" at the target's current position and then use trigonometry to determine where the target will be when the shot reaches the position fired at. Then in the next iteration I "fire" at that new position and determine where the target will be this time. After about 4 repeats I get within a pixel of accuracy.
David Johnson
Updated on July 13, 2022Comments
-
David Johnson almost 2 years
Okay, this all takes place in a nice and simple 2D world... :)
Suppose I have a static object A at position Apos, and a linearly moving object B at Bpos with bVelocity, and an ammo round with velocity Avelocity...
How would I find out the angle that A has to shoot, to hit B, taking into account B's linear velocity and the speed of A's ammo ?
Right now the aim's at the current position of the object, which means that by the time my projectile gets there the unit has moved on to safer positions :)