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

// GDnD wiki example
// Demonstrates: turning a decision ("go there") into smooth NavMesh movement:
//   - MoveTo() returns Running / Arrived / Failed, so a behaviour tree leaf or utility action can call it every tick
//   - throttled retargeting (re-path only when the goal has moved far enough, and not every frame)
//   - optional custom steering on top of NavMeshAgent (updatePosition = false, desiredVelocity + separation,
//     own acceleration and turning, then agent.nextPosition = transform.position)
// Related pages: [[unity-navmesh]], [[steering-behaviours]], [[behaviour-trees]], [[utility-ai]]
// Requires: Unity 6, AI Navigation package (com.unity.ai.navigation) for the NavMesh Surface that bakes the mesh.
 
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
 
public enum MoveStatus { Running, Arrived, Failed }
 
[RequireComponent(typeof(NavMeshAgent))]
public class NavMover : MonoBehaviour
{
    [Header("Retargeting")]
    [Tooltip("Only ask for a new path when the goal has moved at least this far.")]
    [SerializeField] private float repathDistance = 0.75f;
    [Tooltip("...and never more often than this (seconds).")]
    [SerializeField] private float repathInterval = 0.25f;
 
    [Header("Custom steering (optional)")]
    [Tooltip("Off: NavMeshAgent moves the transform itself. On: this script moves it, using the agent only for the path.")]
    [SerializeField] private bool customSteering = false;
    [SerializeField] private float separationRadius = 1.5f;
    [SerializeField] private float separationWeight = 2f;
    [SerializeField] private float turnSpeed = 540f;            // degrees per second
 
    private static readonly List<NavMover> allMovers = new List<NavMover>();
 
    private NavMeshAgent agent;
    private Vector3 currentGoal;
    private bool hasGoal;
    private float lastRepathTime = float.NegativeInfinity;
    private Vector3 velocity;                                   // used only by custom steering
 
    public NavMeshAgent Agent => agent;
 
    private void Awake() => agent = GetComponent<NavMeshAgent>();
 
    private void OnEnable()
    {
        allMovers.Add(this);
        agent.updatePosition = !customSteering;
        agent.updateRotation = !customSteering;
    }
 
    private void OnDisable() => allMovers.Remove(this);
 
    // Safe to call every frame. It only re-paths when the goal has really moved, and it never
    // calls ResetPath() or isStopped between targets, so the agent keeps moving while it re-paths.
    public MoveStatus MoveTo(Vector3 goal)
    {
        bool goalMoved = !hasGoal || (goal - currentGoal).sqrMagnitude > repathDistance * repathDistance;
        if (goalMoved && Time.time - lastRepathTime >= repathInterval)
        {
            if (!agent.SetDestination(goal)) return MoveStatus.Failed;   // goal not near any NavMesh
            currentGoal = goal;
            hasGoal = true;
            lastRepathTime = Time.time;
        }
 
        if (agent.pathPending) return MoveStatus.Running;               // new path arrives in a frame or two
        if (agent.pathStatus == NavMeshPathStatus.PathInvalid) return MoveStatus.Failed;
 
        bool atEndOfPath = agent.remainingDistance <= agent.stoppingDistance;
        if (!atEndOfPath) return MoveStatus.Running;
 
        // A partial path ends somewhere short of the goal: we got as close as we can, but did not arrive.
        return agent.pathStatus == NavMeshPathStatus.PathComplete ? MoveStatus.Arrived : MoveStatus.Failed;
    }
 
    // Use when the decision layer really wants the character to stop (e.g. a BT branch is aborted),
    // not as a way to switch targets: ResetPath() clears the path, so the agent brakes.
    public void Stop()
    {
        hasGoal = false;
        if (agent.hasPath) agent.ResetPath();
        velocity = Vector3.zero;
    }
 
    private void Update()
    {
        if (!customSteering) return;
 
        // 1. Path-following intent. desiredVelocity already includes the agent's own avoidance.
        Vector3 desired = agent.desiredVelocity;
 
        // 2. Blend in our own steering behaviours (here: separation from other movers).
        desired += separationWeight * Separation();
        desired.y = 0f;
        desired = Vector3.ClampMagnitude(desired, agent.speed);
 
        // 3. Integrate with the agent's acceleration limit, then move the transform ourselves.
        velocity = Vector3.MoveTowards(velocity, desired, agent.acceleration * Time.deltaTime);
        transform.position += velocity * Time.deltaTime;
 
        // 4. Hand our real position back to the agent. Its simulated position is constrained to the
        //    NavMesh, so copying it back keeps the body from leaving the mesh or clipping through walls.
        agent.nextPosition = transform.position;
        transform.position = agent.nextPosition;
 
        // 5. Turn to face the direction of travel at a limited rate.
        if (velocity.sqrMagnitude > 0.01f)
        {
            Quaternion facing = Quaternion.LookRotation(velocity);
            transform.rotation = Quaternion.RotateTowards(transform.rotation, facing, turnSpeed * Time.deltaTime);
        }
    }
 
    private Vector3 Separation()
    {
        Vector3 push = Vector3.zero;
        foreach (NavMover other in allMovers)
        {
            if (other == this) continue;
            Vector3 away = transform.position - other.transform.position;
            away.y = 0f;
            float d = away.magnitude;
            if (d > 0.001f && d < separationRadius)
                push += away / d * (1f - d / separationRadius);   // stronger when closer
        }
        return push * agent.speed;
    }
}