How to Make Pathfinding NPCs with PathfindingService in Roblox

Learn how to create intelligent NPCs that navigate around obstacles using Roblox's PathfindingService to bring life to your games.

Creating NPCs that can intelligently navigate your game world is one of those features that makes your Roblox game feel truly alive. Whether you’re building a zombie survival game, a chase sequence, or helpful quest-givers, understanding PathfindingService is essential. In this tutorial, we’ll dive deep into how pathfinding works in Roblox and build a complete NPC that can chase players around obstacles.

What is PathfindingService?

PathfindingService is Roblox’s built-in solution for calculating routes between two points while avoiding obstacles. Instead of NPCs walking straight through walls or getting stuck on objects, PathfindingService analyzes your game’s geometry and finds a walkable path.

The service uses an algorithm to examine the 3D space and creates a series of waypoints—coordinates the NPC should walk toward in sequence. Think of it like GPS navigation: you get turn-by-turn directions rather than just pointing toward your destination.

Setting Up Your NPC

Before we implement pathfinding, we need a basic NPC structure. For this tutorial, we’ll assume you have a Rig (R15 or R6) in the Workspace. Let’s start with the foundation:

local PathfindingService = game:GetService("PathfindingService")
local npc = workspace.NPC -- Your NPC model
local humanoid = npc:WaitForChild("Humanoid")
local rootPart = npc:WaitForChild("HumanoidRootPart")

local function findNearestPlayer()
    local nearestPlayer = nil
    local shortestDistance = math.huge
    
    for _, player in pairs(game.Players:GetPlayers()) do
        if player.Character then
            local character = player.Character
            local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
            
            if humanoidRootPart then
                local distance = (rootPart.Position - humanoidRootPart.Position).Magnitude
                if distance < shortestDistance then
                    shortestDistance = distance
                    nearestPlayer = player
                end
            end
        end
    end
    
    return nearestPlayer
end

This code establishes references to the core services and components we'll need. The findNearestPlayer() function will help our NPC decide who to chase—useful for games with multiple players.

Creating a Path

Now for the exciting part: creating an actual path. PathfindingService works by creating a Path object that you can compute and then extract waypoints from:

local function createPath()
    local path = PathfindingService:CreatePath({
        AgentRadius = 2,
        AgentHeight = 5,
        AgentCanJump = true,
        AgentCanClimb = false,
        WaypointSpacing = 4,
        Costs = {
            Water = 20
        }
    })
    
    return path
end

Let's break down these parameters because they're crucial to how your NPC behaves:

  • AgentRadius: How wide your NPC is. A larger radius means the path will avoid narrower gaps.
  • AgentHeight: The height of your NPC. Important for ensuring paths don't go under low ceilings.
  • AgentCanJump: Whether the NPC can jump over obstacles. Setting this to true allows more complex paths.
  • AgentCanClimb: Whether the NPC can use TrussParts to climb. Useful for platformer-style games.
  • WaypointSpacing: Distance between waypoints. Lower values create smoother but more computation-heavy paths.
  • Costs: A table that assigns "costs" to different materials or regions. Higher costs make the pathfinding avoid those areas when possible.

Computing and Following a Path

Creating a path object is just the first step. We need to compute a specific route to a target, then make our NPC follow it:

local function followPath(destination)
    local path = createPath()
    
    local success, errorMessage = pcall(function()
        path:ComputeAsync(rootPart.Position, destination)
    end)
    
    if not success then
        warn("Path computation failed: " .. errorMessage)
        return
    end
    
    if path.Status == Enum.PathStatus.Success then
        local waypoints = path:GetWaypoints()
        
        for i, waypoint in ipairs(waypoints) do
            if waypoint.Action == Enum.PathWaypointAction.Jump then
                humanoid.Jump = true
            end
            
            humanoid:MoveTo(waypoint.Position)
            humanoid.MoveToFinished:Wait()
        end
    else
        warn("Path not computed successfully")
    end
end

This is where the magic happens! Let's understand the flow:

  1. We create a path object with our desired parameters
  2. We use ComputeAsync() wrapped in pcall for error handling—path computation can fail if positions are invalid
  3. We check if the path status is successful
  4. We retrieve waypoints and iterate through them
  5. For each waypoint, we check if jumping is required and make the humanoid move to that position
  6. We wait for MoveToFinished before proceeding to the next waypoint

