Chapter 6: Navigation System
In Chapter 5: Target System, we learned how NPCs identify and track objects of interest in the game world. But once an NPC has decided to move toward a target, how does it actually get there? How does it avoid walls, navigate around furniture, or find the best path in a complex environment? That’s what the Navigation System handles!
What is the Navigation System?
Think of the Navigation System as the NPC’s “inner GPS.” Just like how your phone’s navigation app helps you find the best route to a destination, the Navigation System helps NPCs find paths through the game world.
Imagine a guard NPC that needs to chase a player through a castle. Without a Navigation System, the guard would just move in a straight line toward the player - trying to walk through walls, falling off ledges, or getting stuck on furniture. The Navigation System gives the NPC the ability to find a sensible path, just like a real person would.
Key Components of the Navigation System
Let’s break down the Navigation System into its main parts:
1. Navigation Mesh (NavMesh)
The Navigation Mesh (or NavMesh) is like a map of all the areas where NPCs can walk:
// Example of generating a NavMesh for the current scene
public static NavMeshData GenerateNavMesh(SceneData sceneData, NavMeshBuildSettings buildSettings)
{
// Create an empty list to store all meshes that will be part of the NavMesh
List<NavMeshBuildSource> sources = new();
// Default bounds (will be updated as we find objects)
Bounds bounds = new()
{
min = 1000000 * Vector3.one,
max = -1000000 * Vector3.one
};
// Find all objects marked as navmesh objects and add them to sources
CollectNavmeshData(sceneData.uniObjects, ref sources, ref bounds);
// Create and build the actual NavMesh
NavMeshData navMeshData = new();
NavMesh.AddNavMeshData(navMeshData);
NavMeshBuilder.UpdateNavMeshDataAsync(navMeshData, buildSettings, sources, bounds);
return navMeshData;
}
This code is responsible for creating the NavMesh - a special representation of the game world that tells NPCs where they can walk. It’s like creating a “walkability map” of your level.
The NavMesh is typically generated from the static geometry in your level (floors, terrain, etc.), while obstacles (like furniture or walls) create “holes” in the mesh that NPCs will navigate around.
2. NavMesh Agent
Each NPC that needs to navigate uses a NavMesh Agent - a component that helps it move along the NavMesh:
// Setting up a NavMesh Agent for an NPC
void SetupNavigation()
{
// Add a NavMeshAgent component to the NPC
agent = gameObject.AddComponent<NavMeshAgent>();
// Configure how the agent updates the character
agent.updatePosition = false;
agent.updateRotation = false;
agent.updateUpAxis = false;
// Update agent dimensions based on character size
UpdateAgentDimensions();
}
public void UpdateAgentDimensions()
{
// Enable/disable navigation based on NPC settings
useNavigation = npc.uniComponent.FindPropertyByName("useNavigation").value.b;
agent.enabled = useNavigation;
// Set the agent's radius and height
if (npc.uniComponent.FindPropertyByName("overrideDimensions").value.b)
{
agent.radius = npc.uniComponent.FindPropertyByName("radius").value.f;
agent.height = npc.uniComponent.FindPropertyByName("height").value.f;
}
else if (character != null)
{
agent.radius = character.uniComponent.FindPropertyByName("radius").value.f;
agent.height = character.uniComponent.FindPropertyByName("height").value.f;
}
// Set how close the agent needs to get to its destination
agent.stoppingDistance = 0;
}
This code configures a NavMesh Agent for an NPC. The agent is like a virtual person walking on the NavMesh:
- It has a size (radius and height) that determines where it can fit
- It can be enabled or disabled to turn navigation on or off
- It needs to know how close to get to destinations (stoppingDistance)
3. Path Finding
When an NPC needs to go somewhere, the Navigation System calculates a path for it:
// Setting a destination for an NPC to navigate to
public bool SetNavigationTarget(Vector3 position)
{
// Make sure we have a valid NavMeshAgent
if (agent == null || !agent.enabled || !agent.isOnNavMesh)
{
return false;
}
// Calculate and follow a path to the destination
return agent.SetDestination(position);
}
This method tells the NavMesh Agent to find a path to a specific position. It’s like typing an address into your GPS - the Navigation System will figure out the best route to get there.
4. Movement Control
Once a path is calculated, the NPC needs to actually follow it:
void FollowAgent()
{
if (agent == null) return;
// Get the next position along the calculated path
Vector3 nextPosition = agent.nextPosition;
Vector3 direction = nextPosition - transform.position;
// Calculate movement inputs for the character
float angle = Vector3.SignedAngle(transform.forward, direction, Vector3.up);
float xAxis = Mathf.Sin(angle * Mathf.Deg2Rad);
float yAxis = Vector3.Dot(transform.forward, direction) *
Mathf.Clamp01(direction.magnitude);
// Apply these inputs to the character controller
((UniCharacter)character.uniComponent.unityComponent).SetInput(xAxis, yAxis);
// Determine if we should be running based on distance to destination
bool running = Vector3.Distance(transform.position, agent.destination) >
agent.radius * 4;
((UniCharacter)character.uniComponent.unityComponent).SetRunningState(running);
}
This code translates the calculated path into actual movement inputs for the NPC. It’s like translating “turn right at the next corner” into the action of turning the steering wheel.
The code:
- Gets the next position along the calculated path
- Calculates the direction to that position
- Converts that direction into input values (like joystick movements)
- Applies those inputs to the character controller
- Decides whether to walk or run based on distance to destination
How the Navigation System Works
Let’s see what happens when an NPC needs to move somewhere:
sequenceDiagram
participant NPC as NPC Controller
participant BM as Behaviour Manager
participant NS as Navigation System
participant NA as NavMesh Agent
participant CH as Character Controller
BM->>NPC: Execute "Go" behaviour
NPC->>NS: SetNavigationTarget(destination)
NS->>NA: Calculate path to destination
NA-->>NS: Return path
loop Until destination reached
NPC->>NA: Get nextPosition
NPC->>NPC: Calculate direction
NPC->>CH: Apply movement inputs
CH->>NPC: Update position
NPC->>NA: Update agent position
end
NPC->>BM: Behaviour completed
This diagram shows the flow:
- The Behaviour System decides the NPC should go somewhere
- The NPC Controller asks the Navigation System to find a path to the destination
- The NavMesh Agent calculates the path
- In a continuous loop, the NPC follows the path by:
- Getting the next position along the path
- Calculating the direction to move
- Applying movement inputs to the character controller
- Updating the position based on the controller’s movement
- When the destination is reached, the Navigation System informs the Behaviour System
Creating a Simple Navigation System
Let’s put everything together to create a navigation system for our game:
Step 1: Setting Up the Navigation Mesh
First, we need to create a NavMesh for our level:
void SetupLevelNavigation()
{
// Create navigation mesh builder settings
NavMeshBuildSettings buildSettings = new NavMeshBuildSettings()
{
agentRadius = 0.5f, // NPCs are 1 meter wide
agentHeight = 2.0f, // NPCs are 2 meters tall
agentClimb = 0.4f, // NPCs can step up 0.4 meters
agentSlope = 45 // NPCs can climb slopes up to 45 degrees
};
// Generate the navigation mesh for the current scene
SceneData currentScene = RuntimeManagerBase.instance.currentSceneData;
NavMeshManager.GenerateNavMesh(currentScene, buildSettings);
}
This code creates a NavMesh for our level with specific settings:
- NPCs are 0.5 meters in radius (1 meter wide)
- NPCs are 2 meters tall
- NPCs can step up 0.4 meters (like climbing a stair)
- NPCs can walk up slopes of up to 45 degrees
Step 2: Setting Up NPCs to Use Navigation
Next, we need to configure our NPCs to use the navigation system:
void EnableNavigationForNPC(NpcController npc)
{
// Enable navigation for this NPC
npc.useNavigation = true;
// Update agent dimensions based on the NPC
npc.UpdateAgentDimensions();
// Start navigation
npc.StartNavigation();
}
This code enables navigation for an NPC and configures its NavMesh Agent based on the NPC’s dimensions.
Step 3: Making NPCs Navigate to Destinations
Now, we can make NPCs move to specific positions:
void MakeNPCGoToPosition(NpcController npc, Vector3 destination)
{
// Create a "Go" behavior targeting the destination
Behaviour goToDestination = new Behaviour(
BehaviourType.Go, // Type: Move to a position
10.0f, // Duration: Up to 10 seconds
TargetType.Point, // Target type: A point in space
destination // Target: The destination position
);
// Add this behavior to a new task
npc.manager.AddTask(1.0f).AddBehaviour(goToDestination);
}
This code creates a behavior that tells the NPC to move to a specific position. The Navigation System will automatically find the best path.
Advanced Navigation Features
The Navigation System can do more than just basic path finding. Here are some additional features:
Random Position Finding
Sometimes we want an NPC to move to a random position (like when wandering or escaping):
public (bool, Vector3) GetRandomPosition()
{
// Set a maximum search radius
float radius = 50;
// Generate a random direction and distance
Vector3 randomDirection = Random.insideUnitSphere * radius;
randomDirection += transform.position;
// Find a valid position on the NavMesh
if (NavMesh.SamplePosition(randomDirection, out NavMeshHit hit, radius, 1))
{
return (true, hit.position);
}
// No valid position found
return (false, Vector3.zero);
}
This method finds a random position within a certain radius that’s on the NavMesh (meaning an NPC can actually walk there). It’s useful for behaviors like patrolling or escaping.
Path Recalculation
Sometimes paths become invalid (for example, if obstacles move). We can force path recalculation:
public void RealculatePath()
{
if (agent != null)
{
agent.CalculatePath(agent.nextPosition, agent.path);
}
}
This method forces the NavMesh Agent to recalculate its path, which can help when the NPC is stuck or when the environment has changed.
Following Moving Targets
When following a moving target (like another character), we need to update the destination regularly:
private void UpdateFollowingTarget()
{
// Only update every 0.2 seconds to save performance
if (Time.time < nextTargetUpdate)
{
return;
}
nextTargetUpdate = Time.time + 0.2f;
// Get current behavior
Behaviour behaviour = manager?.task?.behaviour;
// If following a character target, update destination to their current position
if (behaviour != null &&
behaviour.type == BehaviourType.Go &&
behaviour.targetType == TargetType.Character &&
behaviour.data != null)
{
UniObjectData data = (UniObjectData)behaviour.data;
if (data != null && data.uniObject != null)
{
SetNavigationTarget(data.uniObject.transform.position);
}
}
}
This method checks if the NPC is following a character, and if so, updates the navigation destination to that character’s current position every 0.2 seconds.
Common Navigation Challenges and Solutions
Challenge: NPCs Getting Stuck
Sometimes NPCs can get stuck on complex geometry or when paths become invalid:
void CheckIfStuck()
{
// If we haven't moved much in the last second, we might be stuck
if (Vector3.Distance(lastPosition, transform.position) < 0.1f)
{
stuckTime += Time.deltaTime;
// If stuck for more than 2 seconds, try to get unstuck
if (stuckTime > 2.0f)
{
// Try to find a new path
RealculatePath();
// If that fails, find a random nearby position to move to
if (stuckTime > 4.0f)
{
(bool found, Vector3 position) = GetRandomPosition();
if (found)
{
SetNavigationTarget(position);
}
stuckTime = 0;
}
}
}
else
{
stuckTime = 0;
}
lastPosition = transform.position;
}
This code checks if an NPC is stuck (not moving much) and tries to unstick it by recalculating the path or finding a new random position to move to.
Challenge: Doorways and Narrow Passages
NPCs can struggle with doorways and narrow passages. We can adjust their behavior:
void HandleNarrowPassage()
{
// Check if we're at a narrow passage (like a doorway)
if (IsAtNarrowPassage())
{
// Slow down and be more precise
agent.speed = agent.speed * 0.5f;
agent.angularSpeed = agent.angularSpeed * 1.5f;
agent.radius = agent.radius * 0.8f;
}
else
{
// Return to normal movement
agent.speed = normalSpeed;
agent.angularSpeed = normalAngularSpeed;
agent.radius = normalRadius;
}
}
This code adjusts the NPC’s movement parameters when going through narrow passages, making them move more carefully through tight spaces.
Best Practices for Using the Navigation System
-
Mark Walkable Areas Clearly: Make sure all areas where NPCs should be able to walk are included in the NavMesh.
-
Set Appropriate Agent Dimensions: Use realistic values for agent radius and height based on your character models.
-
Update Moving Target Positions Regularly: When following moving targets, update the destination position frequently (but not every frame, to preserve performance).
-
Check for Stuck NPCs: Implement logic to detect and resolve situations where NPCs get stuck.
-
Don’t Overload the Navigation System: Calculating paths can be CPU-intensive, so limit how many NPCs calculate paths simultaneously.
Integration with Other Systems
The Navigation System works closely with several other systems:
Connection to the Behaviour System
The Behaviour System decides where NPCs should go, and the Navigation System figures out how to get there:
private void Manager_OnBehaviourChangedEvent(Behaviour behaviour)
{
if (behaviour == null) return;
// Remember the current behavior
lastBehaviour = behaviour;
// Handle different behavior types
switch (behaviour.type)
{
case BehaviourType.Escape:
// Start navigation and set destination to escape point
StartNavigation();
SetNavigationTarget((Vector3)behaviour.data);
break;
case BehaviourType.Go:
// Just start navigation (destination will be set separately)
StartNavigation();
break;
case BehaviourType.Wait:
// Stop navigation when waiting
StopNavigation();
break;
// More behavior types...
}
}
This method is called when the NPC’s behavior changes, and it configures the Navigation System accordingly.
Connection to the Target System
The Target System identifies targets, and the Navigation System helps NPCs reach those targets:
void GoToTarget(UniObjectData target)
{
if (target != null && target.uniObject != null)
{
// Start navigation
StartNavigation();
// Set destination to target's position
SetNavigationTarget(target.uniObject.transform.position);
}
}
This method makes an NPC navigate to a target identified by the Target System.
Conclusion
The Navigation System is like a GPS for your NPCs, helping them find their way around the game world. It provides the crucial ability to calculate paths, avoid obstacles, and move intelligently, making your AI characters feel much more realistic.
By combining the Navigation System with the Behaviour System, the NPC Personality System, the Factor Monitoring System, and the Target System, we create NPCs that can:
- Decide where they want to go (Behaviour System)
- Find important objects to interact with (Target System)
- Navigate intelligently to reach those objects (Navigation System)
- Respond appropriately to changing conditions (Factor Monitoring System)
- Display unique personality traits through their movement (Personality System)
In the next chapter, Task Management, we’ll learn how to organize complex sequences of behaviors into coherent tasks, giving our NPCs the ability to accomplish multi-step goals like “patrol the area and report any intruders.”
Generated by AI Codebase Knowledge Builder