Skip to main content

Welcome to a Grasp lesson

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

Lesson 21

Implementing Flee Behavior with Pathfinding

Recommended resources

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

In the previous lesson, you built a complete combat loop for your NPC, implementing the Attack state and a Cooldown state. Your NPC can now patrol, chase down a player, engage in combat, and return to its duties. This is a fantastic foundation for a game AI.

This lesson addresses the final learning outcome in our NPC AI module: Implement a 'Flee' state triggered by a low health condition, using PathfindingService to move to a safe point.

We'll be adding a layer of self-preservation to the NPC. Instead of fighting to the bitter end, it will recognize when it's losing and try to escape. This is a common behavior that makes AI seem more intelligent and believable.

Just like the ghosts in Pac-Man have a state for running away when Pac-Man eats a power pill, our NPC will have a state for running away when its health gets too low.

Pac-Man Ghost FSM Diagram
A classic example of an FSM in games. The 'Run away from Pac-Man' state is a high-priority behavior triggered by a specific event, similar to what we will build today.

1. The Trigger: When to Flee?

First, we need to decide when the NPC should flee. The trigger is low health. We could check the NPC's health in every state's Update function, but this is inefficient and repetitive. A much better approach in Roblox is to use the Humanoid.HealthChanged event. This event fires only when the health value changes, making it the perfect trigger for our transition.

Let's define a health threshold. If the NPC's health drops below this, it's time to run.

Exercise: Set up the Health-Based Trigger

  1. Add a new configuration variable at the top of your NPC script:

    local FLEE_HEALTH_THRESHOLD = 30 -- The health percentage below which the NPC will flee
    
  2. At the end of your script, after you've defined the humanoid variable, add the following code to listen for health changes. This logic should exist outside any specific state.

    -- Listen for health changes to trigger the Flee state
    humanoid.HealthChanged:Connect(function(currentHealth)
        local healthPercentage = (currentHealth / humanoid.MaxHealth) * 100
        
        -- Only trigger if health is low AND we are not already fleeing
        if healthPercentage <= FLEE_HEALTH_THRESHOLD and fsm:getCurrentState() ~= "Flee" then
            print("Health is critical! Transitioning to Flee state.")
            fsm:transitionTo("Flee")
        end
    end)
    

This event connection ensures that no matter what state the NPC is in—Patrol, Chase, or Attack—it will immediately try to flee if its health becomes critical.


2. The Destination: Where to Flee?

An NPC fleeing needs a destination. For this exercise, we'll create a set of pre-defined "safe points" in our game world. When the NPC decides to flee, it will find the closest safe point and run towards it.

Exercise: Create Safe Points

  1. In the Roblox Studio Explorer, create a Folder inside the Workspace.
  2. Name this folder SafePoints.
  3. Inside this folder, create a few Parts. These will be your safe locations.
  4. Position these parts around your map. Good locations might be behind buildings, in alleys, or in areas far from the main patrol route.
  5. Make sure these parts are Anchored and their CanCollide property is turned off. They are just markers, not physical objects.

An example setup with a 'SafePoints' folder in the Workspace containing several designated safe location parts.


3. The Implementation: The Flee State

Now we'll build the fleeState itself. Its job is to find the nearest safe point and use PathfindingService to get there. Since you've already implemented pathfinding for the Patrol and Chase states, the process will be very familiar.

Exercise: Implement the Flee State

First, add the new state to your states table.

local states = {
    Idle = idleState,
    Patrol = patrolState,
    Chase = chaseState,
    Attack = attackState,
    Cooldown = cooldownState,
    Flee = fleeState, -- Add this line
}

Now, let's define fleeState. Add this code block alongside your other state definitions.

-- Define where to find the safe points
local safePointsFolder = workspace:WaitForChild("SafePoints")

