Introduction Trajectory Calculations
Every time a cannonball arcs across a battlefield, a basketball sails through the air toward a hoop, or a grenade bounces off a wall, a trajectory is being traced. In Unity, you can let the physics engine handle this for you attach a Rigidbody, apply a force, and watch it fly. But relying exclusively on simulation means you can never predict where a projectile will land before you fire it. You cannot draw a dotted arc in the scene. You cannot tell an AI turret the exact angle it needs to shoot in order to hit a moving player.
That is what this chapter is about: the mathematics that let you compute a projectile's entire journey in advance, without running a single physics simulation frame. Given an initial speed and a launch angle, you can analytically calculate:
- The exact launch velocity vector
- The maximum height the projectile will reach
- The horizontal range (how far it will travel before hitting the ground)
- The total flight time
- The projectile's position at any point in time during the flight
All of the formulas here assume a vacuum: no air resistance, drag set to zero. The Unity
physics engine applies drag by default, so remember to set the Rigidbody's drag to 0 if you
want your prediction to match reality perfectly.
Calculating the Launch Direction from an Angle
A projectile launched at angle θ above the horizontal with initial speed v₀ has two independent velocity components:
- Horizontal component Vₕ = v₀ × cos(θ)
- Vertical component Vᵥ = v₀ × sin(θ)
Gravity only affects the vertical component. The horizontal component stays constant for the entire flight (in a vacuum). In Unity's coordinate system, Y is up and Z is forward:
// angle is in degrees
float angleRad = angle * Mathf.Deg2Rad;
Vector3 launchVelocity = new Vector3(
0f,
initialSpeed * Mathf.Sin(angleRad), // vertical component
initialSpeed * Mathf.Cos(angleRad) // horizontal component along Z
);
This vector is expressed in local space. To fire in the actual world direction the object is facing, rotate it into world space:
launchVelocity = transform.rotation * launchVelocity;
At 0° the formula gives a purely horizontal velocity (flat shot). At 90° it gives purely vertical velocity (straight up). At 45° the two components are equal and as we will see, 45° is the angle that maximises range.
Calculating Maximum Height
The projectile reaches its maximum height when the vertical velocity reaches zero. Using the kinematic equation v² = u² − 2as:
- H = (v₀ × sin(θ))² / (2 × g)
float angleRad = angle * Mathf.Deg2Rad;
float vy = initialSpeed * Mathf.Sin(angleRad);
// Physics.gravity.y is negative (e.g. -9.81), so we negate it
float maxHeight = (vy * vy) / (2f * Mathf.Abs(Physics.gravity.y));
Always use Physics.gravity.y rather than hard-coding 9.81f. This way your
calculation automatically respects whatever gravity value the project designer has set.
Calculating the Range
The range is the horizontal distance the projectile travels before returning to the same altitude:
- R = (v₀² × sin(2θ)) / g
Notice the sin(2θ) term. This reaches its maximum value of 1 when θ = 45° so
45° is always the angle of maximum range on flat ground.
float range = (initialSpeed * initialSpeed * Mathf.Sin(2f * angle * Mathf.Deg2Rad))
/ Mathf.Abs(Physics.gravity.y);
Complementary Angles
Two launch angles that are complementary (add up to 90°) produce the same range. A 30° shot lands at the same horizontal distance as a 60° shot. This is why, when you calculate the reverse trajectory, you always get two solutions the low, direct trajectory and the high, arcing one.
Flight Time
- T = (2 × v₀ × sin(θ)) / g
float flightTime = (2f * initialSpeed * Mathf.Sin(angle * Mathf.Deg2Rad))
/ Mathf.Abs(Physics.gravity.y);
Flight time has many practical uses. Predictive AI can compute it to know exactly when a grenade will explode or when a player needs to jump in order to catch a thrown object.
Calculating Points Along the Trajectory
The parametric equations of motion give you the position at any time t after launch:
- x(t) = Vₕ × t (horizontal constant velocity)
- y(t) = Vᵥ × t − ½ × g × t² (vertical accelerating downward)
Vector3 GetPositionAtTime(Vector3 origin, Vector3 launchVelocity, float t)
{
float g = Mathf.Abs(Physics.gravity.y);
return origin
+ new Vector3(launchVelocity.x, 0f, launchVelocity.z) * t // horizontal
+ Vector3.up * (launchVelocity.y * t - 0.5f * g * t * t); // vertical
}
Vector3[] ComputeTrajectoryPoints(Vector3 origin, Vector3 launchVelocity, int resolution)
{
float flightTime = ComputeFlightTime(launchVelocity);
float dt = flightTime / (resolution - 1);
Vector3[] points = new Vector3[resolution];
for (int i = 0; i < resolution; i++)
{
float t = i * dt;
points[i] = GetPositionAtTime(origin, launchVelocity, t);
}
return points;
}
float ComputeFlightTime(Vector3 launchVelocity)
{
float vy = launchVelocity.y;
float g = Mathf.Abs(Physics.gravity.y);
return (2f * vy) / g;
}
Displaying the Trajectory
Unity's LineRenderer component is the standard way to draw a 3D polyline in the scene. Once you have your array of trajectory points, displaying them is a few lines of code.
using UnityEngine;
[RequireComponent(typeof(LineRenderer))]
public class TrajectoryPreview : MonoBehaviour
{
[Header("Launch Settings")]
public float initialSpeed = 15f;
public float launchAngle = 45f;
public int resolution = 40;
private LineRenderer lineRenderer;
void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
}
void Update()
{
UpdatePreview();
}
void UpdatePreview()
{
float angleRad = launchAngle * Mathf.Deg2Rad;
Vector3 localVelocity = new Vector3(
0f,
initialSpeed * Mathf.Sin(angleRad),
initialSpeed * Mathf.Cos(angleRad)
);
Vector3 launchVelocity = transform.rotation * localVelocity;
Vector3[] points = ComputeTrajectoryPoints(transform.position, launchVelocity, resolution);
lineRenderer.positionCount = points.Length;
lineRenderer.SetPositions(points);
}
Vector3[] ComputeTrajectoryPoints(Vector3 origin, Vector3 launchVelocity, int count)
{
float vy = launchVelocity.y;
float g = Mathf.Abs(Physics.gravity.y);
float flightTime = (2f * vy) / g;
float dt = flightTime / (count - 1);
Vector3[] pts = new Vector3[count];
for (int i = 0; i < count; i++)
{
float t = i * dt;
pts[i] = origin
+ new Vector3(launchVelocity.x, 0f, launchVelocity.z) * t
+ Vector3.up * (launchVelocity.y * t - 0.5f * g * t * t);
}
return pts;
}
}
Testing with a Real Projectile
using UnityEngine;
public class ProjectileLauncher : MonoBehaviour
{
public GameObject projectilePrefab;
public float initialSpeed = 15f;
public float launchAngle = 45f;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
}
void Fire()
{
float angleRad = launchAngle * Mathf.Deg2Rad;
Vector3 localVelocity = new Vector3(
0f,
initialSpeed * Mathf.Sin(angleRad),
initialSpeed * Mathf.Cos(angleRad)
);
Vector3 launchVelocity = transform.rotation * localVelocity;
GameObject proj = Instantiate(projectilePrefab, transform.position, Quaternion.identity);
Rigidbody rb = proj.GetComponent<Rigidbody>();
// IMPORTANT: set drag to 0 for prediction to match simulation
rb.drag = 0f;
rb.angularDrag = 0f;
rb.velocity = launchVelocity;
}
}
Trajectory by Target Point Reverse Calculation
Given a target position and a fixed launch speed, calculate the angle that gets it there. Starting from the range formula and solving for θ:
- R = (v₀² × sin(2θ)) / g
- sin(2θ) = (g × R) / v₀²
- θ = ½ × Asin((g × R) / v₀²)
This gives the low-angle (direct) solution. There is also a high-angle (arcing) solution: θ_high = 90° − θ_low.
If (g × R) / v₀² > 1, the target is out of range no real solution exists. Always
check this before calling Asin, which would return NaN.
using UnityEngine;
public class TargetingSystem : MonoBehaviour
{
public float initialSpeed = 20f;
public bool ComputeLaunchAngles(Vector3 origin, Vector3 target,
out float lowAngle, out float highAngle)
{
lowAngle = 0f;
highAngle = 0f;
float g = Mathf.Abs(Physics.gravity.y);
Vector3 toTarget = target - origin;
float R = new Vector3(toTarget.x, 0f, toTarget.z).magnitude;
float sinTwoTheta = (g * R) / (initialSpeed * initialSpeed);
if (sinTwoTheta > 1f)
{
return false;
}
float twoTheta = Mathf.Asin(sinTwoTheta);
lowAngle = twoTheta * 0.5f * Mathf.Rad2Deg;
highAngle = (Mathf.PI * 0.5f - twoTheta * 0.5f) * Mathf.Rad2Deg;
return true;
}
public Vector3 ComputeLaunchVelocity(Vector3 origin, Vector3 target, float angleDeg)
{
Vector3 toTarget = target - origin;
Vector3 horizontalDir = new Vector3(toTarget.x, 0f, toTarget.z).normalized;
float angleRad = angleDeg * Mathf.Deg2Rad;
Vector3 velocity = horizontalDir * (initialSpeed * Mathf.Cos(angleRad))
+ Vector3.up * (initialSpeed * Mathf.Sin(angleRad));
return velocity;
}
}
Converting from ZY Plane to Full 3D
Vector3 Compute3DLaunchVelocity(Vector3 origin, Vector3 target, float angleDeg)
{
Vector3 delta = target - origin;
Vector3 horizontalDelta = new Vector3(delta.x, 0f, delta.z);
Vector3 horizontalDir = horizontalDelta.normalized;
float angleRad = angleDeg * Mathf.Deg2Rad;
float horizontalSpeed = initialSpeed * Mathf.Cos(angleRad);
float verticalSpeed = initialSpeed * Mathf.Sin(angleRad);
return horizontalDir * horizontalSpeed + Vector3.up * verticalSpeed;
}
Exercise: Tank Control
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class TankController : MonoBehaviour
{
[Header("Movement")]
public float moveForce = 800f;
public float turnTorque = 400f;
public float maxSpeed = 10f;
[Header("Cannon")]
public Transform cannonPivot;
public float cannonElevationSpeed = 30f;
public float minElevation = 5f;
public float maxElevation = 80f;
private Rigidbody rb;
private float currentElevation = 45f;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void FixedUpdate()
{
HandleMovement();
HandleCannon();
}
void HandleMovement()
{
float forward = Input.GetAxis("Vertical");
float turn = Input.GetAxis("Horizontal");
if (rb.velocity.magnitude < maxSpeed)
rb.AddForce(transform.forward * forward * moveForce);
rb.AddTorque(transform.up * turn * turnTorque);
}
void HandleCannon()
{
float elevInput = Input.GetAxis("Fire2");
currentElevation = Mathf.Clamp(
currentElevation + elevInput * cannonElevationSpeed * Time.fixedDeltaTime,
minElevation,
maxElevation
);
cannonPivot.localRotation = Quaternion.Euler(-currentElevation, 0f, 0f);
}
}
Exercise: Camera Control
using UnityEngine;
public class TankCamera : MonoBehaviour
{
public Transform target;
public Vector3 offset = new Vector3(0f, 5f, -10f);
public float followSpeed = 5f;
public float rotationSpeed = 5f;
void LateUpdate()
{
if (target == null) return;
Vector3 desiredPosition = target.TransformPoint(offset);
transform.position = Vector3.Lerp(transform.position, desiredPosition,
followSpeed * Time.deltaTime);
Quaternion desiredRotation = Quaternion.LookRotation(target.position - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation,
rotationSpeed * Time.deltaTime);
}
}
Exercise: Turret with Trajectory Prediction
using UnityEngine;
[RequireComponent(typeof(LineRenderer))]
public class AITurret : MonoBehaviour
{
[Header("References")]
public Transform muzzle;
public GameObject projectilePrefab;
public Transform playerTank;
[Header("Settings")]
public float launchSpeed = 20f;
public bool preferHighAngle = false;
public int previewResolution = 40;
public float fireCooldown = 3f;
private LineRenderer lineRenderer;
private float cooldownTimer;
void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
}
void Update()
{
cooldownTimer -= Time.deltaTime;
if (playerTank == null) return;
float lowAngle, highAngle;
bool canHit = ComputeLaunchAngles(muzzle.position, playerTank.position,
out lowAngle, out highAngle);
if (!canHit)
{
lineRenderer.positionCount = 0;
return;
}
float chosenAngle = preferHighAngle ? highAngle : lowAngle;
Vector3 launchVelocity = Compute3DLaunchVelocity(
muzzle.position, playerTank.position, chosenAngle);
Vector3[] pts = ComputeTrajectoryPoints(muzzle.position, launchVelocity, previewResolution);
lineRenderer.positionCount = pts.Length;
lineRenderer.SetPositions(pts);
if (cooldownTimer <= 0f)
{
Fire(launchVelocity);
cooldownTimer = fireCooldown;
}
}
void Fire(Vector3 launchVelocity)
{
GameObject proj = Instantiate(projectilePrefab, muzzle.position, Quaternion.identity);
Rigidbody rb = proj.GetComponent<Rigidbody>();
rb.drag = 0f;
rb.velocity = launchVelocity;
}
bool ComputeLaunchAngles(Vector3 origin, Vector3 target,
out float lowAngle, out float highAngle)
{
lowAngle = 0f;
highAngle = 0f;
float g = Mathf.Abs(Physics.gravity.y);
Vector3 delta = target - origin;
float R = new Vector3(delta.x, 0f, delta.z).magnitude;
float sinVal = (g * R) / (launchSpeed * launchSpeed);
if (sinVal > 1f) return false;
float twoTheta = Mathf.Asin(sinVal);
lowAngle = twoTheta * 0.5f * Mathf.Rad2Deg;
highAngle = (Mathf.PI * 0.5f - twoTheta * 0.5f) * Mathf.Rad2Deg;
return true;
}
Vector3 Compute3DLaunchVelocity(Vector3 origin, Vector3 target, float angleDeg)
{
Vector3 delta = target - origin;
Vector3 hDir = new Vector3(delta.x, 0f, delta.z).normalized;
float rad = angleDeg * Mathf.Deg2Rad;
return hDir * (launchSpeed * Mathf.Cos(rad))
+ Vector3.up * (launchSpeed * Mathf.Sin(rad));
}
Vector3[] ComputeTrajectoryPoints(Vector3 origin, Vector3 vel, int count)
{
float g = Mathf.Abs(Physics.gravity.y);
float flightTime = (2f * vel.y) / g;
float dt = flightTime / (count - 1);
Vector3[] pts = new Vector3[count];
for (int i = 0; i < count; i++)
{
float t = i * dt;
pts[i] = origin
+ new Vector3(vel.x, 0f, vel.z) * t
+ Vector3.up * (vel.y * t - 0.5f * g * t * t);
}
return pts;
}
}
Summary
- Decompose an angle and speed into a 3D launch velocity vector
- Calculate maximum height, range, and flight time analytically
- Generate an array of positions along the arc for a LineRenderer preview
- Launch a Rigidbody with the calculated velocity and verify it matches the prediction
- Reverse-calculate the angle needed to hit a target at a given distance
- Handle both the low-angle (direct) and high-angle (arcing) solutions
- Convert all of this into a full 3D targeting system
Need help with Unity3D development?
I'm a senior developer with 16+ years experience, including AAA projects at Ubisoft. Let's discuss how I can help with your game or interactive project.
Start a Conversation