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.

  1. Lerp from P0 to P1 by t → point A
  2. Lerp from P1 to P2 by t → point B
  3. 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);
}
P0 (start)P1 (control)P2 (end)A = Lerp(P0, P1, t)B = Lerp(P1, P2, t)Lerp(A, B, t)shown at t = 0.35
De Casteljau's construction at t = 0.35. Lerp along each dashed side to get A and B, then lerp between them (highlighted) to get the point on the curve. Sweep t from 0 to 1 and that point traces the whole curve, bending toward P1 but never touching it.

Types of curve

TypePointsShapeGood for
Linear2Straight line (just Lerp)Simple moves
Quadratic3 (1 control)One bendJumps, thrown objects, arcs
Cubic4 (2 controls)S-curves, independent start/end directionsPaths, 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".

Even steps of t: t = 0, 0.1, 0.2 … 1Even steps of distance (arc-length table)bunched up where the controls pull, spread out elsewheresame spacing everywhere: constant speed
Why 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

Common Mistakes

See also