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. You cannot guarantee a satisfying parabola in your puzzle game.
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
You can also work backwards: given a target position and a fixed launch speed, calculate the angle needed to hit it. These are not approximations — they are exact, closed-form solutions derived from the equations of kinematics. Once you understand them, you will use them constantly.
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
The starting point for every trajectory calculation is turning a scalar angle and a scalar speed into a velocity vector. 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). This is the core insight of ballistic physics and it simplifies everything that follows.
In Unity's coordinate system, Y is up and Z is forward. A projectile fired straight ahead along the local Z axis at angle θ has the following velocity vector:
// 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 — it fires along the object's local Z axis. To fire in the actual world direction the object is facing, rotate it into world space using the Transform's rotation:
launchVelocity = transform.rotation * launchVelocity;
Now you have a world-space velocity vector ready to hand to a Rigidbody or to feed into the trajectory formulas below. The rotation step is crucial: without it, your turret would always fire along the world Z axis regardless of where it was pointing.
Quick Sanity Check
At 0° the formula gives a purely horizontal velocity (all Cos, zero Sin) — a flat shot. At 90° it gives purely vertical velocity (all Sin, zero Cos) — a shot straight up. At 45° the two components are equal — and as we will see shortly, 45° is the angle that maximises range. These checks confirm the formula is correct before you write a single line of Unity code.
Calculating Maximum Height
As a projectile climbs, gravity decelerates the vertical component. The projectile reaches its maximum height at the exact moment the vertical velocity reaches zero. Using the kinematic equation v² = u² − 2as and setting the final velocity to zero:
- H = (v₀ × sin(θ))² / (2 × g)
Where g is the magnitude of gravitational acceleration (9.81 m/s² in the real world, but Unity lets you customise it in Edit → Project Settings → Physics). In C#:
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 — including
reduced gravity for a Moon level or increased gravity for a heavy-world game.
Maximum height is particularly useful for determining whether a projectile will clear an obstacle. Before
you fire, compute maxHeight and compare it against the obstacle's height. If the projectile
cannot clear it, you can reject that angle and try another.
Calculating the Range
The range is the horizontal distance the projectile travels before returning to the same altitude from which it was launched. The classical formula is:
- R = (v₀² × sin(2θ)) / g
Notice the sin(2θ) term. This function reaches its maximum value of 1 when 2θ = 90°,
i.e., when θ = 45°. So 45° is always the angle of maximum range on flat ground. In C#:
float range = (initialSpeed * initialSpeed * Mathf.Sin(2f * angle * Mathf.Deg2Rad))
/ Mathf.Abs(Physics.gravity.y);
The range formula assumes the projectile lands at the same height as it was launched. If your target is higher or lower, you will need the full trajectory equations (see the section on position at time t below). However, for flat-ground artillery or simple projectile puzzles, this formula is exactly what you need.
Complementary Angles
An interesting property: 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. A 20° shot lands at the same spot as a 70° shot. This is why, when you calculate the reverse trajectory (angle to hit a target), you always get two solutions — the low, direct trajectory and the high, arcing one.
Flight Time
The total flight time is the duration from launch until the projectile returns to its original altitude. By symmetry, the time to reach maximum height equals the time to fall back down, so the total time is simply double the ascent time:
- T = (2 × v₀ × sin(θ)) / g
In C#:
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. A trap can be timed to activate when a projectile arrives. An animation can be synced to a projectile's arc. Whenever you need to coordinate something with a projectile's arrival, compute the flight time first.
Calculating Points Along the Trajectory
To draw a trajectory preview arc, you need the projectile's position at many moments during its flight. 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)
In three-dimensional Unity space, where the horizontal motion is along an arbitrary direction (not just the Z axis):
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
}
To generate an array of points covering the full arc, loop from t = 0 to t = flightTime in small increments:
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;
}
The resolution parameter controls how smooth the arc looks. For a preview arc drawn every
frame, 30–60 points gives a smooth curve without noticeable performance cost. For a one-time calculation
(e.g., checking for obstacles along the path), you can increase it to 100 or more.
Displaying the Trajectory
Unity's LineRenderer component is the standard way to draw a 3D polyline in the scene view and during gameplay. Once you have your array of trajectory points, displaying them is a few lines of code.
First, add a LineRenderer component to your launcher GameObject (or a dedicated child object). Configure it in the Inspector: set the width, assign a material, and enable Use World Space. Then in your script:
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;
}
}
This script updates the preview arc every frame. As the player rotates the launcher or adjusts the angle, the arc updates in real time — exactly the kind of aiming aid you see in Angry Birds, Worms, and countless other games.
LineRenderer Tips
- Set Use World Space to true — your points are already in world space.
- Use a simple unlit material so the line is always fully visible regardless of lighting.
-
To make the arc fade out or become dotted near the end, animate the LineRenderer's
widthCurveor use a custom material with UV scrolling. -
For a dashed arc (common in puzzle games), render every other segment by alternating
enabledon multiple LineRenderers.
Testing with a Real Projectile
The real validation of your trajectory math is to compare the predicted arc against an actual Rigidbody launched with the same velocity. If the prediction is correct, the Rigidbody should follow the LineRenderer exactly.
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;
}
}
Run the scene, watch the projectile fly, and verify it traces the LineRenderer arc. If it diverges, check:
-
Drag — even a small drag value will curve the real projectile away from the mathematical
prediction. Set
rb.drag = 0. -
Gravity scale — the Rigidbody has a Gravity Scale property (default 1). If
someone set it to 2, the simulation uses double gravity but your formula still uses the project gravity.
Always use
rb.gravityScalein your formula if it can differ from 1. - Physics timestep — the simulation integrates at fixed timestep intervals and may be slightly off from continuous math. This is a tiny effect and usually invisible.
Trajectory by Target Point — Reverse Calculation
The previous sections assumed you knew the angle and wanted to compute where the projectile goes. The more interesting problem in gameplay is the reverse: you know where you want the projectile to land, and you need to calculate the angle that gets it there.
Let R be the horizontal distance to the target and v₀ be the fixed launch speed. 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. Because sin is symmetric around 90°, there is also a high-angle (arcing) solution: θ_high = 90° − θ_low.
If (g × R) / v₀² > 1, the target is out of range for the given speed — no real solution
exists. Always check this before calling Asin, which would return NaN.
using UnityEngine;
public class TargetingSystem : MonoBehaviour
{
public float initialSpeed = 20f;
// Returns true if a solution exists, outputs both angle solutions
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;
// Horizontal distance (ignoring Y difference for flat-ground formula)
float R = new Vector3(toTarget.x, 0f, toTarget.z).magnitude;
float sinTwoTheta = (g * R) / (initialSpeed * initialSpeed);
if (sinTwoTheta > 1f)
{
// Target is out of range
return false;
}
float twoTheta = Mathf.Asin(sinTwoTheta); // radians
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;
}
}
With this code, your AI turrets and smart cannons can always compute the exact angle needed. Use the low-angle solution for fast, direct shots and the high-angle solution for lobbing over walls.
Handling Height Differences
The formula above is derived for flat ground (launcher and target at the same height). In practice, you often fire at a target that is higher or lower. For a fully general solution, you must use the full quadratic from the parametric equations. However, for many games the flat-ground approximation is good enough — especially if height differences are small relative to range.
A pragmatic workaround: compute the horizontal distance and the vertical offset separately. Add a small upward correction angle for elevated targets. For precise general solutions, search for the "general trajectory equation" which results in a fourth-degree polynomial — solvable but beyond the scope of this chapter.
Converting from ZY Plane to Full 3D
All of the trajectory math so far lives in a 2D plane: the vertical axis (Y) and one horizontal axis. In 3D space, the horizontal component can point in any direction. The key is to separate the problem:
- Compute the horizontal direction from launcher to target (ignoring Y).
- Compute the horizontal speed component and vertical speed component using the angle.
- Combine them into a 3D velocity vector.
Vector3 Compute3DLaunchVelocity(Vector3 origin, Vector3 target, float angleDeg)
{
// Step 1: horizontal direction
Vector3 delta = target - origin;
Vector3 horizontalDelta = new Vector3(delta.x, 0f, delta.z);
Vector3 horizontalDir = horizontalDelta.normalized;
// Step 2: speed components
float angleRad = angleDeg * Mathf.Deg2Rad;
float horizontalSpeed = initialSpeed * Mathf.Cos(angleRad);
float verticalSpeed = initialSpeed * Mathf.Sin(angleRad);
// Step 3: 3D velocity
return horizontalDir * horizontalSpeed + Vector3.up * verticalSpeed;
}
This approach correctly handles a turret that can rotate freely around the Y axis. The horizontal direction is always computed fresh from the actual target position, so the turret automatically aims in the right direction.
Exercise: Tank Control
The exercises in this chapter build a complete tank game with realistic ballistic projectiles. The first exercise is a tank movement controller. The tank uses Rigidbody forces rather than direct Transform manipulation, so it interacts physically with the terrain.
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; // pivot that rotates up/down
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"); // e.g. right stick Y or Q/E keys
currentElevation = Mathf.Clamp(
currentElevation + elevInput * cannonElevationSpeed * Time.fixedDeltaTime,
minElevation,
maxElevation
);
cannonPivot.localRotation = Quaternion.Euler(-currentElevation, 0f, 0f);
}
}
Exercise: Camera Control
The follow camera smoothly tracks the tank from behind and above. It uses a fixed offset in local space and Lerps to the target position to avoid jarring movement.
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;
// Desired position: behind and above the tank
Vector3 desiredPosition = target.TransformPoint(offset);
transform.position = Vector3.Lerp(transform.position, desiredPosition,
followSpeed * Time.deltaTime);
// Look at the tank
Quaternion desiredRotation = Quaternion.LookRotation(target.position - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation,
rotationSpeed * Time.deltaTime);
}
}
Exercise: Turret with Trajectory Prediction
The final exercise brings everything together: the turret automatically aims at the player tank using the reverse trajectory calculation, displays the predicted arc with a LineRenderer, and fires when the aim is accurate. This is a fully functional AI aiming system.
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 fireThreshold = 2f; // degrees of error allowed before firing
public float fireCooldown = 3f;
private LineRenderer lineRenderer;
private float cooldownTimer;
void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
}
void Update()
{
cooldownTimer -= Time.deltaTime;
if (playerTank == null) return;
// Compute required angle
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;
// Build the 3D velocity
Vector3 launchVelocity = Compute3DLaunchVelocity(
muzzle.position, playerTank.position, chosenAngle);
// Update trajectory preview
Vector3[] pts = ComputeTrajectoryPoints(muzzle.position, launchVelocity, previewResolution);
lineRenderer.positionCount = pts.Length;
lineRenderer.SetPositions(pts);
// Fire if cooldown is done
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;
}
}
With these three scripts — TankController, TankCamera, and
AITurret — you have a playable mini-game that demonstrates every concept from this chapter.
The tank can move and rotate with physics forces, the camera follows smoothly, the AI turret
reverse-calculates the exact angle to hit the player, visualises the predicted arc with a LineRenderer,
and fires automatically.
Summary
Trajectory mathematics is one of the most immediately satisfying branches of game math to learn, because the results are visual, dramatic, and immediately testable. Here is what you can now do:
- 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
In the next chapter, we step back from the world of physics and look at the mathematical objects that underlie every transform in Unity: the transformation matrix.
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