Linear constant speed Projectiles

Predicting a targets position when firing a linear projectile

Overview

Ranged AI are common in games, and in an effort to make it a more enjoyable player experience we want them to predict the players movement (making it believable) but not hitscan so the player has some chance for outplay.

This post is based off the following GDC talk

The project can be found here: Github link

The GDC talk does an exceptional job of explaining the details, which I may re-explain and recount later but for now here a UE implementation of the logic talked about for predicting a linear projectile.

.h

// Given a constant speed, and a targets movement, determine how far to lead the target in order to hit it
bool PredictTargetLocation(const float ProjectileSpeed, FVector& OutPredictedLocation);

.cpp

/*
	Algorithm Summary:
		Based off (https://shellsphinx.github.io/images/PredictableProjectiles.pdf)

		Find the targets future position, at time (t), given:
			- projectile constant velocity
			- targets current instantaneous velocity (will account for average of history later) 
			and position

		Equation for target's movement
			X'	= Targets future position
			Xot = Targets current position (origin)
			Vt	= Targets instantaneous velocity
			t	= time

			Linear equation:
				X' = Xot + (Vt * t)

		Equation for projectile's movement
			X'	= Projectiles future position
			Xop	= Projectiles current position (origin)
			Sp	= Projectiles constant speed
			t	= time

			Linear equation:
				X' = Xop + (Sp * t)  
			Or better the equation of a circle
				(X' - Xop)^2 = (Sp * t)^2

		Both equations have X' and t.
		So combine via X' and solve for t which gives us a quadratic that we can solve 
		to get polynomial expansion: a^2 + b + c = 0
			
		a = (Vt^2 - Sp^2)
		b = (2 * (Xot - Xop) * Vt)
		c = (Xot - Xop)^2	

		then just solve the quadratic which gives (t) and plug back into 
		Equation for target's movement to find X'

	Note: Any vector multiplies are just dot products
*/
bool AZCLinearAI::PredictTargetLocation(const float ProjectileSpeed, FVector& OutPredictedLocation)
{	
	if (TargetChar == nullptr)
	{
		return false;
	}

	const FVector Xot = TargetChar->GetActorLocation();	// Targets current position
	const FVector Vt = TargetChar->GetVelocity();		// Targets current velocity (speed * direction)

	const float Sp = ProjectileSpeed;			// Projectile constant speed
	const FVector Xop = GetActorLocation();			// Projectile current position (spawed at AI's position)

	// a = (Vt^2 - Sp^2)
	float a = FVector::DotProduct(Vt, Vt) - (Sp * Sp);

	const FVector XotDiffXop = Xot - Xop;

	// b = (2 * (Xow - Xop) * Vw)
	float b = 2 * FVector::DotProduct(XotDiffXop, Vt);

	// c = (Xow - Xop)^2
	float c = FVector::DotProduct(XotDiffXop, XotDiffXop);

	float t = 0.f;

	// Solve for Time (t) using quadratic
	if (a == 0.f)
	{// divide by 0 early out
		t = -(c / b);
	}
	else
	{
		// determinant: b^2 - 4ac
		float det = (b * b) - (4 * a * c);
		if (det < 0)
		{// invalid, just use target current position
			OutPredictedLocation = TargetChar->GetActorLocation();
			return true;
		}
		else if (det == 0)
		{// t = -b/2a
			t = -b / (2 * a);
		}
		else
		{
			// (-b + sqrt(b^2 - 4ac)) / 2a
			float tPlus = (-b + FMath::Sqrt((b * b) - (4 * a * c))) / (2 * a);

			// (-b - sqrt(b^2 - 4ac)) / 2a
			float tMinus = (-b - FMath::Sqrt((b * b) - (4 * a * c))) / (2 * a);

			if (tPlus > 0 && tMinus > 0)
			{
				t = FMath::Min(tPlus, tMinus);
			}
			else if (tPlus > 0)
			{
				t = tPlus;
			}
			else if (tMinus > 0)
			{
				t = tMinus;
			}
			else
			{
				ensureMsgf(false, TEXT("This should be mathematically impossible for both solutions (if we got this far) to be invalid"));
				return false;
			}
		}
	}

	// Now that we have Time (t) plug it back into Equation for target's movement
	OutPredictedLocation = Xot + (Vt * t);
	return true;
}

Last updated