Skip to content

NavMesh Integration

Most GOAP agents need to move through the world. This tutorial covers how to use Unity's NavMesh system with the framework, from the built-in strategy to creating custom movement behaviors.


Prerequisites

  • A scene with a baked NavMesh (Window > AI > Navigation > Bake).
  • A NavMeshAgent component on your agent's GameObject.
  • The GoapAgent component with a Brain assigned.

The default GoapAgentContext automatically caches the NavMeshAgent for O(1) access via context.GetCapability<NavMeshAgent>().


Using the Built-In NavMeshMoveStrategy

The framework includes a ready-made movement strategy:

Setting Type Description
TargetKeyId SerializableGuid Blackboard key holding the target position (Vector3)
MoveSpeed float Agent movement speed
StoppingDistance float Distance at which the agent considers itself "arrived"

Setup

  1. Create a NavMeshMoveStrategy asset (Create > RGS > GOAP > Strategies > NavMesh Move).
  2. Create an action in your brain and assign this strategy.
  3. In the action's settings, wire TargetKeyId to the blackboard key that holds the destination.
  4. Set MoveSpeed and StoppingDistance to match your game's movement feel.

This handles destination setting, path following, arrival detection, and cleanup automatically.


Creating a Custom Movement Strategy

When you need movement behavior beyond what the built-in provides (e.g., sprinting, crouching, flanking), create a custom strategy:

using System;
using UnityEngine;
using UnityEngine.AI;
using RGS.GOAP.Core;
using RGS.GOAP.Core.Interfaces;
using RGS.GOAP.Core.Strategies;

[Serializable]
public class CustomMoveSettings : BaseStrategySettings
{
    public SerializableGuid TargetKeyId;
    public float MoveSpeed = 3.5f;
    public float StoppingDistance = 1.0f;
}

[CreateAssetMenu(menuName = "RGS/GOAP/Strategies/Custom Move")]
public class CustomMoveStrategy : BaseGoapActionStrategy
{
    public override Type GetSettingsType() => typeof(CustomMoveSettings);

    public override void OnStart(IGoapAgentContext context, GoapBlackboard blackboard,
                                 BaseStrategySettings settings)
    {
        var s = settings as CustomMoveSettings;
        if (s == null) return;

        var agent = context.GetCapability<NavMeshAgent>();
        if (agent == null)
        {
            Debug.LogWarning(
                $"[CustomMove] No NavMeshAgent on '{context.gameObject.name}'. "
                + "Add a NavMeshAgent component.");
            return;
        }

        Vector3 target = blackboard.GetVector3(s.TargetKeyId, Vector3.negativeInfinity);
        if (float.IsInfinity(target.x)) return;

        agent.isStopped = false;
        agent.speed = s.MoveSpeed;
        agent.stoppingDistance = s.StoppingDistance;
        agent.SetDestination(target);
    }

    public override GoapActionStatus OnUpdate(IGoapAgentContext context, GoapBlackboard blackboard,
                                               float deltaTime, BaseStrategySettings settings)
    {
        var s = settings as CustomMoveSettings;
        if (s == null) return GoapActionStatus.Failure;

        var agent = context.GetCapability<NavMeshAgent>();
        if (agent == null) return GoapActionStatus.Failure;

        // Wait for path to be calculated
        if (agent.pathPending)
            return GoapActionStatus.Running;

        // Check arrival using NavMesh path distance
        if (agent.remainingDistance <= agent.stoppingDistance + 0.1f)
            return GoapActionStatus.Success;

        // Treat path completion as arrival, not error
        if (!agent.hasPath && !agent.pathPending)
            return GoapActionStatus.Success;

        return GoapActionStatus.Running;
    }

    public override void OnStop(IGoapAgentContext context, GoapBlackboard blackboard,
                                BaseStrategySettings settings)
    {
        var agent = context.GetCapability<NavMeshAgent>();
        if (agent != null && agent.isOnNavMesh)
        {
            agent.isStopped = true;
            agent.ResetPath();
        }
    }
}

Arrival Detection

This is one of the most common sources of bugs. Use NavMesh path distance, not euclidean distance:

// BAD — fails on complex NavMesh geometry (walls, ramps, tight corridors)
if (Vector3.Distance(agentPos, targetPoint) <= threshold)

// GOOD — uses NavMesh path distance, works on all geometry
if (agent.remainingDistance <= agent.stoppingDistance + 0.1f)

Warning

Vector3.Distance measures straight-line distance, which can be much shorter than the actual NavMesh path around obstacles. An agent might appear "close" to the target in euclidean distance but still have a long path to walk. Always use agent.remainingDistance.

Handling Path Completion

Treat !agent.hasPath && !agent.pathPending as "navigation completed," not as an error:

bool arrived = agent.remainingDistance <= stoppingDistance + 0.1f;
bool pathCleared = !agent.hasPath && !agent.pathPending;
if (arrived || pathCleared)
{
    return GoapActionStatus.Success;
}

OnStop Cleanup

OnStop is called on success, failure, and external interruption. Always stop the NavMeshAgent:

public override void OnStop(IGoapAgentContext context, GoapBlackboard blackboard,
                            BaseStrategySettings settings)
{
    var agent = context.GetCapability<NavMeshAgent>();
    if (agent != null && agent.isOnNavMesh)
    {
        agent.isStopped = true;
        agent.ResetPath();
    }
}

Without this, an interrupted agent continues walking toward the old destination even after the plan changes.


WayPointManager Integration

For patrol routes, pair movement strategies with the WayPointManager component:

// Access the WayPointManager
var wpManager = context.GetCapability<WayPointManager>();
if (wpManager == null || !wpManager.HasWaypoints) return;

// Current waypoint
Vector3 target = wpManager.CurrentPosition;

// Check arrival
if (wpManager.IsAtCurrentWaypoint(context.transform.position))
    wpManager.AdvanceWaypoint();  // Moves to next (respects loop/ping-pong)

The WaypointSensorSO reads from WayPointManager and writes the current position and arrival status to the blackboard, so your strategies can simply read the blackboard key.

WayPointManager API

Property/Method Description
CurrentWaypoint Current target Transform
CurrentPosition World position of current waypoint
HasWaypoints True if any waypoints are configured
IsAtCurrentWaypoint(pos) Distance check against ArrivalDistance
AdvanceWaypoint() Move to next (respects loop/ping-pong mode)
ResetToStart() Return to first waypoint
SetWaypointIndex(i) Jump to a specific waypoint

Multi-Agent Considerations

The NavMeshAgent component is per-agent, so it's safe to read and write from shared strategy SOs. Each agent has its own path, destination, and movement state.

For scenarios with many agents moving in close proximity:

  • Unity's NavMesh obstacle avoidance handles basic collision avoidance automatically.
  • Consider using NavMesh obstacle avoidance priority to let important agents (e.g., player escorts) take precedence.
  • For very dense crowds, profile NavMeshAgent as a potential bottleneck.

What's Next