// Copyright (c) 2022 Blobcreate & Ge Ge using UnityEngine; namespace Blobcreate.ProjectileToolkit { public static class Projectile { /// /// Computes the launch velocity by the given start point, end point, and coefficient /// a of the quadratic function f(x) = ax^2 + bx + c which determines the trajectory /// of the projectile motion. /// /// The a coefficient of the quadratic function f(x) = ax^2 + bx + c. /// It determines the shape and speed of the trajectory, for example, -0.2f makes the /// trajectory curvier and slower while -0.01f makes it straighter and faster. Should /// always be negative. public static Vector3 VelocityByA(Vector3 start, Vector3 end, float a) { var vec = end - start; var n = vec.y; vec.y = 0; var m = vec.magnitude; var b = n / m - m * a; var vx = Mathf.Sqrt(Physics.gravity.y / (2 * a)); // vy + g*m/vx = (2*a*m + b) * vx var vy = b * vx; var direction = vec / m; return new Vector3(direction.x * vx, vy, direction.z * vx); } /// /// Computes the launch velocity by the given start point, end point, and launch angle /// in degrees. /// /// The launch angle in degrees. 0 means launch /// horizontally. Should be from -90f (exclusive) to 90f (exclusive) and greater than /// the elevation angle formed by start to end. public static Vector3 VelocityByAngle(Vector3 start, Vector3 end, float elevationAngle) { var b = Mathf.Tan(Mathf.Deg2Rad * elevationAngle); var vec = end - start; var n = vec.y; vec.y = 0; var m = vec.magnitude; var a = (n / m - b) / m; var vx = Mathf.Sqrt(Physics.gravity.y / (2 * a)); var vy = b * vx; var direction = vec / m; return new Vector3(direction.x * vx, vy, direction.z * vx); } /// /// Computes the launch velocity by the given start point, end point, and time in /// seconds the projectile flies from start to end. The projectile object will be /// exactly at the end point time seconds after launch. /// /// The time in seconds you want the projectile to fly from start /// to end. public static Vector3 VelocityByTime(Vector3 start, Vector3 end, float time) { return new Vector3( (end.x - start.x) / time, (end.y - start.y) / time - 0.5f * Physics.gravity.y * time, (end.z - start.z) / time); } /// /// Computes the launch velocity by the given start point, end point, and max height /// of the projectile motion. /// /// The height measured from the end point (for example, /// 1f means the max height of the trajectory is 1 meter above the end point). The /// algorithm automatically clamps the value if it is lower than the y value of /// start or end. public static Vector3 VelocityByHeight(Vector3 start, Vector3 end, float heightFromEnd) { var h = end.y + heightFromEnd - start.y; if (h < 0) { h = 0; heightFromEnd = start.y - end.y; } var time = Mathf.Sqrt(-2 * h / Physics.gravity.y) + Mathf.Sqrt(-2 * heightFromEnd / Physics.gravity.y); return VelocityByTime(start, end, time); } /// /// Computes the two angle results by the given start point, end point, and launch /// speed. Returns false if out of reach. /// /// The launch speed of the projectile object. /// The lower angle that satisfies the conditions, or 0 if the /// method returns false. /// The higher angle that satisfies the conditions, or 0 if the /// method returns false. public static bool AnglesBySpeed(Vector3 start, Vector3 end, float speed, out float lowAngle, out float highAngle) { var vec = end - start; var n = vec.y; vec.y = 0; var m = vec.magnitude; // Note that the b and c here are of the quadratic equation that calculates // the b value of the quadratic function of the projectile motion. var b = (2 * speed * speed) / (m * Physics.gravity.y); var c = -(b * n / m) + 1; var delta = b * b - 4 * c; if (delta < 0) { lowAngle = default; highAngle = default; return false; } var deltaRoot = Mathf.Sqrt(delta); lowAngle = Mathf.Atan((-b - deltaRoot) * 0.5f) * Mathf.Rad2Deg; highAngle = Mathf.Atan((-b + deltaRoot) * 0.5f) * Mathf.Rad2Deg; return true; } /// /// Computes the two velocity results by the given start point, end point, and launch /// speed. Returns false if out of reach. /// /// The launch speed of the projectile object. /// The lower-angle velocity that satisfies the conditions, /// or (0, 0, 0) if the method returns false. /// The higher-angle velocity that satisfies the conditions, /// or (0, 0, 0) if the method returns false. public static bool VelocitiesBySpeed(Vector3 start, Vector3 end, float speed, out Vector3 lowAngleV, out Vector3 highAngleV) { if (!AnglesBySpeed(start, end, speed, out var lowAngle, out var highAngle)) { lowAngleV = default; highAngleV = default; return false; } var dirXZ = end - start; dirXZ.y = 0; dirXZ.Normalize(); var right = Vector3.Cross(Vector3.down, dirXZ); var ro = Quaternion.AngleAxis(lowAngle, right); var dir = ro * dirXZ; lowAngleV = speed * dir; ro = Quaternion.AngleAxis(highAngle, right); dir = ro * dirXZ; highAngleV = speed * dir; return true; } /// /// Computes the position of the projectile at the given time counted from the moment /// the projectile is at origin. /// /// The time counted from the moment the projectile is at origin. /// Gravitational acceleration, equals the magnitude of /// gravity (normally equals Physics.gravity.y). public static Vector3 PositionAtTime(Vector3 origin, Vector3 originVelocity, float time, float gAcceleration) { var vy = originVelocity.y + time * gAcceleration; var py = 0.5f * time * (originVelocity.y + vy); var displacement = new Vector3(time * originVelocity.x, py, time * originVelocity.z); return origin + displacement; } /// /// Computes the trajectory points of the projectile and stores them into the buffer. /// /// To calculate the positions to how far, from origin and /// ignoring height. /// How many positions to calculate, including the origin and end. /// Gravitational acceleration, equals the magnitude of /// gravity (normally equals Physics.gravity.y). /// The buffer to store the calculated positions. public static void Positions(Vector3 origin, Vector3 originVelocity, float distance, int count, float gAcceleration, Vector3[] positions) { var vxz = originVelocity; vxz.y = 0; float timeInterval = distance / vxz.magnitude / (count - 1); var y = 0.5f * gAcceleration * timeInterval; positions[0] = origin; for (int i = 1; i < positions.Length; i++) { positions[i] = origin + i * timeInterval * new Vector3(originVelocity.x, originVelocity.y + i * y, originVelocity.z); } } // Will be available in the next release! PEB stands for physics-engine-based. // public static void PEBPositions(Vector3 origin, Vector3 originVelocity, int iterations, float gAcceleration, Vector3[] positions) // { // var vxz = originVelocity; // vxz.y = 0; // positions[0] = origin; // var prevPos = origin; // var vy = originVelocity.y; // for (int i = 1; i < positions.Length; i++) // { // vy += gAcceleration * Time.fixedDeltaTime; // positions[i] = prevPos + Time.fixedDeltaTime * // new Vector3(originVelocity.x, vy, originVelocity.z); // prevPos = positions[i]; // } // } /// /// Tests if a projectile at start can use the vertical velocity (y) of startVelocity /// to hit the elevation (y) of end, if true, outputs the time of flight based on the /// vertical speed. Horizontal speed is ignored. /// /// The velocity at the start point, or launch velocity. /// The time results that a projectile fly from start to /// end with the launch velocity startVelocity. public static bool VerticalFlightTest(Vector3 start, Vector3 end, Vector3 startVelocity, out Vector2 timesOfFlight) { timesOfFlight = new Vector2(-1f, -1f); var a = 0.5f * Physics.gravity.y; var b = startVelocity.y; var c = start.y - end.y; var delta = b * b - 4 * a * c; if (delta < 0f) return false; var ta = (-b + Mathf.Sqrt(delta)) / (2 * a); var tb = (-b - Mathf.Sqrt(delta)) / (2 * a); timesOfFlight = new Vector2(ta, tb); return true; } /// /// Tests if a projectile at start can use startVelocity to hit end, and outputs the /// time of flight. /// /// The velocity at the start point, or launch velocity. /// FlightTestMode (Enum). /// The time that a projectile fly from start to end with /// the launch velocity startVelocity. public static bool FlightTest(Vector3 start, Vector3 end, Vector3 startVelocity, FlightTestMode testMode, out float timeOfFlight) { var dXZ = end - start; var sqrDistance = dXZ.x * dXZ.x + dXZ.z * dXZ.z; var sqrSpeed = startVelocity.x * startVelocity.x + startVelocity.z * startVelocity.z; var testT = Mathf.Sqrt(sqrDistance / sqrSpeed); if (testMode == FlightTestMode.Horizontal) { timeOfFlight = testT; if (sqrSpeed == 0f) { if (sqrDistance == 0f) return true; else return false; } return true; } if (testMode == FlightTestMode.Both) { if (testT == float.NaN) testMode = FlightTestMode.VerticalB; else { var vy = startVelocity.y + testT * Physics.gravity.y; var py = 0.5f * testT * (startVelocity.y + vy) + start.y; timeOfFlight = testT; return Mathf.Abs(py - end.y) < 0.04f; } } if (testMode == FlightTestMode.VerticalB || testMode == FlightTestMode.VerticalA) { if (VerticalFlightTest(start, end, startVelocity, out var results)) { if (testMode == FlightTestMode.VerticalB) timeOfFlight = results.y; else timeOfFlight = results.x; return timeOfFlight >= 0f; } } timeOfFlight = -1f; return false; } /// /// Computes how far a projectile that uses the given speed at start can reach at the /// given elevation endElevation. Returns -1f if can't reach the elevation. /// /// The elevation (y) of the target point you want the /// projectile motion to hit or pass through. /// The launch speed of the projectile object. public static float ElevationalReach(Vector3 start, float endElevation, float speed) { var n = endElevation - start.y; var bm = (2 * speed * speed) / Physics.gravity.y; var invSqr = 4f / (bm * bm + 4f * bm * n); if (invSqr <= 0f) return -1f; return Mathf.Sqrt(1f / invSqr); } /// /// Computes how far a projectile that uses the given speed at start can reach at the /// given elevation endElevation, and outputs the corresponding launch angle. Returns /// -1f if can't reach the elevation. /// /// The elevation (y) of the target point you want the /// projectile motion to hit or pass through. /// The launch speed of the projectile object. /// The angle that satisfies the conditions. public static float ElevationalReach(Vector3 start, float endElevation, float speed, out float angle) { var n = endElevation - start.y; var bm = (2 * speed * speed) / Physics.gravity.y; var invSqr = 4f / (bm * bm + 4f * bm * n); if (invSqr <= 0f) { angle = default; return -1f; } var m = Mathf.Sqrt(1f / invSqr); var b = bm / m; angle = Mathf.Atan(-b * 0.5f) * Mathf.Rad2Deg; return m; } } public enum FlightTestMode { /// /// Calculates the time of flight based on the horizontal speed. Vertical speed is /// ignored. /// Horizontal, /// /// Calculates the time of flight that hit the elevation of the end point for the /// second time, based on the vertical speed. Horizontal speed is ignored. /// VerticalB, /// /// Calculates the time of flight that hit the elevation of the end point for the /// first time, based on the vertical speed. Horizontal speed is ignored. /// VerticalA, /// /// Tests the given velocity both horizontally and vertically, and outputs the time /// of flight if the velocity is correct. /// Both, //Strict // coming soon! } }