// 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 allMovers = new List(); 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(); 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; } }