using ActionGameFramework.Projectiles;
using UnityEngine;
namespace ActionGameFramework.Helpers
{
///
/// Helper class to assist with calculation of common projectile ballistics problems.
///
public static class Ballistics
{
///
/// Calculates the initial velocity of a linear projectile aimed at a given world coordinate.
///
/// Starting point of the projectile.
/// Intended target point of the projectile.
/// Initial speed of the projectile.
/// Vector3 describing initial velocity for this projectile. Vector3.zero if no solution.
public static Vector3 CalculateLinearFireVector(Vector3 firePosition, Vector3 targetPosition,
float launchSpeed)
{
// If we're starting with a zero initial velocity, we give the vector a tiny base magnitude
if (Mathf.Abs(launchSpeed) < float.Epsilon)
{
launchSpeed = 0.001f;
}
return (targetPosition - firePosition).normalized * launchSpeed;
}
///
/// Calculates the time taken for a linear projectile to reach the specified destination, with a given
/// start speed and acceleration.
///
/// Starting point of the projectile.
/// Intended target point of the projectile.
/// Initial speed of the projectile.
/// Post-firing acceleration of the projectile.
/// Time in seconds to complete flight to target.
public static float CalculateLinearFlightTime(Vector3 firePosition, Vector3 targetPosition,
float launchSpeed, float acceleration)
{
float flightDistance = (targetPosition - firePosition).magnitude;
// v^2 = u^2 + 2as
float endV = Mathf.Sqrt((launchSpeed * launchSpeed) + (2 * acceleration * flightDistance));
// t = 2s/(u+v)
return (2f * flightDistance) / (launchSpeed + endV);
}
///
/// Calculates a leading target point that ensures a linear projectile will impact a moving target.
/// Assumes target has constant velocity. Precision can be adjusted parametrically.
///
/// Starting point of the projectile.
/// The current position of the intended target.
/// Vector representing the velocity of the intended target.
/// Initial speed of the projectile.
/// Post-firing acceleration of the projectile.
/// Number of iterations to approximate the correct position. Higher precision is better for faster targets.
/// Vector3 representing the leading target point.
public static Vector3 CalculateLinearLeadingTargetPoint(Vector3 firePosition, Vector3 targetPosition,
Vector3 targetVelocity, float launchSpeed, float acceleration,
int precision = 2)
{
// No precision means no leading, so we early-out.
if (precision <= 0)
{
return targetPosition;
}
Vector3 testPosition = targetPosition;
for (int i = 0; i < precision; i++)
{
float impactTime = CalculateLinearFlightTime(firePosition, testPosition, launchSpeed,
acceleration);
testPosition = targetPosition + (targetVelocity * impactTime);
}
return testPosition;
}
///
/// Calculates the launch velocity for a parabolic-path projectile to hit a given target point when fired
/// at a given angle.
///
/// Position from which the projectile is fired.
/// Intended target position.
/// Angle at which the projectile is to be fired.
/// Gravitational constant (Vertical only. Positive = down)
/// Vector3 representing launch velocity to hit the target. Vector3.zero if no solution.
public static Vector3 CalculateBallisticFireVectorFromAngle(Vector3 firePosition, Vector3 targetPosition,
float launchAngle, float gravity)
{
Vector3 target = targetPosition;
target.y = firePosition.y;
Vector3 toTarget = target - firePosition;
float targetDistance = toTarget.magnitude;
float shootingAngle = launchAngle;
float relativeY = firePosition.y - targetPosition.y;
float theta = Mathf.Deg2Rad * shootingAngle;
float cosTheta = Mathf.Cos(theta);
float num = targetDistance * Mathf.Sqrt(gravity) * Mathf.Sqrt(1 / cosTheta);
float denom = Mathf.Sqrt((2 * targetDistance * Mathf.Sin(theta)) + (2 * relativeY * cosTheta));
if (denom > 0)
{
float v = num / denom;
// Flatten aim vector so we can rotate it
Vector3 aimVector = toTarget / targetDistance;
aimVector.y = 0;
Vector3 rotAxis = Vector3.Cross(aimVector, Vector3.up);
Quaternion rotation = Quaternion.AngleAxis(shootingAngle, rotAxis);
aimVector = rotation * aimVector.normalized;
return aimVector * v;
}
return Vector3.zero;
}
///
/// Calculates the launch velocity for a parabolic-path projectile to hit a given target point when fired
/// at a given angle. Uses vertical gravity constant defined in project Physics settings.
///
/// Position from which the projectile is fired.
/// Intended target position.
/// Angle at which the projectile is to be fired.
/// Vector3 representing launch velocity to hit the target. Vector3.zero if no solution.
public static Vector3 CalculateBallisticFireVectorFromAngle(Vector3 firePosition, Vector3 targetPosition,
float launchAngle)
{
return CalculateBallisticFireVectorFromAngle(firePosition, targetPosition, launchAngle,
Mathf.Abs(Physics.gravity.y));
}
///
/// Calculates the launch velocity for a parabolic-path projectile to hit a given target point when
/// fired at a given speed.
///
/// Position from which the projectile is fired.
/// Intended target position.
/// The speed that the projectile is launched at.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// Gravitational constant (Vertical only. Positive = down)
/// Vector3 representing launch launchSpeed to hit the target. Vector3.zero if no solution.
public static Vector3 CalculateBallisticFireVectorFromVelocity(Vector3 firePosition, Vector3 targetPosition,
float launchSpeed, BallisticArcHeight arcHeight,
float gravity)
{
float theta = CalculateBallisticFireAngle(firePosition, targetPosition, launchSpeed, arcHeight, gravity);
// If our angle is impossible, we early-out.
if (float.IsNaN(theta))
{
return Vector3.zero;
}
Vector3 target = targetPosition;
target.y = firePosition.y;
Vector3 toTarget = target - firePosition;
float targetDistance = toTarget.magnitude;
Vector3 aimVector = Vector3.forward;
if (targetDistance > 0f)
{
// Flatten aim vector so we can rotate it
aimVector = toTarget / targetDistance;
aimVector.y = 0;
}
Vector3 rotAxis = Vector3.Cross(aimVector, Vector3.up);
Quaternion rotation = Quaternion.AngleAxis(theta, rotAxis);
aimVector = rotation * aimVector.normalized;
return aimVector * launchSpeed;
}
///
/// Calculates the launch velocity for a parabolic-path projectile to hit a given target point when
/// fired at a given speed. Uses vertical gravity constant defined in project Physics settings.
///
/// Position from which the projectile is fired.
/// Intended target position.
/// The speed that the projectile is launched at.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// Vector3 representing launch launchSpeed to hit the target. Vector3.zero if no solution.
public static Vector3 CalculateBallisticFireVectorFromVelocity(Vector3 firePosition, Vector3 targetPosition,
float launchSpeed, BallisticArcHeight arcHeight)
{
return CalculateBallisticFireVectorFromVelocity(firePosition, targetPosition, launchSpeed, arcHeight,
Mathf.Abs(Physics.gravity.y));
}
///
/// Calculates the angle at which a projectile with a given initial speed must be fired to impact a target.
///
/// Position from which the projectile is fired
/// Intended target position.
/// The speed that the projectile is launched at.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// Gravitational constant (Vertical only. Positive = down)
/// The required launch angle in degrees. NaN if no valid solution.
public static float CalculateBallisticFireAngle(Vector3 firePosition, Vector3 targetPosition,
float launchSpeed, BallisticArcHeight arcHeight, float gravity)
{
Vector3 target = targetPosition;
target.y = firePosition.y;
Vector3 toTarget = target - firePosition;
float targetDistance = toTarget.magnitude;
float relativeY = targetPosition.y - firePosition.y;
float vSquared = launchSpeed * launchSpeed;
// If the distance to our target is zero, we can assume it's right on top of us (or that we're our own target).
if (Mathf.Approximately(targetDistance, 0f))
{
// If we're preferring a high-angle shot, we just fire straight up.
if (arcHeight == BallisticArcHeight.UseHigh || arcHeight == BallisticArcHeight.PreferHigh)
{
return 90f;
}
// If we're doing a low-angle direct shot, we tweak our angle based on relative height of target.
if (relativeY > 0)
{
return 90f;
}
if (relativeY < 0)
{
return -90f;
}
}
float b = Mathf.Sqrt((vSquared * vSquared) -
(gravity * ((gravity * (targetDistance * targetDistance)) + (2 * relativeY * vSquared))));
// The "underarm", parabolic arc angle
float theta1 = Mathf.Atan((vSquared + b) / (gravity * targetDistance));
// The "overarm", direct arc angle
float theta2 = Mathf.Atan((vSquared - b) / (gravity * targetDistance));
bool theta1Nan = float.IsNaN(theta1);
bool theta2Nan = float.IsNaN(theta2);
// If both are invalid, we early-out with a NaN to indicate no solution.
if (theta1Nan && theta2Nan)
{
return float.NaN;
}
// We'll init with the parabolic arc.
float returnTheta = theta1;
// If we want to return the direct arc
if (arcHeight == BallisticArcHeight.UseLow)
{
returnTheta = theta2;
}
// If we want to return theta1 wherever valid, but will settle for theta2 if theta1 is invalid
if (arcHeight == BallisticArcHeight.PreferHigh)
{
returnTheta = theta1Nan ? theta2 : theta1;
}
// If we want to return theta2 wherever valid, but will settle for theta1 if theta2 is invalid
if (arcHeight == BallisticArcHeight.PreferLow)
{
returnTheta = theta2Nan ? theta1 : theta2;
}
return returnTheta * Mathf.Rad2Deg;
}
///
/// Calculates the angle at which a projectile with a given initial speed must be fired to impact a target.
/// Uses vertical gravity constant defined in project Physics settings.
///
/// Position from which the projectile is fired
/// Intended target position.
/// The speed that the projectile is launched at.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// The required launch angle in degrees. NaN if no valid solution.
public static float CalculateBallisticFireAngle(Vector3 firePosition, Vector3 targetPosition,
float launchSpeed, BallisticArcHeight arcHeight)
{
return CalculateBallisticFireAngle(firePosition, targetPosition, launchSpeed, arcHeight,
Mathf.Abs(Physics.gravity.y));
}
///
/// Calculates the amount of time it will take a projectile to complete its arc.
///
/// Position from which the projectile is fired
/// Intended target position.
/// The speed that the projectile is launched at.
/// The angle in degrees that the projectile was fired at.
/// Gravitational constant (Vertical only. Positive = down)
/// Time in seconds to complete arc to target. NaN if no valid solution.
public static float CalculateBallisticFlightTime(Vector3 firePosition, Vector3 targetPosition, float launchSpeed,
float fireAngle, float gravity)
{
float relativeY = firePosition.y - targetPosition.y;
Vector3 targetVector = targetPosition - firePosition;
targetVector.y = 0;
float targetDistance = targetVector.magnitude;
fireAngle *= Mathf.Deg2Rad;
float sinFireAngle = Mathf.Sin(fireAngle);
float a = (launchSpeed * Mathf.Sin(fireAngle)) / gravity;
float b = Mathf.Sqrt((launchSpeed * launchSpeed * (sinFireAngle * sinFireAngle)) + (2 * gravity * relativeY)) /
gravity;
float flightTime1 = a + b;
float flightTime2 = a - b;
float flightDistance1 = launchSpeed * Mathf.Cos(fireAngle) * flightTime1;
float flightDistance2 = launchSpeed * Mathf.Cos(fireAngle) * flightTime2;
if (flightTime2 > 0)
{
if (Mathf.Abs(targetDistance - flightDistance2) < Mathf.Abs(targetDistance - flightDistance1))
{
return flightTime2;
}
}
return flightTime1;
}
///
/// Calculates the amount of time it will take a projectile to complete its arc.
/// Uses vertical gravity constant defined in project Physics settings.
///
/// Position from which the projectile is fired
/// Intended target position.
/// The speed that the projectile is launched at.
/// The angle in degrees that the projectile was fired at.
/// Time in seconds to complete arc to target. NaN if no valid solution.
public static float CalculateBallisticFlightTime(Vector3 firePosition, Vector3 targetPosition,
float launchSpeed, float fireAngle)
{
return CalculateBallisticFlightTime(firePosition, targetPosition, launchSpeed, fireAngle,
Mathf.Abs(Physics.gravity.y));
}
///
/// Calculates an approximate leading target point to ensure a ballistic projectile will impact a moving target assuming a given launch speed.
/// Assumes constant target velocity and constant projectile speed after launch. Precision can be adjusted parametrically.
///
/// Starting point of the projectile.
/// The current position of the intended target.
/// Vector representing the velocity of the intended target.
/// Initial speed of the projectile.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// Number of iterations to approximate the correct position. Higher precision is better for faster targets.
/// Gravitational constant (Vertical only. Positive = down)
/// Vector3 representing the leading target point. Vector3.zero if no solution.
public static Vector3 CalculateBallisticLeadingTargetPointWithSpeed(Vector3 firePosition, Vector3 targetPosition,
Vector3 targetVelocity, float launchSpeed,
BallisticArcHeight arcHeight, float gravity,
int precision = 2)
{
// No precision means no leading, so we early-out.
if (precision <= 1)
{
return targetPosition;
}
Vector3 testPosition = targetPosition;
for (int i = 0; i < precision; i++)
{
float fireAngle = CalculateBallisticFireAngle(firePosition, testPosition, launchSpeed, arcHeight, gravity);
float impactTime = CalculateBallisticFlightTime(firePosition, testPosition, launchSpeed, fireAngle, gravity);
if (float.IsNaN(fireAngle) || float.IsNaN(impactTime))
{
return Vector3.zero;
}
testPosition = targetPosition + (targetVelocity * impactTime);
}
return testPosition;
}
///
/// Calculates an approximate leading target point to ensure a ballistic projectile will impact a moving target assuming a given launch speed.
/// Assumes constant target velocity and constant projectile speed after launch. Precision can be adjusted parametrically.
/// Uses vertical gravity constant defined in project Physics settings.
///
/// Starting point of the projectile.
/// The current position of the intended target.
/// Vector representing the velocity of the intended target.
/// Initial speed of the projectile.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// Number of iterations to approximate the correct position. Higher precision is better for faster targets.
/// Vector3 representing the leading target point. Vector3.zero if no solution.
public static Vector3 CalculateBallisticLeadingTargetPointWithSpeed(Vector3 firePosition, Vector3 targetPosition,
Vector3 targetVelocity, float launchSpeed,
BallisticArcHeight arcHeight, int precision = 2)
{
return CalculateBallisticLeadingTargetPointWithSpeed(firePosition, targetPosition, targetVelocity, launchSpeed,
arcHeight, Mathf.Abs(Physics.gravity.y), precision);
}
///
/// Calculates an approximate leading target point to ensure a ballistic projectile will impact a moving target assuming a given launch angle.
/// Assumes constant target velocity and constant projectile speed after launch. Precision can be adjusted parametrically.
/// Uses vertical gravity constant defined in project Physics settings.
///
/// Starting point of the projectile.
/// The current position of the intended target.
/// Vector representing the velocity of the intended target.
/// The angle at which the projectile is to be launched.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// Gravitational constant (Vertical only. Positive = down)
/// Number of iterations to approximate the correct position. Higher precision is better for faster targets.
/// Vector3 representing the leading target point. Vector3.zero if no solution.
public static Vector3 CalculateBallisticLeadingTargetPointWithAngle(Vector3 firePosition,
Vector3 targetPosition,
Vector3 targetVelocity, float launchAngle,
BallisticArcHeight arcHeight, float gravity,
int precision = 2)
{
// No precision means no leading, so we early-out.
if (precision <= 1)
{
return targetPosition;
}
Vector3 testPosition = targetPosition;
for (int i = 0; i < precision; i++)
{
float launchSpeed = CalculateBallisticFireVectorFromAngle(firePosition, testPosition, launchAngle, gravity)
.magnitude;
float impactTime = CalculateBallisticFlightTime(firePosition, testPosition, launchSpeed, launchAngle, gravity);
if (float.IsNaN(launchSpeed) || float.IsNaN(impactTime))
{
return Vector3.zero;
}
testPosition = targetPosition + (targetVelocity * impactTime);
}
return testPosition;
}
///
/// Calculates an approximate leading target point to ensure a ballistic projectile will impact a moving target assuming a given launch angle.
/// Assumes constant target velocity and constant projectile speed after launch. Precision can be adjusted parametrically.
/// Uses vertical gravity constant defined in project Physics settings.
///
/// Starting point of the projectile.
/// The current position of the intended target.
/// Vector representing the velocity of the intended target.
/// The angle at which the projectile is to be launched.
/// Preference between parabolic ("underhand") or direct ("overhand") projectile arc.
/// Number of iterations to approximate the correct position. Higher precision is better for faster targets.
/// Vector3 representing the leading target point. Vector3.zero if no solution.
public static Vector3 CalculateBallisticLeadingTargetPointWithAngle(Vector3 firePosition,
Vector3 targetPosition,
Vector3 targetVelocity, float launchAngle,
BallisticArcHeight arcHeight, int precision = 2)
{
return CalculateBallisticLeadingTargetPointWithAngle(firePosition, targetPosition, targetVelocity,
launchAngle, arcHeight, Mathf.Abs(Physics.gravity.y),
precision);
}
}
}