Source file unity-csharp/game-ai/navmesh-steering/WaypointPathFollower.cs from the GDnD code examples. Download raw file
// GDnD wiki example
// Demonstrates: following a list of waypoints smoothly, whether they come from NavMesh.CalculatePath
// (path.corners) or from your own grid A*:
// - line-of-sight smoothing (drop waypoints you can already see past)
// - track progress along the path (advance past corners you have reached or cut), and seek a lookahead point
// - Arrive (slow down) only on the final waypoint, so the agent does not brake at every corner
// Related pages: [[unity-navmesh]], [[steering-behaviours]], [[pathfinding-algorithms]], [[navigation-mesh-construction]]
using System;
using System.Collections.Generic;
using UnityEngine;
// Plain C# class: the owner asks it for a desired velocity each frame and moves however it likes
// (transform, CharacterController, kinematic Rigidbody, or NavMeshAgent.Move).
public class WaypointPathFollower
{
public float MaxSpeed = 4f;
public float WaypointRadius = 0.6f; // "close enough" to a corner to aim at the next one
public float Lookahead = 1.5f; // aim this far ahead along the path, not at the corner itself
public float SlowingRadius = 2.5f; // Arrive starts slowing inside this distance of the LAST point
public float ArrivedRadius = 0.15f;
private readonly List<Vector3> points = new List<Vector3>();
private int target; // index of the waypoint we are heading for
public bool IsFinished => target >= points.Count;
// canSeeDirectly(a, b): true if a straight move from a to b is clear.
// NavMesh: (a, b) => !NavMesh.Raycast(a, b, out _, NavMesh.AllAreas). Grid A*: walk the cells on the line.
// Pass null to skip smoothing (NavMesh corners are already string-pulled, so they rarely need it).
public void SetPath(IList<Vector3> waypoints, Func<Vector3, Vector3, bool> canSeeDirectly = null)
{
points.Clear();
if (waypoints == null || waypoints.Count == 0) { target = 0; return; }
// Line-of-sight pruning: from each kept point, jump to the furthest point still visible.
int i = 0;
points.Add(waypoints[0]);
while (i < waypoints.Count - 1)
{
int furthest = i + 1;
if (canSeeDirectly != null)
for (int j = waypoints.Count - 1; j > i + 1; j--)
if (canSeeDirectly(waypoints[i], waypoints[j])) { furthest = j; break; }
points.Add(waypoints[furthest]);
i = furthest;
}
target = points.Count > 1 ? 1 : 0; // points[0] is where we started
}
public Vector3 DesiredVelocity(Vector3 position)
{
if (IsFinished) return Vector3.zero;
int last = points.Count - 1;
// Where are we along the path? Segment k runs from points[k-1] to points[k]. If we are nearer
// a later segment (we cut a corner), or inside WaypointRadius of the current corner, move on.
// Never aim at a corner we have already passed: that is what causes U-turns and jitter.
for (int k = target + 1; k <= Mathf.Min(target + 2, last); k++)
if (DistanceToSegment(position, k) < DistanceToSegment(position, target)) target = k;
while (target < last && Flat(points[target] - position).magnitude < WaypointRadius)
target++;
if (target == last)
{
// Final segment: Arrive. Only the last point gets braking, so corners are taken at speed.
Vector3 toGoal = Flat(points[last] - position);
float distance = toGoal.magnitude;
if (distance < ArrivedRadius) { target = points.Count; return Vector3.zero; }
float speed = MaxSpeed * Mathf.Min(1f, distance / SlowingRadius);
return toGoal / distance * speed;
}
// Otherwise seek a point Lookahead metres further along the path from our closest point on it.
Vector3 aim = PointAlongPath(ClosestPointOnSegment(position, target), Lookahead);
Vector3 toAim = Flat(aim - position);
return toAim.sqrMagnitude < 1e-6f ? Vector3.zero : toAim.normalized * MaxSpeed;
}
private Vector3 PointAlongPath(Vector3 from, float distance)
{
for (int k = target; k < points.Count; k++)
{
float segment = Vector3.Distance(from, points[k]);
if (segment >= distance) return Vector3.MoveTowards(from, points[k], distance);
distance -= segment;
from = points[k];
}
return points[points.Count - 1];
}
private Vector3 ClosestPointOnSegment(Vector3 p, int k)
{
Vector3 a = points[k - 1], b = points[k], ab = b - a;
float t = ab.sqrMagnitude < 1e-6f ? 1f : Mathf.Clamp01(Vector3.Dot(p - a, ab) / ab.sqrMagnitude);
return a + ab * t;
}
private float DistanceToSegment(Vector3 p, int k) => Flat(ClosestPointOnSegment(p, k) - p).magnitude;
private static Vector3 Flat(Vector3 v) => new Vector3(v.x, 0f, v.z);
}