The MoveToFinished event is crucial—without waiting for it, the NPC would try to move to all waypoints simultaneously, which doesn't work.

Making It Continuous

Right now, our NPC follows a path once and stops. Let's make it continuously chase the nearest player:

local function startChasing()
    while task.wait(0.5) do
        local targetPlayer = findNearestPlayer()
        
        if targetPlayer and targetPlayer.Character then
            local targetRoot = targetPlayer.Character:FindFirstChild("HumanoidRootPart")
            
            if targetRoot then
                local distance = (rootPart.Position - targetRoot.Position).Magnitude
                
                -- Only pathfind if player is within range but not too close
                if distance > 5 and distance < 100 then
                    followPath(targetRoot.Position)
                elseif distance <= 5 then
                    -- Stop moving if too close
                    humanoid:MoveTo(rootPart.Position)
                end
            end
        end
    end
end

-- Start the chasing behavior
startChasing()

This creates a loop that recalculates the path every 0.5 seconds. We add distance checks to avoid unnecessary pathfinding when the player is very close or too far away.

Handling Path Blocking

Sometimes paths become blocked while the NPC is following them—a door closes, a platform moves, or terrain changes. We should handle this gracefully:

local function followPathWithBlocking(destination)
    local path = createPath()
    
    local success, errorMessage = pcall(function()
        path:ComputeAsync(rootPart.Position, destination)
    end)
    
    if not success or path.Status ~= Enum.PathStatus.Success then
        return false
    end
    
    local waypoints = path:GetWaypoints()
    local blockedConnection
    local reachedConnection
    local pathBlocked = false
    
    blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
        if blockedWaypointIndex > 1 then
            pathBlocked = true
        end
    end)
    
    for i, waypoint in ipairs(waypoints) do
        if pathBlocked then
            break
        end
        
        if waypoint.Action == Enum.PathWaypointAction.Jump then
            humanoid.Jump = true
        end
        
        humanoid:MoveTo(waypoint.Position)
        humanoid.MoveToFinished:Wait()
    end
    
    blockedConnection:Disconnect()
    return not pathBlocked
end

The path.Blocked event fires when the path becomes obstructed. By connecting to this event, we can exit the pathfinding loop early and recalculate a new path in the main loop.

Optimizing Performance

Pathfinding can be computationally expensive, especially with many NPCs. Here are key optimization strategies:

  • Increase wait time between recalculations: Not every NPC needs to recalculate every frame. 0.5-1 second intervals work well.
  • Use distance checks: Only pathfind when targets are within a reasonable range.
  • Reuse path objects when possible: Though creating them is cheap, it's still good practice.
  • Increase WaypointSpacing: For large, open areas, you don't need waypoints every 2 studs.
  • Limit active NPCs: Consider deactivating pathfinding for NPCs far from any player.

Common Pitfalls to Avoid

Through my experience with PathfindingService, here are mistakes I see frequently:

  • Not using pcall: ComputeAsync can error out, always wrap it in pcall
  • Forgetting to wait for MoveToFinished: This creates very buggy movement
  • Wrong AgentRadius/Height: Measure your NPC and set these accurately or paths will look unnatural
  • Pathfinding through walls: Ensure your walls have CanCollide enabled and aren't in folders that PathfindingService ignores
  • Not checking Path.Status: Always verify the path computed successfully before using waypoints

Recap and Next Steps

Congratulations! You've learned how to create NPCs that intelligently navigate your game world using PathfindingService. You now understand how to create paths with custom parameters, compute routes to targets, handle waypoints including jumps, manage path blocking, and optimize for performance.

To build on this foundation, consider exploring:

  • Path modifiers and regions: Create zones that NPCs prefer or avoid using PathfindingModifiers
  • Custom NPC states: Implement patrol, chase, and search behaviors with state machines
  • Smart target selection: Add line-of-sight checks so NPCs only chase players they can "see"
  • Advanced movement: Combine pathfinding with custom animations for more lifelike NPCs

Keep experimenting, and remember that great NPC behavior often comes from combining pathfinding with other game logic. Happy scripting!

Author

Leave a Reply

Your email address will not be published. Required fields are marked *