Source file unity-csharp/game-ai/navmesh-steering/CalculatePathMover.cs from the GDnD code examples. Download raw file

// GDnD wiki example
// Demonstrates: the "hybrid" route: NavMesh.CalculatePath for the route, your own code for the movement.
//   No NavMeshAgent at all: the path's corners go to WaypointPathFollower, and a CharacterController
//   moves the body. Swap CalculatePath for your own grid A* and nothing else changes.
// Related pages: [[unity-navmesh]], [[pathfinding-algorithms]], [[steering-behaviours]]
// Requires: Unity 6, a baked NavMesh (AI Navigation package, NavMesh Surface component).
 
using UnityEngine;
using UnityEngine.AI;
 
[RequireComponent(typeof(CharacterController))]
public class CalculatePathMover : MonoBehaviour
{
    [SerializeField] private Transform goal;
    [SerializeField] private float maxSpeed = 4f;
    [SerializeField] private float acceleration = 12f;
    [SerializeField] private float repathDistance = 1f;        // re-plan when the goal moves this far
    [SerializeField] private bool smoothPath = true;
 
    private readonly WaypointPathFollower follower = new WaypointPathFollower();
    private NavMeshPath path;                                   // created in Awake, reused every re-plan
    private CharacterController body;
    private Vector3 plannedGoal = new Vector3(float.MaxValue, 0f, 0f);
    private Vector3 velocity;
 
    private void Awake()
    {
        body = GetComponent<CharacterController>();
        path = new NavMeshPath();
        follower.MaxSpeed = maxSpeed;
    }
 
    private void Update()
    {
        if (goal != null && (goal.position - plannedGoal).sqrMagnitude > repathDistance * repathDistance)
            Replan(goal.position);
 
        Vector3 desired = follower.DesiredVelocity(transform.position);
        velocity = Vector3.MoveTowards(velocity, desired, acceleration * Time.deltaTime);
        body.Move((velocity + Physics.gravity) * Time.deltaTime);   // crude gravity keeps it grounded
 
        if (velocity.sqrMagnitude > 0.01f)
            transform.rotation = Quaternion.LookRotation(velocity);
    }
 
    private void Replan(Vector3 target)
    {
        plannedGoal = target;
        // Synchronous: fine for one character re-planning occasionally; budget it for crowds.
        if (NavMesh.CalculatePath(transform.position, target, NavMesh.AllAreas, path)
            && path.status != NavMeshPathStatus.PathInvalid)
        {
            follower.SetPath(path.corners,
                smoothPath ? (a, b) => !NavMesh.Raycast(a, b, out _, NavMesh.AllAreas) : null);
        }
    }
 
    private void OnDrawGizmosSelected()
    {
        if (path == null) return;
        Gizmos.color = Color.cyan;
        for (int i = 1; i < path.corners.Length; i++)
            Gizmos.DrawLine(path.corners[i - 1], path.corners[i]);
    }
}