Bezier Curves
A Bezier curve is a smooth curve defined by a few control points. Games use them for projectile arcs, missile paths, camera rails, card-dealing animations, racing lines and UI connectors. This guide explains how they work, gives ready-to-use code, and shows how to move along a curve at a constant speed.
The pen tool in a drawing app. You click a start point and an end point, then drag "handles" that pull the line toward them like magnets. The line never touches the handles, but bends toward them. The more you drag, the stronger the bend.
Built from Lerps
A Bezier curve is just Lerp applied several times. Take three points: start P0, control P1, end P2.
- Lerp from
P0toP1byt→ point A - Lerp from
P1toP2byt→ point B - Lerp from A to B by
t→ the point on the curve
As t goes from 0 to 1, that point traces a smooth curve from P0 to P2, bending toward P1. This step-by-step method is called De Casteljau's algorithm.
Vector3 QuadraticByLerps(Vector3 p0, Vector3 p1, Vector3 p2, float t)
{
Vector3 a = Vector3.Lerp(p0, p1, t);
Vector3 b = Vector3.Lerp(p1, p2, t);
return Vector3.Lerp(a, b, t);
}
Types of curve
| Type | Points | Shape | Good for |
|---|---|---|---|
| Linear | 2 | Straight line (just Lerp) | Simple moves |
| Quadratic | 3 (1 control) | One bend | Jumps, thrown objects, arcs |
| Cubic | 4 (2 controls) | S-curves, independent start/end directions | Paths, rails, missiles, UI lines |
The code
The direct formulas are faster than chaining Lerps. The derivative gives the direction the curve is heading at t, which you use to rotate an object along the path.
using UnityEngine;
public static class Bezier
{
// Quadratic: (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2
public static Vector3 Quadratic(Vector3 p0, Vector3 p1, Vector3 p2, float t)
{
float u = 1f - t;
return u * u * p0 + 2f * u * t * p1 + t * t * p2;
}
public static Vector3 QuadraticTangent(Vector3 p0, Vector3 p1, Vector3 p2, float t)
{
return 2f * (1f - t) * (p1 - p0) + 2f * t * (p2 - p1);
}
// Cubic: (1-t)^3 P0 + 3(1-t)^2 t P1 + 3(1-t) t^2 P2 + t^3 P3
public static Vector3 Cubic(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float u = 1f - t;
return u * u * u * p0
+ 3f * u * u * t * p1
+ 3f * u * t * t * p2
+ t * t * t * p3;
}
public static Vector3 CubicTangent(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float u = 1f - t;
return 3f * u * u * (p1 - p0)
+ 6f * u * t * (p2 - p1)
+ 3f * t * t * (p3 - p2);
}
}
Example: a lobbed grenade
Place the control point above the midpoint between thrower and target. Higher control = higher arc.
using System.Collections;
using UnityEngine;
public class Lob : MonoBehaviour
{
[SerializeField] private float arcHeight = 3f;
[SerializeField] private float flightTime = 1f;
public IEnumerator FlyTo(Vector3 target)
{
Vector3 start = transform.position;
Vector3 control = (start + target) / 2f + Vector3.up * arcHeight;
for (float e = 0f; e < flightTime; e += Time.deltaTime)
{
float t = e / flightTime;
transform.position = Bezier.Quadratic(start, control, target, t);
// Face the direction of travel
Vector3 dir = Bezier.QuadraticTangent(start, control, target, t);
if (dir != Vector3.zero) transform.rotation = Quaternion.LookRotation(dir);
yield return null;
}
transform.position = target;
}
}
A quadratic curve with the control at the midpoint gives an arc that peaks at half of arcHeight, because the curve only bends toward the control point.
Example: an editable path with Gizmos
Use four child Transforms as the points, so you can drag them around in the Scene view.
using UnityEngine;
public class BezierPath : MonoBehaviour
{
public Transform p0, p1, p2, p3;
[SerializeField, Range(4, 100)] private int gizmoSegments = 30;
public Vector3 GetPoint(float t) => Bezier.Cubic(p0.position, p1.position, p2.position, p3.position, t);
public Vector3 GetTangent(float t) => Bezier.CubicTangent(p0.position, p1.position, p2.position, p3.position, t);
void OnDrawGizmos()
{
if (!p0 || !p1 || !p2 || !p3) return;
// Handles (straight lines to the control points)
Gizmos.color = Color.gray;
Gizmos.DrawLine(p0.position, p1.position);
Gizmos.DrawLine(p3.position, p2.position);
// The curve itself, as many short lines
Gizmos.color = Color.cyan;
Vector3 prev = GetPoint(0f);
for (int i = 1; i <= gizmoSegments; i++)
{
Vector3 next = GetPoint(i / (float)gizmoSegments);
Gizmos.DrawLine(prev, next);
prev = next;
}
}
}
To show it in the game, feed the same points to a LineRenderer: set positionCount and call SetPosition(i, GetPoint(i / (float)(count - 1))).
Moving at a constant speed
Here's the catch: t is not distance. Where control points bunch up, equal steps of t cover less ground, so an object moving with t += speed * dt speeds up and slows down along the curve.
A tape measure printed on stretchy rubber. The marks (t) are evenly spaced on the tape, but when the tape is bent around a curve, some sections are stretched and others squashed. To walk at an even pace you need a real measurement: a table that says "5 meters along = mark 0.37".
t isn't distance. Moving t forward by the same amount each frame makes an object race along the stretched parts and crawl through the bunched ones. Converting distance to t with an arc-length table gives even spacing.The fix is an arc-length table: sample the curve, add up the distances, then look up t from a distance.
using UnityEngine;
[RequireComponent(typeof(BezierPath))]
public class PathFollower : MonoBehaviour
{
[SerializeField] private Transform mover;
[SerializeField] private float speed = 3f; // meters per second
[SerializeField] private int samples = 100;
private BezierPath path;
private float[] lengths; // lengths[i] = distance along the curve at t = i / samples
private float distance;
public float TotalLength => lengths[samples];
void Start()
{
path = GetComponent<BezierPath>();
BuildTable(); // rebuild if the control points move
}
void BuildTable()
{
lengths = new float[samples + 1];
Vector3 prev = path.GetPoint(0f);
for (int i = 1; i <= samples; i++)
{
Vector3 p = path.GetPoint(i / (float)samples);
lengths[i] = lengths[i - 1] + Vector3.Distance(prev, p);
prev = p;
}
}
// Convert a distance along the curve into the matching t
float DistanceToT(float d)
{
d = Mathf.Clamp(d, 0f, TotalLength);
for (int i = 1; i <= samples; i++)
{
if (lengths[i] >= d)
{
float segment = Mathf.InverseLerp(lengths[i - 1], lengths[i], d);
return (i - 1 + segment) / samples;
}
}
return 1f;
}
void Update()
{
distance = Mathf.Repeat(distance + speed * Time.deltaTime, TotalLength); // loop
float t = DistanceToT(distance);
mover.position = path.GetPoint(t);
mover.rotation = Quaternion.LookRotation(path.GetTangent(t));
}
}
For long paths, replace the linear search with a binary search. For a looping path, make sure P3 equals P0.
Joining curves into longer paths
Chain cubic curves end to end: the end point of one is the start of the next. For a smooth joint (no sharp corner), the last control point of curve A, the shared point, and the first control point of curve B must lie on one straight line. Drawing apps do this automatically when you "mirror" handles.
In production
- Unity Splines package (
com.unity.splines, Package Manager): edit Bezier splines in the Scene view with proper tools, evaluate them in code, and animate objects with theSplineAnimatecomponent. Use it for real level paths. - DOTween:
transform.DOPath(points, duration, PathType.CatmullRom)moves along a smooth path through the points you give it. - Cinemachine uses splines for camera dolly tracks.
Common Mistakes
- Expecting the curve to pass through control points. It only passes through the start and end. It bends toward the controls.
- Assuming even t = even speed. Use an arc-length table (above) when speed matters.
LookRotationwith a zero tangent. Happens when a control point sits exactly on an end point. Check forVector3.zero.- Recomputing the table every frame. Build it once, and again only when points move.
See also
- Interpolation & Easing
- Debug (Gizmos)
- DOTween
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official Unity documentation.