local fleeState = {
    path = nil,
    waypoints = nil,
    currentWaypointIndex = 0,
    connections = {},

    Enter = function()
        print("NPC entered Flee state.")

        -- 1. Find the closest safe point
        local closestPoint, minDistance = nil, math.huge
        for _, point in ipairs(safePointsFolder:GetChildren()) do
            local distance = (npc.PrimaryPart.Position - point.Position).Magnitude
            if distance < minDistance then
                minDistance = distance
                closestPoint = point
            end
        end

        if not closestPoint then
            warn("No safe points found! NPC cannot flee. Returning to Idle.")
            fsm:transitionTo("Idle")
            return
        end
        
        print("Fleeing towards safe point: " .. closestPoint.Name)

        -- 2. Compute the path to the safe point
        fleeState.path = PathfindingService:CreatePath()
        local success, errorMessage = pcall(function()
            fleeState.path:ComputeAsync(npc.PrimaryPart.Position, closestPoint.Position)
        end)

        if not success or fleeState.path.Status ~= Enum.PathStatus.Success then
            warn("Pathfinding failed to find a path to the safe point.", errorMessage)
            fsm:transitionTo("Idle") -- Fallback if no path can be found
            return
        end

        -- 3. Start following the path
        fleeState.waypoints = fleeState.path:GetWaypoints()
        fleeState.currentWaypointIndex = 1 -- Start at the first waypoint
        
        -- Connect events for movement
        fleeState.connections.reached = humanoid.MoveToFinished:Connect(fleeState.onWaypointReached)
        fleeState.connections.blocked = fleeState.path.Blocked:Connect(fleeState.onPathBlocked)
        
        -- Move to the first waypoint
        fleeState.onWaypointReached(true)
    end,

    onWaypointReached = function(reached)
        -- Check if movement was successful and if there are more waypoints
        if reached and fleeState.currentWaypointIndex < #fleeState.waypoints then
            fleeState.currentWaypointIndex += 1
            local waypoint = fleeState.waypoints[fleeState.currentWaypointIndex]
            humanoid:MoveTo(waypoint.Position)
            if waypoint.Action == Enum.PathWaypointAction.Jump then
                humanoid.Jump = true
            end
        elseif reached then
            -- We've reached the final waypoint
            print("NPC reached the safe point. Hiding for a moment.")
            fleeState.Exit() -- Manually call exit to clean up
            
            -- Wait for a few seconds before returning to patrol
            task.wait(5)
            if fsm:getCurrentState() == "Flee" then -- Check if we're still in this state
               fsm:transitionTo("Patrol")
            end
        end
    end,
    
    onPathBlocked = function(blockedWaypointIndex)
        -- If the path ahead is blocked, re-calculate the path
        if blockedWaypointIndex >= fleeState.currentWaypointIndex then
            print("Flee path blocked, recalculating.")
            fleeState.Exit() -- Clean up old path
            fleeState.Enter() -- Re-run the enter logic to find a new path
        end
    end,

    Update = function(dt)
        -- Most logic is event-driven, but you could add checks here
        -- For example, check if health has recovered.
    end,

    Exit = function()
        -- Disconnect all events to prevent memory leaks
        for _, connection in pairs(fleeState.connections) do
            connection:Disconnect()
        end
        table.clear(fleeState.connections)

        -- Stop any movement
        humanoid:MoveTo(npc.PrimaryPart.Position)
        print("NPC is leaving Flee state.")
    end,
}

This state does three key things: finds the closest safe point, calculates a path, and moves the NPC along that path using the same MoveToFinished event pattern from your Patrol state.


4. Making the Escape "Smarter"

Currently, PathfindingService will find the shortest path to the safe point. But what if the shortest path leads the NPC right past the player who is trying to attack it? A smarter NPC would prefer a safer, more discreet route, even if it's longer.

We can achieve this by using pathfinding costs. By assigning higher costs to "dangerous" areas, we can tell the PathfindingService to avoid them.

The following video and documentation explain this concept clearly.

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

This video from BrawlDev shows how to use material costs and Pathfinding Modifiers to influence an NPC's path. Pay close attention to how it makes the NPC prefer one path over another.

Watch from 16:43 to 19:53 to understand how setting costs for materials (like Wood vs. Neon) can create a 'smarter' path. Then, watch from 21:16 to 23:36 to see how a PathfindingModifier object can create a 'danger zone' that the NPC will avoid.

Let's apply this. We'll create a special PathfindingModifier to label a "Danger Zone" around the player. When the NPC flees, it will try to avoid this zone.

Exercise: Implement a Dynamic Danger Zone

  1. Create the Modifier Part: Create a Part and name it DangerZone. Parent it to your NPC model. This part will move with the NPC and represent the area it considers dangerous (i.e., the area around itself).

    • Set Anchored to false.
    • Set CanCollide to false.
    • Set Transparency to 0.8.
    • Set Size to something large, like 50, 10, 50. This is the radius of danger.
    • Weld this part to the NPC's HumanoidRootPart so it moves with the NPC.
  2. Add the PathfindingModifier: Inside the DangerZone part, insert a PathfindingModifier object. In its properties, set the Label to Danger.

  3. Update the Path Creation: We need to tell the PathfindingService about this new cost. We'll modify how we create the path in the fleeState.

    Find this line in fleeState.Enter:
    fleeState.path = PathfindingService:CreatePath()

    Replace it with this:

    -- Define the agent parameters with costs
    local agentParams = {
        Costs = {
            -- Any area with the 'Danger' label is extremely undesirable
            Danger = 50 
        }
    }
    fleeState.path = PathfindingService:CreatePath(agentParams)
    

Now, when ComputeAsync is called for the flee path, the pathfinding algorithm will see a high cost associated with the area immediately around the NPC and will strongly prefer routes that go around this zone. This encourages it to find a path away from its current dangerous location, rather than just the shortest path to safety.

For more details on PathfindingModifier and Costs, the official documentation is an excellent resource.

Character pathfinding | Documentation - Roblox Creator Hub

The official Roblox documentation provides a detailed look at how to use PathfindingModifier and the Costs table to create intelligent pathing.

Read the section titled Pathfinding modifiers. It covers both setting costs by material (Set material costs) and by defined regions using a PathfindingModifier object (Work with regions), which is exactly what we just implemented.


Conclusion

Fantastic work! You have now implemented a Flee state, giving your NPC a crucial self-preservation instinct. It not only recognizes danger but also uses advanced pathfinding techniques to find a safer escape route.

Key Takeaways:

  • Event-Driven Transitions: Using events like Humanoid.HealthChanged is a powerful and efficient way to trigger state changes from anywhere in your FSM.
  • Defining Goals: The Flee state's effectiveness depends on having well-defined goals (our "Safe Points").
  • Smarter Pathfinding with Costs: You can move beyond the "shortest path" and guide your NPC's movement by using PathfindingModifier and Costs to define desirable and undesirable areas.

You have now built a very capable NPC AI with a range of behaviors. In the next module, we will switch gears and see how these same FSM principles can be applied to a completely different problem: managing UI Flow.