Skip to main content

Welcome to a Grasp lesson

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

Lesson 19

Implementing Player-Tracking AI with Pathfinding

Recommended resources

Hello and welcome back!

In our last lesson, you successfully implemented player detection, enabling your NPC to notice the player and transition from its Patrol state to a placeholder Chase state. This was a crucial step in making your AI reactive. Now, it's time to give that Chase state some teeth.

This lesson focuses on the learning outcome: Implement the 'Chase' state, using PathfindingService to follow a player, with a transition back to 'Patrol' if the player is lost. We will transform your NPC from a passive observer into an active pursuer that can intelligently navigate the game world to hunt down the player.

Roblox NPC AI: Pathfinding Chase
An NPC using pathfinding to chase a player. This is the dynamic behavior we'll build today.

From MoveTo to PathfindingService

So far, we've used Humanoid:MoveTo() to guide the NPC to static waypoints. This works well for predictable routes, but what happens when the target is a moving player and there are obstacles in the way? Simply calling MoveTo(player.Position) would cause the NPC to run straight into walls.

This is where PathfindingService comes in. It's a powerful Roblox service designed to calculate a navigable path between two points, avoiding obstacles along the way.

Roblox PathfindingService in Action
A visual representation of PathfindingService creating a path (green dotted line) for an object to navigate around obstacles (purple blocks).

The process involves three main steps:

  1. Create a Path Object: We ask PathfindingService to create a Path object that can store the path information.
  2. Compute the Path: We tell the Path object to calculate a route from the NPC's current location to the target's location using path:ComputeAsync().
  3. Follow the Waypoints: The computed path consists of a series of points, or waypoints. We then command the NPC's Humanoid to move to each waypoint in sequence until it reaches the end.

The Core Logic of a Chase

A chase is not a one-and-done path calculation. Because the player is constantly moving, the NPC must continuously update its path to adapt. The following video provides an excellent, detailed walkthrough of creating a script that makes an NPC follow the closest player using PathfindingService.

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

This video by BrawlDev is a fantastic resource that covers almost everything we need for our 'Chase' state. Pay close attention to how it handles finding a target, calculating a path, and making the character move.

Watch the segments from 1:34 to 7:51 and 9:38 to 10:51. Focus on these key concepts: 1:34 - 7:51: How to get the PathfindingService, create a path object with CreatePath(), and compute the path with ComputeAsync(). Note the use of pcall()—this is a crucial safety measure as path computation can sometimes fail. 9:38 - 10:51: How to get the list of waypoints with GetWaypoints() and then loop through them, using humanoid:MoveTo() and humanoid.MoveToFinished:Wait() to traverse the path.

This video establishes the fundamental pattern for pathfinding. We will now integrate this pattern into the Update function of our chaseState.


Exercise: Implementing the 'Chase' State

Let's get practical. We will now build out the full logic for the chaseState in your NPC script.

1. Configuration and State Variables

First, let's add some configuration variables near the top of your script and prepare your chaseState table to hold data.

-- Add these near your other configuration variables
local PATHFINDING_SERVICE = game:GetService("PathfindingService")
local CHASE_MAX_DISTANCE = 100 -- How far the player can get before the NPC gives up
local PATH_RECOMPUTE_INTERVAL = 1 -- Re-calculate the path every 1 second

-- Find your chaseState table and modify it
local chaseState = {
    -- We'll store the NPC's target and other chase-related data here
    currentTarget = nil,
    lastPathRecompute = 0, -- A timer to control how often we re-calculate the path

    Enter = function(target)
        -- ...
    end,
    Update = function(dt)
        -- ...
    end,
    Exit = function()
        -- ...
    end
}

By adding currentTarget and lastPathRecompute to the chaseState table itself, we make them persistent throughout the duration of the state.

2. Enter: Acquiring the Target

When the FSM transitions to Chase, it passes the player target to the Enter function. We need to store this target and start the chase immediately.

-- Inside your chaseState table
Enter = function(target)
    print("NPC has entered the Chase state. Chasing: " .. target.Name)
    chaseState.currentTarget = target
    chaseState.lastPathRecompute = 0 -- Reset timer to force an immediate path calculation

    -- Stop any previous movement
    humanoid:MoveTo(npc.PrimaryPart.Position)
end,

3. Update: The Heart of the Chase

The Update function is where the continuous logic happens: checking if the target is still valid, checking if it's time to re-calculate the path, and executing the pathfinding.

The logic here follows the pattern you saw in the video and in many online examples, but adapted for our FSM's Update loop.

Make model chase a player with pathfinding

This DevForum post provides a concise, complete script for making a model chase a player. We will model our Update logic on this example.

Review the final code block in this forum thread. Notice the core loop: findNearestChar(), then if a character is found, ComputeAsync a path to them, get the Waypoints, and loop through them with MoveTo. Our Update function will perform the 'compute and move' part of this loop each time its timer is up.

