§ How to · with Picoo

How to make a Roblox pathfinding NPC (with AI)

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

A Roblox pathfinding NPC needs five pieces: PathfindingService waypoint generation, a state machine (idle/chase/attack), obstacle handling, stuck detection, and animation hooks. Describe the NPC you want and Picoo writes the scripts for it. The build below is one example of that output.

Example build: 5 files · 156 lines. Picoo writes this code for each prompt, so file names and line counts vary from run to run.

PathfindingService waypoints

While chasing, each pass of the brain loop computes a path to the target's current position with PathfindingService:CreatePath, walks its waypoints with Humanoid:MoveTo, then waits 0.5 seconds before the next pass.

State machine

Idle, chase and attack, stored in a State attribute on the NPC model. A player inside sight range starts a chase; a player inside attack range switches it to attack.

Obstacle handling

AgentRadius and AgentHeight come from each NPC's config, so the path fits its body and routes around walls. AgentCanJump is on, and the NPC jumps at waypoints marked Jump.

Stuck-detection recovery

A separate script watches each chasing NPC. If it moves less than 2 studs in 3 seconds, the NPC walks to a random nearby point and computes a new path from there.

Nearest-player targeting

findTarget picks the closest player whose character is inside the NPC's sightRange. For a vision cone or a line-of-sight raycast, ask for it in your prompt.

Animation hooks

State changes go out on an NPCStateChange RemoteEvent, so a client script can play walk, attack and idle animations to match.

Example output from a build like this

5 files in this example. Picoo writes this code for your prompt, so file names and sizes vary.

ServerScriptService/NPCBrain.lua

State machine, pathfinding, target selection

ServerScriptService/NPCStuckDetector.lua

Position tracking, recovery waypoints

ReplicatedStorage/Modules/NPCConfig.lua

Per-NPC stats (speed, range, damage, sight)

ServerScriptService/NPCSpawner.lua

Tag-based NPC initialization

ReplicatedStorage/Remotes/NPCStateChange

State broadcast to clients for animation sync

Sample output: ServerScriptService/NPCBrain.lua

--!strict
-- ServerScriptService/NPCBrain.lua  (Picoo · excerpt)
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local NPCConfig = require(ReplicatedStorage.Modules.NPCConfig)

type State = "idle" | "chase" | "attack"

local function findTarget(npc: Model, sightRange: number): Player?
    local pos = npc.PrimaryPart.Position
    local closest, closestDist = nil, sightRange
    for _, player in ipairs(Players:GetPlayers()) do
        if not player.Character or not player.Character.PrimaryPart then continue end
        local d = (player.Character.PrimaryPart.Position - pos).Magnitude
        if d < closestDist then closest, closestDist = player, d end
    end
    return closest
end

local function moveToward(npc: Model, target: Vector3, config)
    local hum = npc:FindFirstChildOfClass("Humanoid") :: Humanoid
    local path = PathfindingService:CreatePath({
        AgentRadius = config.radius,
        AgentHeight = config.height,
        AgentCanJump = true,
    })
    local ok = pcall(function() path:ComputeAsync(npc.PrimaryPart.Position, target) end)
    if not ok or path.Status ~= Enum.PathStatus.Success then return end
    for _, wp in ipairs(path:GetWaypoints()) do
        if wp.Action == Enum.PathWaypointAction.Jump then hum.Jump = true end
        hum:MoveTo(wp.Position)
        hum.MoveToFinished:Wait()
    end
end

local function brain(npc: Model)
    local config = NPCConfig[npc:GetAttribute("NPCType") or "default"]
    npc:SetAttribute("State", "idle" :: State)
    while npc.Parent do
        local state = npc:GetAttribute("State") :: State
        local target = findTarget(npc, config.sightRange)
        if state == "idle" and target then npc:SetAttribute("State", "chase") end
        if state == "chase" and target then
            moveToward(npc, target.Character.PrimaryPart.Position, config)
            local dist = (target.Character.PrimaryPart.Position - npc.PrimaryPart.Position).Magnitude
            if dist < config.attackRange then npc:SetAttribute("State", "attack") end
        end
        task.wait(0.5)
    end
end

Building a Roblox pathfinding NPC

NPCs that pathfind correctly are the difference between an immersive Roblox game and a janky one. The two most common failures are NPCs that get stuck on corners and NPCs that walk straight into walls because nothing computes a route around them. The example below handles both.

In this example Picoo wrote five files. NPCBrain.lua runs a state machine per NPC (idle, chase, attack) and stores the current state in an attribute on the model. A player inside sight range starts a chase, and a player inside attack range switches the NPC to attack.

PathfindingService is the core movement primitive. The brain computes a path from the NPC's position to the target's with AgentRadius and AgentHeight taken from the NPC's config, so the route fits the NPC's body and goes around walls. AgentCanJump is on, and at any waypoint marked Jump the NPC jumps before walking on.

Stuck detection is the resilience layer. Physics can push an NPC into a wall, and a path can hug a corner too tightly. The example's stuck detector watches position over a 3-second window; if the NPC moved less than 2 studs while chasing, it walks to a random nearby point and re-routes from there.

Targeting in this example is distance only: findTarget picks the closest player inside sightRange, walls or not. For stealth gameplay, ask for a vision cone with a raycast line-of-sight check, so players behind cover or behind the NPC's back go unseen. Because Picoo writes this code for your prompt, you can put those details in the first request.

See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.

Frequently asked

Why does the NPC sometimes get stuck on corners?+

A path can hug a corner tightly enough for the NPC's body to catch on it, and physics can push an NPC off its route. The example's stuck detector watches each chasing NPC: if it has moved less than 2 studs in 3 seconds, it walks the NPC to a random nearby point and computes a new path from there.

Can I have NPCs that attack other NPCs (not just players)?+

Yes. findTarget can look for models with a CollectionService tag such as 'Enemy' or 'Friendly' instead of looping over Players. Ask for faction-based targeting in a follow-up prompt.

How do I make the NPC patrol when idle?+

Ask for a patrol route: a list of Vector3 points in NPCConfig that the idle state walks through in order, picking the route back up when a chase ends.

What about line-of-sight checks?+

The example's findTarget checks distance only, so an NPC can notice a player through a wall. For line of sight, ask for a raycast from the NPC's head to the player that ignores the NPC's own model: if the ray hits something else first, the player is hidden. A vision cone adds an angle check against the NPC's LookVector, so players behind it go unseen.

Does Picoo have a recipe for NPCs?+

Yes, npc_spawner. Prompts such as 'make a zombie that chases me' or 'spawn some patrolling NPCs' start from it. It spawns real R15 NPCs (5 by default) in Workspace.NPCs that wander around their spawn point with Humanoid:MoveTo, within a 40-stud patrol radius. Chasing, with or without PathfindingService, goes in a separate script on top of those NPCs.

Related Picoo prompts