Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 17

Implementing a Patrol State with PathfindingService

Recommended resources

Hello! Welcome back to our course on Finite State Machines.

In our last lesson, we successfully implemented the Idle state and a timed transition. Your NPC now waits patiently for a few seconds and then transitions into an empty Patrol state. Today, we're going to bring that Patrol state to life.

The learning outcome for this lesson is to implement the 'Patrol' state, using PathfindingService to move the NPC between a series of waypoints. You will learn how to make your NPC navigate the game world intelligently, forming the foundation for more complex behaviors like chasing and fleeing.

What is Pathfinding?

Imagine you want an NPC to walk from point A to point B. The simplest way is to move it in a straight line. But what if there's a wall in the way? The NPC would walk straight into it, which isn't very intelligent.

Pathfinding is the process of finding a walkable route from a starting point to a destination while avoiding obstacles. Roblox provides a built-in PathfindingService that handles all the complex calculations for you. It generates a series of points, called waypoints, that create a path around obstacles. Our NPC will then move from one waypoint to the next until it reaches its goal.

Roblox NPC Waypoint Patrol
An NPC following a patrol route defined by a series of green waypoint parts. This is what we will build today.

Core Concepts of PathfindingService

To get a solid visual understanding of how PathfindingService works, the following video provides a clear explanation of the basic concepts and shows how to generate and visualize a path.

Pathfinding (AI, Dynamic Paths, Modifiers) - Roblox Advanced Scripting #35 (2025)

Watch the first part of this video from BrawlDev. It introduces the key components of pathfinding in Roblox.

Watch from the introduction up to 9:45. Focus on: What pathfinding is and why it's useful (0:50). How to get the service (game:GetService("PathfindingService")) and create a path object (PathfindingService:CreatePath()) (3:30). The concepts of Path:ComputeAsync() to calculate the route and Path:GetWaypoints() to retrieve the points that form that route (5:25). How the video visualizes the waypoints, which helps in understanding what a 'path' is made of (8:30).

To summarize the key steps for generating a path:

  1. Get the Service: local PathfindingService = game:GetService("PathfindingService")
  2. Create a Path Object: local path = PathfindingService:CreatePath()
  3. Compute the Path: path:ComputeAsync(startPosition, endPosition) calculates the route.
  4. Get the Waypoints: local waypoints = path:GetWaypoints() returns a table of points for the NPC to follow.

Making the NPC Move Sequentially

Once we have a table of waypoints, we need to tell our NPC's Humanoid to move through them one by one. The core mechanism for this is a combination of the Humanoid:MoveTo() function and the Humanoid.MoveToFinished event.

  • Humanoid:MoveTo(position) tells the NPC to start walking towards a specific point.
  • Humanoid.MoveToFinished:Connect(function(reached) ... end) sets up a listener that fires when the NPC either arrives at the destination (reached will be true) or gives up (reached will be false).

This combination allows us to create a chain reaction: when the NPC reaches waypoint 1, the MoveToFinished event fires, and inside that event, we tell it to move to waypoint 2. This continues until the entire path is traversed.

Let's see this specific technique in action.

Pathfinding (AI, Dynamic Paths, Modifiers) - Roblox Advanced Scripting #35 (2025)

The same video also demonstrates how to move an NPC along the generated waypoints. This is the fundamental loop for character movement.

Watch the segment from 9:45 to 10:51. Pay close attention to how humanoid:MoveTo() is used inside a loop, and how humanoid.MoveToFinished:Wait() ensures the NPC reaches one waypoint before moving to the next. Note: The video uses :Wait(), which pauses the entire script. For better integration with our FSM and to avoid freezing other logic, we will use an event connection (:Connect()) instead. The underlying principle is the same.


Exercise: Implementing the Patrol State

Now, let's integrate this pathfinding logic into the FSM you've been building. We will modify the patrolState so that the NPC walks through a series of predefined patrol points.

1. Set up your Patrol Route
First, we need to define the path our NPC will patrol.

  • In the Workspace, create a Folder. Rename it Waypoints.
  • Inside this Waypoints folder, create at least three Parts. Name them Waypoint1, Waypoint2, Waypoint3, etc.
  • Position these parts around your game world to create a patrol route. Make sure they are Anchored. The NPC will walk to them in numerical order.

2. Update the NPC Script
Open the Script inside your NPC. We'll modify it to include the pathfinding logic. The code below builds directly on the script from our last lesson. The new parts are clearly marked.

-- Requiring your FSM module (adjust the path as needed)
local FSM = require(game.ReplicatedStorage.FSM) 
local RunService = game:GetService("RunService")
local PathfindingService = game:GetService("PathfindingService") -- NEW: Get the service

-- NPC and Humanoid references
local npc = script.Parent
local humanoid = npc:WaitForChild("Humanoid")

-- == NEW: PATROL CONFIGURATION ==
local waypointsFolder = workspace:WaitForChild("Waypoints")
local patrolPoints = waypointsFolder:GetChildren()
table.sort(patrolPoints, function(a, b) return a.Name < b.Name end) -- Ensure they are in order (Waypoint1, Waypoint2...)

local currentPatrolIndex = 1
local moveToFinishedConnection -- This will hold our event connection so we can clean it up later

