Unity6 Game Development · Lesson 35

NavMeshAgent requests, pending paths and arrival

Observe navigation states only after a compatible NavMesh is prepared.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

Prepared navigation data is part of the feature

An agent needs a compatible navigation surface before a destination request becomes meaningful. The lesson uses AI Navigation 2.0 documentation with Unity6000.0, a NavMeshSurface configured for a known Agent Type and an agent placed on the resulting baked data. Collection layers and geometry determine what was baked, so a visually open floor is not automatically reachable navigation space. Record the installed package version rather than assuming every later version uses identical Inspector defaults. Keep the agent free from an unreviewed competing Rigidbody or Transform writer for this baseline. These are integration prerequisites, not values a pure C# arrival predicate can infer.

Submit a request once, then observe guarded states

RequestPath checks an assigned destination, an enabled agent and isOnNavMesh before calling SetDestination. Its Boolean result means the destination was requested successfully, not that route calculation or travel finished. The component records the request frame and begins observations on a later frame rather than immediately treating stale data as a result. Update checks binding, pending calculation, path status, active path and finite remaining distance in that order. Only a complete active path inside stoppingDistance with sufficiently small velocity is labelled arrived. Publish logs transitions between observation labels instead of producing the same message every frame.

    private void Update()
    {
        if (!submitted || Time.frameCount <= requestFrame) return;
        if (!agent.enabled || !agent.isOnNavMesh) { Publish("not on NavMesh"); return; }
        if (agent.pathPending) { Publish("pending"); return; }
        if (agent.pathStatus != NavMeshPathStatus.PathComplete) { Publish(agent.pathStatus.ToString()); return; }
        if (!agent.hasPath) { Publish("no active path"); return; }
        float distance = agent.remainingDistance;
        if (float.IsInfinity(distance) || float.IsNaN(distance)) { Publish("unknown distance"); return; }
        if (distance > agent.stoppingDistance) { Publish("travelling"); return; }
        Publish(agent.velocity.sqrMagnitude > 0.01f ? "braking" : "arrived");
    }

Read each supplied observation as evidence, not an agent simulation

The first portable observation is pending, so even a zero distance cannot make it arrived. The second is partial and is rejected before distance matters. The third has an infinite remaining distance, which the classifier labels unknown distance rather than treating the value as a useful measurement. A complete active path at distance two is travelling, while one at one tenth with stopping distance two tenths and zero squared speed meets this policy’s arrival condition. These five outcomes verify guard ordering on supplied data. They do not calculate a route, build a NavMesh, avoid an obstacle or prove that an agent could ever produce those observations in a particular scene.

Do not convert missing path evidence into success

An agent already standing at its requested position can legitimately have no active path, and an external ResetPath can also remove one. This observer conservatively reports no active path instead of inferring success from absence. That can under-report a gameplay arrival, but it avoids promoting a cleared or never-created path into proof that the requested destination was reached. A production goal system can add request identity, an explicit destination-distance policy and accepted zero-travel commands. Define that additional contract deliberately. Likewise a partial path ending near its own endpoint is not proof that the original goal was reached, even when its remaining distance becomes small.

Practice: distinguish reachable, partial and initially satisfied goals

Bake a simple floor with one obstacle, request a reachable destination and observe state changes with Auto Braking enabled. Then use an unreachable island in a fresh scene copy and inspect whether the path becomes partial or invalid for that setup. Finally place the destination at the agent’s starting position and record the conservative no-active-path case instead of rewriting it as a failure of the engine. Each experiment should preserve the surface Agent Type, agent placement, destination and baked-data version. A log line without those setup facts cannot establish whether the algorithm, data or current binding caused the result.

  • Hint: SetDestination acceptance and PathComplete are separate observations.
  • Hint: a destination at the current position requires an explicit zero-travel goal policy if gameplay should count it as success.

Practice: add adversarial observations before changing arrival policy

Supply no active path with zero distance, a complete path with nonzero braking speed, a distance exactly equal to the stopping threshold, and an unknown distance. Predict each classifier result. Then add a separately named AlreadyAtGoal command result that uses an explicitly supplied geometric distance and request identity, without changing what a missing navigation path means. Validate negative thresholds and invalid speed values if the pure function becomes public. This exercise is independent of baking an island scene: it strengthens the decision contract while admitting that only the engine adapter can supply real current path observations and package-dependent navigation data.

  • Hint: missing evidence should not accidentally become the most successful state.
  • Hint: preserve the distinction between an accepted zero-travel goal and an externally cleared path.

Keep route readiness and gameplay completion separately auditable

