Interception of a moving target

This article describes how target interception, the ability to go where your target will be at some point in the future, can be accomplished in your Unity project. An example project using this feature can be downloaded here.

The first time we witnessed target interception in a game was in Unreal Tournament back in 1999. With the bots set to a higher skill level, they suddenly started to lead their shots, i.e. as we strafed left and right we experienced running straight into the bullets! This was as cool as it was frustrating, and we ended up dying a lot before we learned to be quicker on our feet.

Giving your AI the skill to intercept its target – be it with bullets or cars or planes or what have you – is a great way to make gameplay more challenging while also making the AI seem more competent.

To accomplish this in our game we took inspiration from this article, which not only provides a great code example but also goes into detail about the math behind it. It’s a lengthy read but well worth it if you want a deeper understanding on the subject.

Before we start, here’s a short clip showing interception doing its job:

So let’s formulate the problem, using straight-flying projectiles as an example:

Given the projectile’s start position and speed, and the target’s current position and velocity, at what point along the target’s trajectory do we aim such that the travel time of the projectile and the travel time of the target is exactly the same?

We won’t go into the math here since the article we mentioned already does that for us, except to say that by viewing the situation as sides in a triangle, where side A is the line from the projectile’s start position to the target, side B is the target’s trajectory, and side C is the line along which we should aim, we can use the law of cosines, which after expansion plugs into the quadratic formula (AX^2 + Bx + C = 0), which in turn tells us when the interception will happen.

Once we have the time to interception we get:

Interception position = target position + target velocity * time to interception.

Once we have the interception position we know where we should aim our weapon.

And we arrive at the code:

using UnityEngine;

public static class Util
{
    public const float DOUBLE_PI = 6.2831853f;
    public const float FLT_EPSILON = 0.00001f;
    public const float MAX_DISTANCE = 999999.0f;
    public const float MIN_SPEED = 0.1f;


    // quadratic formula solver: ax^2 + bx + c = 0 => x = -b +- sqrt(b^2 - 4ac) / 2a
    public static void QuadraticSolver(float a, float b, float c, out float x1, out float x2)
    {
        x1 = 0;
        x2 = 0;
        float preRoot = b * b - 4.0f * a * c;
        // if a == 0 it's a linear formula, not a quadratic one
        if (a == 0)
        {
            x1 = -c / b;
            x2 = x1;
        }
        else if (preRoot > 0)
        {
            x1 = (-b + Mathf.Sqrt(preRoot)) / (2.0f * a);
            x2 = (-b - Mathf.Sqrt(preRoot)) / (2.0f * a);
        }
    }


    // Get an interception position given a tracker's position and its speed, and the target's position and velocity.
    // The predicted interception position is targetPos + targetVelocity * timeToInterception, where timeToInterception is the unknown.
    // The function returns false when the target's speed is greater than the tracker's speed and the target is moving away from the tracker.
    // In this case the tracker will not be able to intercept the target.
    // Source: https://www.codeproject.com/articles/990452/interception-of-two-moving-objects-in-d-space
    public static bool GetInterceptPosition(Vector3 trackerPos, Vector3 targetPos, Vector3 targetVelocity, float trackerSpeed, out Vector3 interceptPos)
    {
        interceptPos = targetPos; // assume we will aim for the target's pos directly
        float targetSpeedSqr = targetVelocity.sqrMagnitude;

        if (targetSpeedSqr < FLT_EPSILON)
            return true; // target is not moving

        Vector3 vecToTracker = trackerPos - targetPos;
        float distanceToTracker = vecToTracker.magnitude;
        if (distanceToTracker < FLT_EPSILON)
            return true; // already at the target's position

        float a = trackerSpeed * trackerSpeed - targetSpeedSqr;
        float dotTrackerAndVelocity = Vector3.Dot(vecToTracker.normalized, targetVelocity.normalized);
        if (a < 0.0f && dotTrackerAndVelocity < 0.0f)
            // the target's speed is greater than the tracker's speed and the target is moving away from the tracker,
            // which means that the tracker will not be able to intercept the target
            return false;

        float b = 2.0f * Vector3.Dot(targetVelocity, vecToTracker);
        float c = -distanceToTracker * distanceToTracker;

        float t1 = 0.0f;
        float t2 = 0.0f;
        QuadraticSolver(a, b, c, out t1, out t2);
        float timeToIntercept = 0.0f;
        if (t1 > 0.0f && t2 > 0.0f)
            timeToIntercept = Mathf.Min(t1, t2); // both values positive, take the smaller one
        else
            timeToIntercept = Mathf.Max(t1, t2); // one value negative, take the bigger (positive) one

        interceptPos = targetPos + targetVelocity * timeToIntercept;
        //Debug.DrawLine(trackerPos, interceptPos, Color.red);
        return true;
    }

}

A couple of important things to note:

The code works for both 2D and 3D games. If you’re doing a 2D game you still need to supply Vector3 parameters (with the z-component being zero), or rewrite the intercept method.

We calculate the intercept position from the target’s current velocity, i.e. we assume that the target’s velocity will remain constant. If the target changes speed or movement direction the interception may fail.

This also means that if the target has a rigid body with significant drag the interception may also fail even though no other forces acts on the target, because the physics system will slow it down as the interceptor speeds towards the presumed meeting point. The same holds true for gravity, which if they act on either or both the target and the interceptor may complicate matters.

To see how the interception code can be used, we have created a simple Unity project where you can test it out. It’s running on Editor version 2020.3.20f1 but you should be able to run it on any version later than that (no promises though!). Once you have opened the project in the editor, open the “InterceptionExample” scene. You can download the project from here.

The interception code is in Util.cs, and it’s used in the Update()-method in AICannonHandler.cs.

Use ‘W’, ‘S’, ‘A’ and ‘D’ to move the blue ball around and notice how the yellow projectiles intercept your ball, but only as long as you don’t change the ball’s velocity.

If you have any questions or comments about the code, feel free to email us at info@syntheticforms.com.

/Roger

Disclaimer: The code is tested and has a works-on-our-machine certificate, but we cannot guarantee that it will work for you. Also, you use it at your own risk. We assume no responsibility for any problems you encounter.