Now, let's implement the Update function. This is the largest piece of code, so read through it carefully.

-- Inside your chaseState table
Update = function(dt)
    -- Add the delta time to our recompute timer
    chaseState.lastPathRecompute = chaseState.lastPathRecompute + dt

    local target = chaseState.currentTarget
    
    -- == Condition to EXIT the chase state ==
    -- Check if target is gone (left the game) or if their character is gone
    if not target or not target.Parent then
        print("Target lost (disconnected). Transitioning back to Patrol.")
        fsm:transitionTo("Patrol")
        return
    end

    local targetRootPart = target:FindFirstChild("HumanoidRootPart")
    if not targetRootPart then
        print("Target lost (no root part). Transitioning back to Patrol.")
        fsm:transitionTo("Patrol")
        return
    end

    -- Check if target is too far away
    local distance = (npc.PrimaryPart.Position - targetRootPart.Position).Magnitude
    if distance > CHASE_MAX_DISTANCE then
        print("Target is too far away. Transitioning back to Patrol.")
        fsm:transitionTo("Patrol")
        return
    end

    -- == CORE CHASE LOGIC ==
    -- Only recompute the path if our interval has passed
    if chaseState.lastPathRecompute >= PATH_RECOMPUTE_INTERVAL then
        chaseState.lastPathRecompute = 0 -- Reset the timer

        -- Create and compute the path
        local path = PATHFINDING_SERVICE:CreatePath()
        
        local success, errorMessage = pcall(function()
            path:ComputeAsync(npc.PrimaryPart.Position, targetRootPart.Position)
        end)

        if success and path.Status == Enum.PathStatus.Success then
            local waypoints = path:GetWaypoints()
            
            -- Follow the path
            -- We spawn a new thread so the path-following doesn't block the Update function
            coroutine.wrap(function()
                for i, waypoint in ipairs(waypoints) do
                    -- If we are no longer in the chase state, stop moving.
                    if fsm:getCurrentState() ~= "Chase" then
                        break
                    end
                    humanoid:MoveTo(waypoint.Position)
                    humanoid.MoveToFinished:Wait()
                end
            end)()
        else
            warn("Pathfinding failed: " .. tostring(errorMessage))
        end
    end
end,

Key Concepts in this Update function:

  • Guard Clauses: The checks at the top (if not target, if distance > ...) are "guard clauses." They ensure the target is still valid and within range. If not, we transition out of the Chase state immediately.
  • Timed Re-computation: We use lastPathRecompute and dt (delta time) to ensure we only run the expensive ComputeAsync function once per second, not on every frame.
  • Coroutines: The coroutine.wrap is important. Following a path with MoveToFinished:Wait() can take several seconds. If we did this directly in Update, it would freeze all other state logic. Running it in a coroutine allows the path-following to happen in the "background" while the Update function continues to run every frame, checking distances and timers.

4. Exit: Cleaning Up

Finally, when we leave the Chase state, we should clean up by resetting our state variables and stopping any current movement.

-- Inside your chaseState table
Exit = function()
    print("NPC is leaving the Chase state.")
    chaseState.currentTarget = nil -- Clear the target
    
    -- Stop the NPC from walking to its last waypoint
    humanoid:MoveTo(npc.PrimaryPart.Position)
end,

5. Test Your Complete Chase State

It's time to see your creation in action!

  1. Run the simulation. The NPC should start in Idle, then switch to Patrol.
  2. Walk your character into the NPC's line of sight to trigger the Chase state.
  3. The NPC should now actively follow you, navigating around any obstacles you place in its path.
  4. Run away from the NPC. Once you are more than 100 studs away (CHASE_MAX_DISTANCE), the NPC should stop, print a message, and transition back to the Patrol state.
  5. Try to get the NPC to chase you again to confirm it can re-enter the Chase state correctly.

Conclusion

Fantastic work! You have now implemented a robust and dynamic Chase state. Your NPC is no longer a simple automaton but a reactive agent that can pursue a target through a complex environment and make decisions based on changing conditions.

Key Takeaways:

  • PathfindingService is the standard tool in Roblox for creating intelligent movement that avoids obstacles.
  • Chasing a moving target requires periodic path re-computation, which should be timed to avoid performance issues.
  • A state's Update function is used for continuous checks, such as verifying a target's distance, which can trigger a transition out of the current state.
  • Using coroutines is essential for handling long-running actions (like following a path) without blocking the main update loop of your FSM.

Next Steps

Your NPC can now chase a player, but what happens when it catches them? In our next lesson, we will answer that question by implementing an 'Attack' state, triggered when the NPC is within attack range during the 'Chase' state. This will complete the core "Patrol -> Chase -> Attack" loop that is fundamental to so many game AI.