A useful navigation receipt names the package version, Agent Type, surface bake, initial binding and destination request, then records the relevant current path observations. The example does not chase a moving target every frame, manage off-mesh traversal or assert universal goal completion. Its conservative arrival label is an application policy built on documented observations. The engine script was checked syntactically and against pinned API references without an installed Unity Editor. Desktop execution established only the supplied classifier outputs. Carry those limits forward when integrating quests, patrols or interaction ranges, where reaching a navigation endpoint and satisfying a gameplay objective can be different events.

Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. In a Unity6000.0 3D project, install a compatible AI Navigation 2.0 package (com.unity.ai.navigation) through Package Manager. This lesson references its 2.0 documentation; record the actual installed package version.
  2. Create a floor and static obstacle geometry. Add a NavMeshSurface component from AI Navigation, select the intended Agent Type, choose collection layers/geometry deliberately, and Bake the surface in the Inspector.
  3. Create a Capsule with NavMeshAgent using the same Agent Type and place its feet on the baked NavMesh. Attach NavPathProbe.cs; remove a nonkinematic Rigidbody or other Transform position writer from this baseline agent.
  4. Create an empty Destination on a reachable part of that same surface and assign its Transform. Set a small nonnegative stoppingDistance, such as 0.2, and a positive agent speed.
  5. Enter Play mode and observe requested/pending/travelling/arrival state changes. Test an unreachable island or mismatched agent type separately; an accepted request is not a guarantee of a complete path.
  6. Invoke Request Path after deliberately moving the target. The example does not automatically chase a moving target or declare a cleared path to be successful arrival.
  7. For the arrival baseline keep Auto Braking enabled. Test a destination initially at the agent position and a deliberately cleared path separately: no active path is intentionally not labelled arrived by this observer.
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public sealed class NavPathProbe : MonoBehaviour
{
    [SerializeField] private Transform destination;
    private NavMeshAgent agent;
    private bool submitted;
    private string lastState;
    private int requestFrame;
    private void Awake() { agent = GetComponent<NavMeshAgent>(); }
    private void Start() { RequestPath(); }
    [ContextMenu("Request Path (Play Mode)")]
    public void RequestPath()
    {
        if (!Application.isPlaying || !isActiveAndEnabled || agent == null) return;
        submitted = false;
        if (destination == null || !agent.enabled || !agent.isOnNavMesh)
        { Publish("assign destination and place enabled agent on a compatible NavMesh"); return; }
        submitted = agent.SetDestination(destination.position);
        requestFrame = Time.frameCount;
        Publish(submitted ? "requested" : "request rejected");
    }
    private void Update()
    {
        if (!submitted || Time.frameCount <= requestFrame) return;
        if (!agent.enabled || !agent.isOnNavMesh) { Publish("not on NavMesh"); return; }
        if (agent.pathPending) { Publish("pending"); return; }
        if (agent.pathStatus != NavMeshPathStatus.PathComplete) { Publish(agent.pathStatus.ToString()); return; }
        if (!agent.hasPath) { Publish("no active path"); return; }
        float distance = agent.remainingDistance;
        if (float.IsInfinity(distance) || float.IsNaN(distance)) { Publish("unknown distance"); return; }
        if (distance > agent.stoppingDistance) { Publish("travelling"); return; }
        Publish(agent.velocity.sqrMagnitude > 0.01f ? "braking" : "arrived");
    }
    private void Publish(string state)
    {
        if (state == lastState) return;
        lastState = state;
        Debug.Log("Navigation: " + state, this);
    }
}

What to observe in Unity

The prepared agent requests a route and reports conservative current path states. Reachable movement can reach the stated arrival conditions; absent, partial or unknown paths are not reported as arrived.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

Console.WriteLine(Classify(true, "complete", true, 0, 0.2, 0));
Console.WriteLine(Classify(false, "partial", true, 0, 0.2, 0));
Console.WriteLine(Classify(false, "complete", true, double.PositiveInfinity, 0.2, 0));
Console.WriteLine(Classify(false, "complete", true, 2, 0.2, 0));
Console.WriteLine(Classify(false, "complete", true, 0.1, 0.2, 0));
string Classify(bool pending, string path, bool hasPath, double distance, double stopping, double speedSquared)
{
    if (pending) return "pending";
    if (path != "complete") return path;
    if (!hasPath) return "no active path";
    if (!double.IsFinite(distance)) return "unknown distance";
    if (distance > stopping) return "travelling";
    return speedSquared > 0.01 ? "braking" : "arrived";
}
Open C# practice

C# practice output

pending
partial
unknown distance
travelling
arrived
Try this
Accepted request, complete path and observed arrival are three different facts.

Further reading