A few months ago, I was working out a few basic formulas for trajectory.
I eventually settled on using a Trajectory class with static functions to determine the necessary launch vector to reach the destination based on a specified speed, (2D) angle, or time.
In addition to those static functions, however, I also created a second set, which populates the attributes of a trajectory assigned to an object in order to display a preview line of where the object will go under any circumstances.
Two issues I wasn’t able to account for in my experimentation were:
-
factoring in drag and still reaching a target destination accurately and…
-
varying gravitational pull and still reaching the target accurately.
However, the preview paths drawn are always perfectly accurate, assuming no collisions occur outside the center of the approximation.
I’m afraid I won’t be sharing absolutely everything in this response, but just to get a few notes out there regarding PhysX’ implementation of various physical elements:
rigidbodyDrag = Mathf.Clamp01(1.0f - (rb.drag * Time.fixedDeltaTime)); // Per FixedUpdate()
// This means that if your drag is equal to the framerate of FixedUpdate(), your object will lose 100% of its speed every frame
velocityPerFrame = lastFrameVelocity + (Physics.gravity * Time.fixedDeltaTime);
velocityPerFrame *= rigidbodyDrag;
positionPerFrame += (velocityPerFrame * Time.fixedDeltaTime);
Those are a few key elements involved in accurately determining your position per frame.
However, I’ll leave that to you and cut to the chase for now: Calculating launch trajectory in the first place.
The basis for these trajectory calculations is to go from Vector A to Vector B, with any height difference, or as close as it can get, based on each of three parameters.
First, A → B, arriving after a specified length of time:
public static Vector3 HitTargetAtTime(Vector3 startPosition, Vector3 targetPosition, Vector3 gravityBase, float timeToTarget)
{
Vector3 AtoB = targetPosition - startPosition;
Vector3 horizontal = GetHorizontalVector(AtoB, gravityBase);
float horizontalDistance = horizontal.magnitude;
Vector3 vertical = GetVerticalVector(AtoB, gravityBase);
float verticalDistance = vertical.magnitude * Mathf.Sign(Vector3.Dot(vertical, -gravityBase));
float horizontalSpeed = horizontalDistance / timeToTarget;
float verticalSpeed = (verticalDistance + ((0.5f * gravityBase.magnitude) * (timeToTarget * timeToTarget))) / timeToTarget;
Vector3 launch = (horizontal.normalized * horizontalSpeed) - (gravityBase.normalized * verticalSpeed);
return launch;
}
Second, A → B, attempting to arrive based on a specified launch angle relative to the direction of gravity:
public static Vector3 HitTargetByAngle(Vector3 startPosition, Vector3 targetPosition, Vector3 gravityBase, float limitAngle)
{
if(limitAngle >= 90f || limitAngle <= -90f)
{
return Vector3.zero;
}
Vector3 AtoB = targetPosition - startPosition;
Vector3 horizontal = GetHorizontalVector(AtoB, gravityBase);
float horizontalDistance = horizontal.magnitude;
Vector3 vertical = GetVerticalVector(AtoB, gravityBase);
float verticalDistance = vertical.magnitude * Mathf.Sign(Vector3.Dot(vertical, -gravityBase));
float radAngle = Mathf.Deg2Rad * limitAngle;
float angleX = Mathf.Cos(radAngle);
float angleY = Mathf.Sin(radAngle);
float gravityMag = gravityBase.magnitude;
if(verticalDistance / horizontalDistance > angleY / angleX)
{
return Vector3.zero;
}
float destSpeed = (1 / Mathf.Cos(radAngle)) * Mathf.Sqrt((0.5f * gravityMag * horizontalDistance * horizontalDistance) / ((horizontalDistance * Mathf.Tan(radAngle)) - verticalDistance));
Vector3 launch = ((horizontal.normalized * angleX) - (gravityBase.normalized * angleY)) * destSpeed;
return launch;
}
Third, A → B, attempting to arrive based on a specified launch speed:
public static Vector3[] HitTargetBySpeed(Vector3 startPosition, Vector3 targetPosition, Vector3 gravityBase, float launchSpeed)
{
Vector3 AtoB = targetPosition - startPosition;
Vector3 horizontal = GetHorizontalVector(AtoB, gravityBase);
float horizontalDistance = horizontal.magnitude;
Vector3 vertical = GetVerticalVector(AtoB, gravityBase);
float verticalDistance = vertical.magnitude * Mathf.Sign(Vector3.Dot(vertical, -gravityBase));
float x2 = horizontalDistance * horizontalDistance;
float v2 = launchSpeed * launchSpeed;
float v4 = launchSpeed * launchSpeed * launchSpeed * launchSpeed;
float gravMag = gravityBase.magnitude;
float launchTest = v4 - (gravMag * ((gravMag * x2) + (2 * verticalDistance * v2)));
Vector3[] launch = new Vector3[2];
if(launchTest < 0)
{
launch[0] = (horizontal.normalized * launchSpeed * Mathf.Cos(45.0f * Mathf.Deg2Rad)) - (gravityBase.normalized * launchSpeed * Mathf.Sin(45.0f * Mathf.Deg2Rad));
launch[1] = (horizontal.normalized * launchSpeed * Mathf.Cos(45.0f * Mathf.Deg2Rad)) - (gravityBase.normalized * launchSpeed * Mathf.Sin(45.0f * Mathf.Deg2Rad));
}
else
{
float[] tanAngle = new float[2];
tanAngle[0] = (v2 - Mathf.Sqrt(v4 - gravMag * ((gravMag * x2) + (2 * verticalDistance * v2)))) / (gravMag * horizontalDistance);
tanAngle[1] = (v2 + Mathf.Sqrt(v4 - gravMag * ((gravMag * x2) + (2 * verticalDistance * v2)))) / (gravMag * horizontalDistance);
float[] finalAngle = new float[2];
finalAngle[0] = Mathf.Atan(tanAngle[0]);
finalAngle[1] = Mathf.Atan(tanAngle[1]);
launch[0] = (horizontal.normalized * launchSpeed * Mathf.Cos(finalAngle[0])) - (gravityBase.normalized * launchSpeed * Mathf.Sin(finalAngle[0]));
launch[1] = (horizontal.normalized * launchSpeed * Mathf.Cos(finalAngle[1])) - (gravityBase.normalized * launchSpeed * Mathf.Sin(finalAngle[1]));
}
return launch;
}
And, the associated helper functions:
public static Vector3 GetHorizontalVector(Vector3 AtoB, Vector3 gravityBase)
{
Vector3 output;
Vector3 perpendicular = Vector3.Cross(AtoB, gravityBase);
perpendicular = Vector3.Cross(gravityBase, perpendicular);
output = Vector3.Project(AtoB, perpendicular);
return output;
}
public static Vector3 GetVerticalVector(Vector3 AtoB, Vector3 gravityBase)
{
Vector3 output;
output = Vector3.Project(AtoB, gravityBase);
return output;
}
Once you have the launch vector calculated, you can make use of the basic velocity and drag formulas addressed above and calculate where the object will be located at various points in time during its flight. The time to destination can be calculated for each (and, obviously, is the condition for the first), therefore, in order to determine how many calculations are necessary to create an accurate path:
// Time
totalTime = timeToTarget;
// Angle
totalTime = horizontalDistance / (angleX * destSpeed);
// Speed
totalTime = horizontalDistance / (launchSpeed * Mathf.Cos(finalAngle));
calculations = (int)(totalTime / Time.fixedDeltaTime);
This will determine how many calculations of the velocity/position/drag updates will be necessary in order to display a perfectly accurate path based on the launch vector and total time estimate.
Aaaaaaanyway, I hope this gets you started in the direction you’d like to go, and I really wish I’d had some of this information on hand sooner when I first started working at this!
Edit [2/20/2023]: Cleaned up erroneous text filter ($$anonymous$$) and cached degree-to-radian angle multiplication(s)