While developing games which involve an action element, we often need to figure out a way to collide with a moving target. Such scenarios can be typically called a 'hitting a moving target' problem. This is particularly prominent in tower defense games or missile command like games. We may need to create an AI or algorithm which could figure out the enemy's motion and fire at it.
Let's see how we can solve this particular problem, this time in Unity.
1. The Missile Command Game
For this particular tutorial, we will consider a missile command game. In the game we have a turret on the ground which fires missiles at an incoming asteroid. We should not allow the asteroid to hit the ground.
The game play is tap-based, where we need to tap to aim the turret. With human assistance, the game mechanics are pretty straightforward as the turret just needs to aim and fire. But imagine if the turret needs to automatically fire at incoming asteroids.
The Challenges for Auto-Firing AI
The turret needs to find out how many asteroids are approaching the ground. Once it has a set of all approaching asteroids, it would then need to do a threat analysis to determine which one to target. A slow-moving asteroid is a lesser threat than a fast-moving one. Also, an asteroid which is closer to the ground is an imminent threat as well.
These problems can be solved by comparing the speed and position of the incoming asteroids. Once we have determined which one to target, we reach the most complicated problem. When should the turret fire? At which angle should it fire? When should the missile set to explode after firing? The third question becomes relevant because the missile explosion can also destroy the asteroid and has a bigger radius of effect as well.
To simplify the problem, the turret can decide to fire right away. Then we need to only figure out the angle of firing and distance of detonation. Also, there may be the case where the asteroid has already passed the area where it could be hit, meaning there is no solution!
You should download the unity source provided along with this tutorial to see the solution in action. We will see how we derive that solution.
2. The Solution
We are going to do a little refresher of our high school mathematics in order to find the solution. It is very straightforward and involves solving a quadratic equation. A quadratic equation looks like axˆ2 + bx + c = 0
, where x
is the variable to be found and it occurs with the highest power of 2.
Analysing the Problem
Let us try to represent our problem diagrammatically.
The green line shows the predicted path to be followed by the asteroid. As we are dealing with uniform motion, the asteroid moves with constant velocity. Our turret will need to rotate and fire the missile along the blue path for it to collide with the asteroid at a future time.
For uniform motion, the distance travelled by an object is the product of time and the object's speed, i.e. D = T x S
, where D
stands for the distance, T
is the time taken to travel D
, and S
is the speed of travel. Assuming that our asteroid and the missiles would definitely collide, we can find the distance of the blue line followed by the missile in terms of time t
. In the same time t
, our asteroid will also reach the same position.
Essentially, in the same time t
, the asteroid will reach the collision position from its current position, and the missile will also reach the same collision position in the same time t
. So at time t
, both the asteroid and the missile would be at the same distance from the turret as they would be colliding with each other.
Enter Math
We can equate the distance from the turret to the asteroid and missile at this future time t
in order to derive our quadratic equation with the variable t
. Consider two points on a two-dimensional plane with coordinates (x1,y1)
and (x2,y2)
. The distance D
between them can be calculated using the equation below.
Dˆ2 = (x2-x1)ˆ2 + (y2-y1)ˆ2
If we denote the turret position as (Tx,Ty)
, the missile speed as s
and the unknown collision position as (X,Y)
, then the above equation can be rewritten as:
Dˆ2 = (X-Tx)ˆ2 + (Y-Ty)ˆ2; D = s * t;
where t
is the time taken for the missile to travel the distance D
. Equating both, we get our first equation for unknowns X
and Y
with another unknown t
.
sˆ2 * tˆ2 = (X-Tx)ˆ2 + (Y-Ty)ˆ2
We know that the asteroid also reaches the same collision spot (X,Y)
in the same time t
, and we have the following equations using the horizontal and vertical components of the asteroid's velocity vector. If the velocity of the asteroid can be denoted by (Vx,Vy)
and the current position as (Ax,Ay)
, then the unknown X
and Y
can be found as below.
X = t * Vx + Ax; Y = t * Vy + Ay;
Substituting these in the earlier equation gives us a quadratic equation with a single unknown t
.
sˆ2 * tˆ2 = ((t * Vx + Ax)-Tx)ˆ2 + ((t * Vy + Ay)-Ty)ˆ2;
Expanding and combining similar terms:
sˆ2 * tˆ2 = (t * Vx + Ax)ˆ2 + Txˆ2 - 2*Tx*(t * Vx + Ax) + (t * Vy + Ay)ˆ2 + Tyˆ2 - 2*Ty*(t * Vy + Ay); sˆ2 * tˆ2 = tˆ2 * Vxˆ2 + Axˆ2 + 2*t*Vx*Ax + Txˆ2 - 2*Tx*(t * Vx + Ax) + tˆ2 * Vyˆ2 + Ayˆ2 + 2*t*Vy*Ay + Tyˆ2 - 2*Ty*(t * Vy + Ay); sˆ2 * tˆ2 = tˆ2 * Vxˆ2 + Axˆ2 + 2*t*Vx*Ax + Txˆ2 - 2*Tx*t*Vx - 2*Tx*Ax + tˆ2 * Vyˆ2 + Ayˆ2 + 2*t*Vy*Ay + Tyˆ2 - 2*Ty*t*Vy - 2*Ty*Ay; 0 = (Vxˆ2 + Vyˆ2 - sˆ2) * tˆ2 + 2* (Vx*Ax - Tx*Vx + Vy*Ay - Ty*Vy) *t + Ayˆ2 + Tyˆ2 - 2*Ty*Ay + Axˆ2 + Txˆ2 - 2*Tx*Ax; (Vxˆ2 +Vyˆ2 - sˆ2) * tˆ2 + 2* (Vx*(Ax - Tx) + Vy*(Ay - Ty)) *t + (Ay - Ty)ˆ2 + (Ax - Tx)ˆ2 = 0;
Representing the power of two as ˆ2
and the multiplication symbol as *
may have made the above look like hieroglyphics, but it essentially boils down to the final quadratic equation axˆ2 + bx + c = 0
, where x
is the variable t
, a
is Vxˆ2 +Vyˆ2 - sˆ2
, b
is 2* (Vx*(Ax - Tx) + Vy*(Ay - Ty))
, and c
is (Ay - Ty)ˆ2 + (Ax - Tx)ˆ2
. We used the equations below in the derivation.
(a+b)ˆ2 = aˆ2 + 2*a*b + bˆ2; (a-b)ˆ2 = aˆ2 - 2*a*b + bˆ2;
Solving the Quadratic Equation
To solve a quadratic equation, we need to calculate the discriminant D
using the formula:
D = bˆ2 - 4 * a * c;
If the discriminant is less than 0
then there is no solution, if it is 0
then there is a single solution, and if it is a positive number then there are two solutions. Solutions are calculated using the formulas given below.
t1 = (-b + sqrt(D))/ 2 * a; t2 = (-b - sqrt(D))/ 2 * a;
Using these formulas, we can find values for the future time t
when the collision will happen. A negative value for t
means we have missed the opportunity to fire. The unknowns X
and Y
can be found by substituting the value of t
in their respective equations.
X = t * Vx + Ax; Y = t * Vy + Ay;
Once we know the collision point, we can rotate our turret to fire the missile, which would definitely hit the moving asteroid after t
secs.
3. Implementing in Unity
For the sample Unity project, I have used the sprite creation feature of the latest Unity version to create the necessary placeholder assets. This can be accessed with Create > Sprites > as shown below.
We have a game script named MissileCmdAI
which is attached to the scene camera. It holds the reference to the turret sprite, missile prefab, and asteroid prefab. I am using SimplePool
by quill18 to maintain the object pools for missiles and asteroids. It can be found on GitHub. There are component scripts for missile and asteroid which are attached to their prefabs and handle their motion once released.
The Asteroids
Asteroids are randomly spawned at fixed height but random horizontal position and are hurled at a random horizontal position on the ground with a random speed. The frequency of asteroid spawning is controlled using an AnimationCurve
. The SpawnAsteroid
method in the MissileCmdAI
script looks as below:
void SpawnAsteroid(){ GameObject asteroid=SimplePool.Spawn(asteroidPrefab,Vector2.one,Quaternion.identity); Asteroid asteroidScript=asteroid.GetComponent<Asteroid>(); asteroidScript.Launch(); SetNextSpawn(); }
The Launch
method in the Asteroid
class is shown below.
public void Launch(){//place the asteroid in top with random x & launch it to bottom with random x bl=Camera.main.ScreenToWorldPoint(new Vector2(10,0)); br=Camera.main.ScreenToWorldPoint(new Vector2(Screen.width-20,0)); tl=Camera.main.ScreenToWorldPoint(new Vector2(0,Screen.height)); tr=Camera.main.ScreenToWorldPoint(new Vector2(Screen.width,Screen.height)); transform.localScale=Vector2.one*(0.2f+Random.Range(0.2f,0.8f)); asteroidSpeed=Random.Range(asteroidMinSpeed,asteroidMaxSpeed); asteroidPos.x=Random.Range(tl.x,tr.x); asteroidPos.y=tr.y+1; destination.y=bl.y; destination.x=Random.Range(bl.x,br.x); Vector2 velocity= asteroidSpeed* ((destination-asteroidPos).normalized); transform.position=asteroidPos; asteroidRb.velocity=velocity;//set a velocity to rigidbody to set it in motion deployDistance=Vector3.Distance(asteroidPos,destination);//after traveling this much distance, return to pool } void Update () { if(Vector2.Distance(transform.position,asteroidPos)>deployDistance){//once we have traveled the set distance, return to pool ReturnToPool(); } } void OnTriggerEnter2D(Collider2D projectile) { if(projectile.gameObject.CompareTag("missile")){//check collision with missile, return to pool ReturnToPool(); } }
As seen in the Update
method, once the asteroid has traveled the predetermined distance to ground, deployDistance
, it would return to its object pool. Essentially this means it has collided with the ground. It would do the same on the event of collision with the missile.
The Targeting
In order for the auto-targeting to work, we need to call the corresponding method frequently to find and target the incoming asteroid. This is done in the MissileCmdAI
script in its Start
method.
InvokeRepeating("FindTarget",1,aiPollTime);//set ai code polling
The FindTarget
method loops through all the asteroids present in the scene to find the closest and fastest asteroids. Once found, it then calls the AcquireTargetLock
method to apply our calculations.
void FindTarget(){//find fastest & closest asteroid GameObject[] aArr=GameObject.FindGameObjectsWithTag("asteroid"); GameObject closestAsteroid=null; Asteroid fastestAsteroid=null; Asteroid asteroid; foreach(GameObject go in aArr){ if(go.transform.position.y<groundProximity){//find closest if(closestAsteroid==null){ closestAsteroid=go; }else if(go.transform.position.y<closestAsteroid.gameObject.transform.position.y){ closestAsteroid=go; } } asteroid=go.GetComponent<Asteroid>(); if(fastestAsteroid==null){//find fastest fastestAsteroid=asteroid; }else if(asteroid.asteroidSpeed>fastestAsteroid.asteroidSpeed){ fastestAsteroid=asteroid; } } //if we have a closest one target that, else target the fastest if(closestAsteroid!=null){ AcquireTargetLock(closestAsteroid); }else if(fastestAsteroid!=null){ AcquireTargetLock(fastestAsteroid.gameObject); } }
AcquireTargetLock
is where the magic happens as we apply our quadratic equation solving skills to find the time of collision t
.
void AcquireTargetLock(GameObject targetAsteroid){ Asteroid asteroidScript=targetAsteroid.GetComponent<Asteroid>(); Vector2 targetVelocity=asteroidScript.asteroidRb.velocity; float a=(targetVelocity.x*targetVelocity.x)+(targetVelocity.y*targetVelocity.y)-(missileSpeed*missileSpeed); float b=2*(targetVelocity.x*(targetAsteroid.gameObject.transform.position.x-turret.transform.position.x) +targetVelocity.y*(targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)); float c= ((targetAsteroid.gameObject.transform.position.x-turret.transform.position.x)*(targetAsteroid.gameObject.transform.position.x-turret.transform.position.x))+ ((targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)*(targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)); float disc= b*b -(4*a*c); if(disc<0){ Debug.LogError("No possible hit!"); }else{ float t1=(-1*b+Mathf.Sqrt(disc))/(2*a); float t2=(-1*b-Mathf.Sqrt(disc))/(2*a); float t= Mathf.Max(t1,t2);// let us take the larger time value float aimX=(targetVelocity.x*t)+targetAsteroid.gameObject.transform.position.x; float aimY=targetAsteroid.gameObject.transform.position.y+(targetVelocity.y*t); RotateAndFire(new Vector2(aimX,aimY));//now position the turret } } public void RotateAndFire(Vector2 deployPos){//AI based turn & fire float turretAngle=Mathf.Atan2(deployPos.y-turret.transform.position.y,deployPos.x-turret.transform.position.x)*Mathf.Rad2Deg; turretAngle-=90;//art correction turret.transform.localRotation=Quaternion.Euler(0,0,turretAngle); FireMissile(deployPos, turretAngle);//launch missile } void FireMissile(Vector3 deployPos, float turretAngle){ float deployDist= Vector3.Distance(deployPos,turret.transform.position);//how far is our target GameObject firedMissile=SimplePool.Spawn(missilePrefab,turret.transform.position,Quaternion.Euler(0,0,turretAngle)); Rigidbody2D missileRb=firedMissile.GetComponent<Rigidbody2D>(); Missile missileScript=firedMissile.GetComponent<Missile>(); missileScript.LockOn(deployDist); missileRb.velocity=missileSpeed*firedMissile.transform.up;//missile is rotated in necessary direction already }
Once we find the point of impact, we can easily calculate the distance for the missile to travel in order to hit the asteroid, which is passed on through the deployDist
variable onto the LockOn
method of the missile. The missile uses this value to return to its object pool once it has travelled this distance the same way as the asteroid. Before this happens, it would have definitely hit the asteroid, and the collision events would have been triggered.
Conclusion
Once we implement it, the result looks almost magical. By reducing the aiPollTime
value, we can make it an invincible AI turret which would shoot down any asteroid unless the asteroid speed becomes close to or higher than our missile speed. The derivation we followed can be used to solve a variety of similar problems which could be represented in the form of a quadratic equation.
I would like you to experiment further by adding the effect of gravity to the motion of the asteroid and missile. This would change the motion to projectile motion, and the corresponding equations would change. Good luck.
Note also that Unity has an active economy. There are many other products that help you build out your project. The nature of the platform also makes it a great option from which you can better your skills. Whatever the case, you can see what we have available in the Envato Marketplace.