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;
}
}