Interception: Finding closest possible point to target when interceptor is too slow

I have been trying to use the following script I found to calculate lead/interception points:

using UnityEngine;

public static class InterceptPointCalculator
{
    //first-order intercept using absolute target position

    public static Vector3 FirstOrderIntercept(Vector3 shooterPosition,
                                                Vector3 shooterVelocity,
                                                float shotSpeed,
                                                Vector3 targetPosition,
                                                Vector3 targetVelocity)
    {
        Vector3 targetRelativeVelocity = targetVelocity - shooterVelocity;
        float t = FirstOrderInterceptTime(shotSpeed,
                                            targetPosition - shooterPosition,
                                            targetRelativeVelocity);

        Vector3 interceptPoint = targetPosition + t * (targetRelativeVelocity);
        return interceptPoint;
    }

    //first-order intercept using relative target position
    public static float FirstOrderInterceptTime(float shotSpeed,
                                                Vector3 targetRelativePosition,
                                                Vector3 targetRelativeVelocity)
    {
        float velocitySquared = targetRelativeVelocity.sqrMagnitude;
        if (velocitySquared < 0.001f)
        {
            return 0f;
        }

        float a = velocitySquared - shotSpeed * shotSpeed;

        //handle similar velocities
        if (Mathf.Abs(a) < 0.001f)
        {
            float t = -targetRelativePosition.sqrMagnitude
                        / (2f * Vector3.Dot(targetRelativeVelocity,
                                            targetRelativePosition));

            return Mathf.Max(t, 0f); //don't shoot back in time
        }

        float b = 2f * Vector3.Dot(targetRelativeVelocity, targetRelativePosition),
                c = targetRelativePosition.sqrMagnitude,
                determinant = b * b - 4f * a * c;

        if (determinant > 0f)
        { //determinant > 0; two intercept paths (most common)
            float t1 = (-b + Mathf.Sqrt(determinant)) / (2f * a),
                    t2 = (-b - Mathf.Sqrt(determinant)) / (2f * a);
            if (t1 > 0f)
            {
                if (t2 > 0f)
                {
                    return Mathf.Min(t1, t2); //both are positive
                }
                else
                {
                    return t1; //only t1 is positive
                }
            }
            else
            {
                return Mathf.Max(t2, 0f); //don't shoot back in time
            }
        }
        else if (determinant < 0f) //determinant < 0; no intercept path
        {
            return 0f;
        }
        else //determinant = 0; one intercept path, pretty much never happens
        {
            return Mathf.Max(-b / (2f * a), 0f); //don't shoot back in time
        }
    }
}

I have two main questions:

  1. I believe the script returns zero FirstOrderInterceptTime (resulting in the target’s position) if there is no possible intercept point. That is, if the shooter/shot is too slow to ever reach the target on its current path at its current speed. But is there a way to find the closest possible point to the target that the shooter could get along its path (assuming the target is flying relatively toward the shooter)?

  2. I can’t seem to find much use for the shooter’s velocity in the above code. When the shooter’s velocity is zero, the shot is always accurate. But when the shooter’s velocity is not zero, the shots miss. If the shooter goes relatively toward the target, the shots fall short. The only way I could get the shots to always be accurate is to just input Vector3.zero as the shooter’s velocity. What is the purpose of the shooter’s velocity?

Thank you! :slight_smile:

  1. movePosition=target.position+target.velocity-shooter.velocity. // if the distance is above a threshold then ignore target

  2. You’re probably not adding the shooter’s velocity to your projectiles and so your shooter’s velocity is redundant.

BTW - Your script seems unnecessarily complicated.

Right. Following the rules of classical mechanics, shooting a projective with speed x from a moving interceptor would naturally add the interceptor speed to the bullet. So shooting forward means the bullet is faster while when shooting backwards the bullets would move slower in absolute terms.

So it depends on what behaviour you actually want. In certain gameplay scenarios realism is not always wanted as when you’re dealing with relatively slow bullets, the player could potentially catch his own shots unless the bullets are at least twice as fast as the player.

Over here I also posted an intercept method including a WebGL build so you can see it in action, especially in comparison to the “simple” prediction. You can freely change the bullet / interceptor speed or the enemy speed and watch the result. In the simulation the enemy “wraps” back around once it reaches the wrapping point. The shoot button will disable once the calculated / predicted interception point is beyond that point. So as long as you don’t change any speeds while shots are flying you should get a 100% hit count, even when the bullet speed is slower compared to the target / enemy speed.