-- A function to handle moving to the next patrol point
local function followPathTo(destination)
    -- Create a path object
    local path = PathfindingService:CreatePath()

    -- Compute the path asynchronously to prevent yielding
    local success, errorMessage = pcall(function()
        path:ComputeAsync(npc.PrimaryPart.Position, destination)
    end)

    if success and path.Status == Enum.PathStatus.Success then
        local waypoints = path:GetWaypoints()
        
        -- Disconnect any previous connection to avoid duplicates
        if moveToFinishedConnection then
            moveToFinishedConnection:Disconnect()
        end

        -- Move through the waypoints of the generated path
        for i, waypoint in ipairs(waypoints) do
            humanoid:MoveTo(waypoint.Position)

            -- If the waypoint requires a jump, make the humanoid jump
            if waypoint.Action == Enum.PathWaypointAction.Jump then
                humanoid.Jump = true
            end

            -- Wait until the humanoid reaches the waypoint or times out
            humanoid.MoveToFinished:Wait()
        end

        -- Once the entire path to the destination is complete, move to the next patrol point
        print("Reached patrol point #" .. currentPatrolIndex)
        currentPatrolIndex = (currentPatrolIndex % #patrolPoints) + 1 -- Loop back to 1
        
        -- Trigger the next leg of the patrol
        patrolState.OnPathComplete()

    else
        warn("Cannot compute path to destination: " .. tostring(errorMessage))
        -- Optional: Handle failure, e.g., wait and try again
    end
end

-- == STATE DEFINITIONS ==
-- Keep your Idle state from the previous lesson
local IDLE_DURATION = 5 
local idleStartTime
local idleState = {
    Enter = function()
        print("NPC has entered the Idle state.")
        idleStartTime = time() 
    end,
    Update = function(dt)
        if time() - idleStartTime >= IDLE_DURATION then
            fsm:transitionTo("Patrol")
        end
    end,
    Exit = function()
        print("NPC is leaving the Idle state.")
        idleStartTime = nil 
    end
}

-- Update the Patrol state with our new logic
local patrolState = {
    Enter = function()
        print("NPC has entered the Patrol state.")
        -- Start patrolling to the first point in the sequence
        local targetPoint = patrolPoints[currentPatrolIndex]
        followPathTo(targetPoint.Position)
    end,
    
    Update = function(dt)
        -- Our movement is handled by events, so this can be empty for now.
    end,
    
    Exit = function()
        print("NPC is leaving the Patrol state.")
        -- CRITICAL: Disconnect the event when we leave the state to prevent memory leaks
        -- and stop the NPC from continuing its patrol when it should be doing something else (like chasing).
        if moveToFinishedConnection then
            moveToFinishedConnection:Disconnect()
            moveToFinishedConnection = nil
        end
        -- Optional: Make the humanoid stop its current movement
        humanoid:MoveTo(npc.PrimaryPart.Position) 
    end,

    -- Custom function for our state to handle path completion
    OnPathComplete = function()
        -- Move to the next point in the patrol sequence
        local nextTarget = patrolPoints[currentPatrolIndex]
        followPathTo(nextTarget.Position)
    end
}


-- == FSM INITIALIZATION (should be the same as last lesson) ==
local fsm = FSM.new("Idle") 

fsm:defineState("Idle", idleState)
fsm:defineState("Patrol", patrolState)
-- fsm:defineTransition("Idle", "Patrol") -- If your FSM needs this

-- Start the FSM's update loop
RunService.Heartbeat:Connect(function(dt)
    fsm:update(dt)
end)

A Note on the Logic: The provided code uses humanoid.MoveToFinished:Wait(). This is simple and effective for this example because our followPathTo function runs in a separate thread started by the Enter callback. A more advanced, event-based approach would use :Connect() to avoid the Wait() and manage the waypoint index inside the connected function, which you might explore as you get more comfortable.

3. Test Your Implementation
Run the simulation. You should observe the following:

  1. The NPC prints "NPC has entered the Idle state."
  2. After 5 seconds, it prints "NPC is leaving the Idle state." and "NPC has entered the Patrol state."
  3. The NPC begins walking towards Waypoint1. If there are obstacles, it will navigate around them.
  4. Upon reaching Waypoint1, the output will show "Reached patrol point #1", and the NPC will start moving towards Waypoint2.
  5. This continues until it reaches the last waypoint, after which it will loop back and head towards Waypoint1 again.

Conclusion

Fantastic work! You have now implemented a core AI behavior. Your NPC is no longer static but moves with purpose through its environment.

Here are the key takeaways from this lesson:

  • PathfindingService is Roblox's tool for calculating routes that avoid obstacles.
  • A path is composed of waypoints. NPCs move sequentially from one waypoint to the next.
  • The Humanoid:MoveTo() function and Humanoid.MoveToFinished event are the primary tools for scripting NPC movement along a path.
  • When integrating this into an FSM, the Enter callback is the ideal place to start the movement, and the Exit callback is crucial for cleanup, especially for disconnecting event listeners to prevent bugs and memory leaks.

Next Steps

Your NPC can now patrol and idle. In the next lesson, we'll make it react to the player. We will implement player proximity detection to trigger a transition from 'Patrol' to a new 'Chase' state, making our AI much more interactive and dynamic.