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); } } }