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
NavMeshAgentcomponent on your agent's GameObject. - The
GoapAgentcomponent 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¶
- Create a
NavMeshMoveStrategyasset (Create > RGS > GOAP > Strategies > NavMesh Move). - Create an action in your brain and assign this strategy.
- In the action's settings, wire
TargetKeyIdto the blackboard key that holds the destination. - Set
MoveSpeedandStoppingDistanceto 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
NavMeshAgentas a potential bottleneck.
What's Next¶
- Action Strategies — Full strategy lifecycle reference.
- Guard Post Demo — See NavMesh movement in a complete demo.
- Animation Integration — Combine movement with animation blending.