One problem I’ve set as an exercise for students for some time is hitting a moving target. The problem is, if the projectile takes some time to reach the target, then you need to aim ahead of the target by an amount. And that amount depends on the distance to the aim ahead point, which depends on the time, and so on.
The typical approach to solving this problem is to create a quadratic equation to find the time when the projectile trajectory intersects the target trajectory. Then you can work your way back by plugging the time into the target velocity and calculating the aim ahead point. Finally you calculate the angle you need to hit that point and fire at that angle.
- equate target and projectile positions in terms of time
- solve for time
- project target position at time
- find angle to projected position
- fire along that angle
If you’ve ever been through this process, you’ll know the number of terms in the equation you need to solve gets quite large during the working out. And that means that the potential for making a mistake is quite high. So you have to proceed carefully to avoid factoring errors.
While it is good practice, it is also tedious. Having done it a number of times before, I found that I couldn’t be bothered going through the explanation again. I wondered if there was a simpler approach. As it turned out, there was.
The trick to lazy programming is not calculating things until you absolutely need them. And the way to do that is to write your program backwards. Sometimes this is called programming by wishful thinking, or programming by intent, or top down programming. But whatever you want to call it, the purpose is to stake out our final destination before writing the code.
transform.rotation = AimAheadOfMovingTarget(target);
Another problem solving trick is to solve a simpler problem first. So let’s ignore cases where the target velocity could be in any direction and focus just on the case that forms a right angled triangle.
To find the angle here we only need lengths o and h and we can use simple trigonometry to find the angle a:
\sin{a} = \frac{o}{h}
Or:
a = \arcsin{\frac{o}{h}}
So all we really need, to calculate angle a, is the ratio of lengths o and h. Given:
v = target\ velocity\\ p = projectile\ velocity\\ t = time
The values for o and h are:
o = ||v||\ t\\ h = ||p||\ t
So plugging that back into the expression for the angle we get:
a = \arcsin{\frac{||v||\ t}{||p||\ t}}
And as you can see, the time variables t will cancel out leaving:
a = \arcsin{\frac{||v||}{||p||}}
So we don’t need to know the intercept time t. And we also don’t need to know the distance to the target. And what this means is that the angle will be the same whatever distance the target is from us.
One thing we do need to know though, is the relative direction. If the target trajectory was to the left of us, instead of to the right, the values for o and h would be the same. We wouldn’t know whether to rotate to the left or the right.
To make this work we really want a negative value for o if the trajectory of the target is to the left. That way, our result will be positive for a clockwise turn and negative for anti-clockwise. So what we want is a function that, given a vector and a direction, will return us a distance in that direction. Fortunately, that function already exists. It is the dot product:
{a} \cdot {b} = ||a||\ ||b|| \cos{\theta}
Where a and b are vectors, and θ is the angle between them. So given a vector that points to the right of our target:
r = unit\ vector\ to\ the\ right\ of\ target
We can find a directional value for the trajectory v like this:
v \cdot r = ||v||\ ||r|| \cos{\theta}
And since we defined r to be of length 1, this becomes:
v \cdot r = ||v||\cos{\theta}
If angle θ is 0, the cosine will be 1, so the result will be the length of v. But if the angle θ is 180°, the cosine will be -1, so the result will be the negative length of v.
Quaternion AimAheadOfMovingTarget(Target target)
{
float v = Vector3.Dot(target.velocity, Vector3.right);
float p = projectile.speed;
float a = Mathf.Asin(v / p) * Mathf.Rad2Deg;
return Quaternion.AngleAxis(a, Vector3.up);
}
As a bonus, if the angle is anything in between, the result will be that component of the velocity that is moving orthogonally to the right of us. So wherever the target is moving, we will get the right value to plug into our triangle to get the aim ahead angle. It does seem odd here that the other component is not relevant. It’s fairly obvious in the case that the target is coming directly towards or away from us that the angle would be 0 in both cases. Although it might be that we will never hit due to the target outrunning the projectile.
But if the direction was diagonally away or towards it doesn’t seem immediately intuitive that the angle needed to hit it would be the same. If we work backwards, starting with the assumption that we hit, we can trace back to where the target must have come from.
Now if we scale down the far target triangle such that its location matches the near target, we can see that these are similar triangles and the ratio between o and h, and therefore the angle a, remain the same.
So that solves pretty much every case. However, we do still need to find this right pointing vector r. In our simple case it is a global constant, but in the more general case we will need to take the vector to the target start position into account. The vector r is 90° to the up and to target vectors. And the up is at 90° to the to target and target velocity vectors.
We can find a vector at 90° to two vectors using the cross product function:
a \times b = ||a||\ ||b||\sin{\theta}\ n
Where a and b are the two vectors and n is a vector at 90°. As you can see, the vector n is scaled by the lengths of the vectors and the sine of the angle between them, but we can ignore all of that and just use the vector.
Vector3 toTarget = target.position - position;
Vector3 up = Vector3.Cross(toTarget, target.velocity);
Vector3 right = Vector3.Cross(up, toTarget).normalized;
The final step is to combine the rotation around the up axis with a rotation to point at the target. The Quaternion class provides functions to do both of these. We just need to remember to combine them in reverse order.
Quaternion AimAheadOfMovingTarget(Target target)
{
Vector3 toTarget = target.position - position;
Vector3 up = Vector3.Cross(toTarget, target.velocity);
Vector3 right = Vector3.Cross(up, toTarget).normalized;
float v = Vector3.Dot(target.velocity, right);
float p = projectile.speed;
float a = Mathf.Asin(v / p) * Mathf.Rad2Deg;
return Quaternion.AngleAxis(a, up) * Quaternion.LookRotation(toTarget, up);
}
I have uploaded a Unity project to GitHub to demonstrate this approach.
Leave a Reply