Note that the interception point could be very very far away when the conditions are on the edge or no course at all may be possible. In that case no matter the direction, the distance to the enemy will only get larger, so the closest point is now. It will not calculate a course to a point of closest approach. If it’s completely impossible to actually reach the target

Code copy

public static Vector3 CalculateInterceptCourse(Vector3 aTargetPos, Vector3 aTargetSpeed, Vector3 aInterceptorPos, float aInterceptorSpeed)
{
    Vector3 targetDir = aTargetPos - aInterceptorPos;

    float iSpeed2 = aInterceptorSpeed * aInterceptorSpeed;
    float tSpeed2 = aTargetSpeed.sqrMagnitude;
    float fDot1 = Vector3.Dot(targetDir, aTargetSpeed);
    float targetDist2 = targetDir.sqrMagnitude;
    float d = (fDot1 * fDot1) - targetDist2 * (tSpeed2 - iSpeed2);
    if (d < 0.1f)  // negative == no possible course because the interceptor isn't fast enough
        return Vector3.zero;
    float sqrt = Mathf.Sqrt(d);
    float S1 = (-fDot1 - sqrt) / targetDist2;
    float S2 = (-fDot1 + sqrt) / targetDist2;

    if (S1 < 0.0001f)
    {
        if (S2 < 0.0001f)
            return Vector3.zero;
        else
            return (S2) * targetDir + aTargetSpeed;
    }
    else if (S2 < 0.0001f)
        return (S1) * targetDir + aTargetSpeed;
    else if (S1 < S2)
        return (S2) * targetDir + aTargetSpeed;
    else
        return (S1) * targetDir + aTargetSpeed;
}

Additionally here’s another function which might be useful: FindClosestPointOfApproach

public static float FindClosestPointOfApproach(Vector3 aPos1, Vector3 aSpeed1, Vector3 aPos2, Vector3 aSpeed2)
{
    Vector3 PVec = aPos1 - aPos2;
    Vector3 SVec = aSpeed1 - aSpeed2;
    float d = SVec.sqrMagnitude;
    // if d is 0 then the distance between Pos1 and Pos2 is never changing
    // so there is no point of closest approach... return 0
    // 0 means the closest approach is now!
    if (d >= -0.0001f && d <= 0.0002f)
        return 0.0f;
    return (-Vector3.Dot(PVec, SVec) / d);
}

The function “FindClosestPointOfApproach” will just tell you the “time” when two moving things will reach their closest point of approach. Note that this point can lie in the past (negative time). To determine where both objects will be just add their respective velocities multiplied by that time to their current position. Should be kinda obvious ^^.

I’ve done some work on the Quake(world) engine and while many would consider it the first FPS with realistic physics it actually had pretty basic projectiles that didn’t get any velocity at all from the player when being launched. So to avoid killing the player the projectiles wouldn’t register any collisions against the player. It’s surprising how unnoticeable it was but I suspect it’s because of how high velocity the projectiles were.

Despite this when making a bot I still had to take into account the bot’s velocity when launching projectiles because the turning/aiming obviously isn’t instant. So a bot moving laterally and away from a target will take longer to get the target within its crosshair.

So yeah, shooter velocity nearly always needs to be considered. :slight_smile:

:smile: Since I’m sort of a veteran Quake3 / life player it’s kinda funny to read that. The usual run speed in Quake was 320 ups while rockets fly at 800 ups (was raised to 1k in QL) and in large enough maps it was possible to get faster than a rocket relatively easy. I would say the average movement speed in Q3 was closer to 450-600 ups. When you start moving you naturally start with a circle jump :slight_smile: Hmm, I haven’t played Q3 / QL in a long time, I’m probably getting too old. Though I loved DeFRaG.

Anyways, most shooters do not include the reference speed. Projetiles simply move in world space at the given speed in the given direction as if you stood stil the moment you shoot. I rarely encountered games which actually included the players speed. Though in most cases it just felt weird. A prime example where it’s actually used is when dropping bombs from an airplane like in battlefield. Though shooting rockets from a helicopter usually aren’t affected (technically they are propelled projectiles and not ballistic ones so it makes sense). Though grenades in Quake also did not take the player speed into account. It would be really weird when you strafe sideways and shoot a grenade and it doesn’t fly where you aim but drifts sideways. In reality that’s what would happen, not so in games :slight_smile:

Thanks, guys! That helps a lot! :smiley: