Most people probably don’t consider the intricacies of computer-game enemies shooting at them. In many cases rightfully so, since it’s mostly a very simple feature. The game code to send a fast moving projectile towards you rarely involves more than a subtraction of two points to get a direction, and multiplying with whatever speed bullets happen to travel at in the given game. If it’s fast enough, it’ll hit you. Other times, bullets are instant hit and the game simply checks for intersection with a line segment – no chance of escaping that one either.
In the game I’m currently working on, however, most of the enemies don’t shoot bullets; they throw water balloons. Just as in real life, timing and aiming a throw is quite a bit more involved than firing a bullet, and while most of us learn this as kids by trial and error, a game usually solves it with a bit of math.
It really isn’t much more complicated than the two scenarios I just described, but it does involve matching a polynomial with a linear interpolation, so if both of those sound alien to you, and you happen to be faced with this problem, read on :).
Before we look at the math, here’s a quick overview of the problem we’re faced with:
Throwing B at P
Let’s say we’re the enemy and we’re at location B0, throwing at the player who is currently at P0. What we want is to find the velocity F that our projectile B must travel with so that it will impact with P when the gravity (g) pulls the projectile to the ground. N is the current velocity of the player.
The path of the projectile is given by the second order polynomial
B = B0 + F*t + G*t²
The path of the player is given by the linear equation
P = P0 + N*t
Assuming, of course, that he does not change direction or speed – which is kind of the whole point of the game 🙂
Before we try to solve this, note that the G vector is special in that it only affects one axis. This simplifies matters a great deal since our second-order polynomial then only exist for this axis, and hence, we can separate the motion in the X/Z plane from that of the Y axis. Or more specifically, we can ignore X/Z and pick a velocity in Y that creates a nice curve and gives us a time of impact, and then adjust our projectile speed in X/Z to match the players location at the calculated time.
So let’s look at the equation for Y:
B0.y + F.y*t + G.y*t² = P0.y + N.y*t
For some rather game-specific reasons, I don’t care if the player moves in Y, so my N.y is always 0. It should be straight forward to change the code to take this into account, you just need to carry the N.y term along. Anyway, in my case, I can simplify to:
G.y*t² + F.y*t + (B0.y -P0.y) = 0
Which is the 2nd order polynomial we need to solve to determine our time of impact, t. If this doesn’t ring any bells, just google it, there’s a text-book solution for this, and chances are you’ve solved a million of these in school and just forgotten about it ;). Below I’ll just go through the code.
This is what a prettified version of my implementation looks like:
Vector3 P0 = player.transform.position;
Vector3 B0 = enemy.transform.position + initialOffset;
Vector3 F = (P0-B0);
float l = F.magnitude;
F.y = l*.5f;
Vector3 G = new Vector3(0,-9.81f/2,0);
Vector3 N = player.transform.forward * player.runningSpeed;
float t = SolvePoly(G.y, F.y, B0.y-P0.y);
F.z = (P0.z-B0.z) / t + N.z;
F.x = (P0.x-B0.x) / t + N.x;
WaterBalloon b = (WaterBalloon)Instantiate(_waterBalloon);
b.transform.position = B0;
b.rigidbody.velocity = F;
In short, this starts by picking a Y velocity that is proportional to the distance to the target. This creates a higher arc for targets that are further away. With F.y set, I can solve the second order polynomial for Y and derive the time of impact “t”, which I then use to calculate the remaining two components of F.
F is now my initial velocity which I can assign directly to my (non kinematic) rigidbody in Unity. If, for some reason, you are using a kinematic projectile, you’ll need to move it yourself in the FixedUpdate method. Something along the lines of
transform.position += _velocity * Time.fixedDeltaTime;
_velocity += _gravity * Time.fixedDeltaTime;
should do the trick.
There’s a couple of details in this code that are maybe not immediately obvious:
Line 2: I add an initial offset to the enemy location to get the projectile start location. This is simply because the enemy is supposed to throw the balloon, not kick it – I.e. I want it leaving his hands, not his feet 🙂
Line 8: I set the Y velocity to half the distance between the enemy and the player. This is a more or less arbitrary choice – the smaller the number, the flatter the curve. Note that this value alone determines time-to-impact, so if you keep it constant, short throws will take just as long to hit as long throws.
Line 10: If you look closer at the definition of G you’ll notice that gravity is defined to be only half of earths normal gravity of 9.81. The reason for this is that the polynomial is describing the curve of the projectile which is the integral of its velocity, not the velocity itself. If you differentiate to get the velocity of the curve:
p = p0 + v*t + k*t² =>
p’ = v + 2*k*dt
…and insert g/2 for k, you will see that the change in velocity caused by gravity correctly becomes g*dt.
Line 11: I calculate the players velocity using his forward direction and a speed – if the target is a non-kinematic rigidbody you could probably use the velocity directly, but mine isn’t.
Line 13: As you probably know, a second order polynomial has two solutions – I, however, only calculate one of them. If you plot the curve, the reason will be obvious: The polynomial will cross zero twice between thrower and target. Once going up, and once coming down – we want the one coming down which is obviously the larger t and because of the nature of our particular set of coefficients, this is always the solution described by s1 below:
private float SolvePoly(float a,float b,float c)
float sqrtd = Mathf.Sqrt(b*b - 4*a*c);
float s1 = (-b - sqrtd)/(2*a);
Line 14: It is worth noting that this *can* fail. If the chosen Y velocity is not sufficiently large to bring the projectile to the same height as the target location, the second order polynomial will have no solutions, and sqrt will return NaN, hence the check for NaN. You *could* check for this in SolvePoly before doing the sqrt, but in my case it’s such a rare occasion that I’m not too concerned with the performance penalty. I’m happy as long as it does not create any visual artifacts.
Lines 16-17: These two equations are easily derived from the original equations for projectile position B and players position P, like this
B0 + F*t + G*t² = P0 + N*t =>
And then solve for F, noting that G is zero in the cases of X and Z which are the ones we are currently interested in.
F*t = P0 – B0 + N*t =>
F = (P0 – B0) / t + N