Tutorial · Game systems

Roblox NPC pathfinding: make an NPC follow and chase players

An NPC that walks around walls to reach the nearest player needs three things: a rig that can move, a route from PathfindingService, and a loop that follows the route without fighting the physics engine. This guide builds all three and ends with one server script you can drop into an NPC rig.

Updated 30 September 202615 min readIntermediateLuau

In short

Put a Script inside the NPC model. Create a path with PathfindingService:CreatePath(), compute it with path:ComputeAsync(start, goal) inside pcall, then walk the waypoints one at a time with Humanoid:MoveTo(), waiting for MoveToFinished before the next one. To chase a player, pick the nearest character and recompute the path when they have moved a few studs, not every frame.

  • The NPC needs a Humanoid and an unanchored HumanoidRootPart. Call SetNetworkOwner(nil) on the root part so the server simulates it: that is the usual fix for an NPC that stutters near players.
  • Jump when a waypoint's Action is Jump, and compute a new path when Path.Blocked fires for a waypoint ahead of the NPC.
  • When the player is in plain sight, skip pathfinding and MoveTo them directly. Stop a few studs away, or keep going if the NPC damages on touch.
On this page
  1. How pathfinding works
  2. 1. Set up the NPC
  3. 2. Walk to a point
  4. 3. Chase script
  5. How the chase works
  6. Fix stuttering
  7. 4. Touch damage
  8. Modifiers and links
  9. Common errors
  10. With RoCode
  11. Questions
  12. Sources

How NPC pathfinding works

Roblox builds a navigation mesh of the surfaces a character could walk on. PathfindingService searches that mesh for a route between two points and returns it as a list of waypoints, each a position plus an action such as walk or jump. The NPC's Humanoid then walks from one waypoint to the next. Pathfinding only plans the route: moving along it is your script's job.

The APIs this guide uses
APIWhat it does
PathfindingService:CreatePath()Makes a reusable Path object, sized for your NPC with agent parameters.
Path:ComputeAsync(start, finish)Computes the route. It yields and can fail, so call it inside pcall and check Path.Status.
Path:GetWaypoints()Returns the route as an array of PathWaypoint values, each with a Position, Action and Label.
Path.BlockedFires with a waypoint index when something blocks the computed route.
Humanoid:MoveTo(position)Walks the NPC towards a point.
Humanoid.MoveToFinishedFires with true when the NPC gets within about a stud of the point, or false if it has not arrived after 8 seconds.

Run all of this in a server Script: the server sees the whole map even with streaming on, every player sees the same NPC, and damage is decided where a client cannot fake it. Script vs LocalScript vs ModuleScript explains the split.

Step 1: Set up an NPC that can move

Any model can follow a path if it has the three things below. Rigs from the Toolbox or older places sometimes lack one, and the usual symptom is an NPC that stands still without an error.

What the NPC model needs
RequirementWhy it matters
A Humanoid named HumanoidMoveTo, WalkSpeed and jumping all belong to the Humanoid.
A HumanoidRootPartThe part the Humanoid drives. Paths start from its position.
Nothing anchoredAn anchored part cannot move, and neither can the parts jointed to it. The server always owns anchored parts, so the stutter fix below cannot be applied to them.
  1. Open the Avatar tab and click Character to open the Rig Builder. Choose R15, then Block Avatar. A rig appears in Workspace. R6 rigs work with the same scripts.
  2. Rename the rig Chaser and move it onto open ground.
  3. Select its HumanoidRootPart and check that Anchored is off in the Properties window. Do the same for any other anchored part in the model. To spot them quickly, turn on Anchored parts in the Visualization Options widget at the upper right of the 3D viewport.
Explorer: the NPC model at the end of this guide
Workspace
  Chaser Model
    Humanoid Humanoid
    HumanoidRootPart Part
    Head, UpperTorso, LowerTorso ... (the body parts)
    ChaseAI Script new
    TouchDamage Script new

Step 2: Walk the NPC to a point

Start with the smallest script that works: walk to a fixed part. It shows every stage of pathfinding without the chasing logic.

  1. Insert a Part into Workspace, name it Goal, turn Anchored on and CanCollide off, and place it on the floor where the NPC has to walk around a wall to reach it.
  2. In the Explorer, hover over Chaser, click the + button and insert a Script. Name it WalkToGoal and paste the code below.
  3. Press Play. The NPC walks around the wall to the Goal part.
Script in the Chaser model named WalkToGoal
local PathfindingService = game:GetService("PathfindingService")

local npc = script.Parent :: Model
local humanoid = npc:WaitForChild("Humanoid") :: Humanoid
local rootPart = npc:WaitForChild("HumanoidRootPart") :: BasePart
local goalPart = workspace:WaitForChild("Goal") :: BasePart

-- The server has to own the NPC's physics for ChangeState (jumping) to work from a Script
local canSetOwner, ownerProblem = rootPart:CanSetNetworkOwnership()
if not canSetOwner then
	warn(`{npc.Name} cannot move: {ownerProblem}. Untick Anchored on every part of the rig.`)
	return
end
rootPart:SetNetworkOwner(nil)

local path = PathfindingService:CreatePath({
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
})

-- ComputeAsync yields and can throw, so it goes inside pcall
local ok, err = pcall(function()
	path:ComputeAsync(rootPart.Position, goalPart.Position)
end)

if not ok then
	warn(`Path request failed: {err}`)
elseif path.Status ~= Enum.PathStatus.Success then
	warn(`No path to the goal: {path.Status.Name}`)
else
	for index, waypoint in path:GetWaypoints() do
		if index == 1 then
			continue -- the first waypoint is where the NPC is already standing
		end
		humanoid:MoveTo(waypoint.Position)
		if waypoint.Action == Enum.PathWaypointAction.Jump then
			humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
		end
		-- Wait until this waypoint is reached before moving to the next one
		local reached = humanoid.MoveToFinished:Wait()
		if not reached then
			warn(`{npc.Name} got stuck on the way to the goal`)
			break
		end
	end
end

The Goal part has CanCollide off because a path cannot end inside a solid part: the path's Status would be NoPath.

The table passed to CreatePath describes the NPC, which Roblox calls the agent, so the route avoids gaps it cannot fit through. The script above sets the first three keys to their defaults so you can see where to change them.

Agent parameters for CreatePath
KeyDefaultWhat it controls
AgentRadius2The NPC's radius in studs. Raise it for wide NPCs so paths keep clear of walls.
AgentHeight5The NPC's height. Spaces lower than this, such as under stairs, count as blocked.
AgentCanJumptrueAllows routes that need a jump. Those waypoints have Action set to Jump.
AgentCanClimbfalseAllows climbing TrussParts. Those waypoints have the label Climb.
WaypointSpacing4Spacing in studs between the intermediate waypoints. math.huge removes them.
CostsnoneHow expensive materials and labelled regions are to cross. See modifiers and links.

Two details in the loop matter. The first waypoint is the NPC's own position, so the loop skips it. And each MoveTo is followed by MoveToFinished:Wait(): a new MoveTo replaces the previous goal, so a loop that does not wait sends the NPC straight at the last waypoint, into the wall it should walk around.

For jumps, the waypoint's Action is Jump and the script calls ChangeState(Enum.HumanoidStateType.Jumping). Roblox's Humanoid reference notes that a server script can only change a state when the server owns the character's physics, one reason the script sets the network owner first.

Roblox's pathfinding page moves a player's own character with Humanoid:Move() and a per-frame arrival check, and calls that the best way to move a Humanoid along a path. MoveTo with MoveToFinished is shorter and easier to follow for a server NPC, so this guide uses it; study the official script if you need finer control later.

Step 3: The full chase script

This is the script most people are looking for. The NPC chases the nearest player within 80 studs, walks around walls, jumps where the route needs it, finds a new route when something blocks it, and walks home when nobody is near.

  1. Delete WalkToGoal from Chaser if you added it.
  2. Hover over Chaser, click +, insert a Script named ChaseAI and paste the code below.
  3. Press Play, walk behind a wall, then run out of range. The NPC follows you around the wall, then returns to where it started.
Script in the Chaser model named ChaseAI
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")

local npc = script.Parent :: Model
local humanoid = npc:WaitForChild("Humanoid") :: Humanoid
local rootPart = npc:WaitForChild("HumanoidRootPart") :: BasePart

-- Settings (distances in studs, times in seconds)
local DETECTION_RANGE = 80 -- players further away than this are ignored
local STOP_DISTANCE = 4 -- stop this close to the target (0 = walk into them, for touch damage)
local DIRECT_CHASE_RANGE = 30 -- inside this range, skip pathfinding when nothing is in the way
local REPATH_DISTANCE = 4 -- how far the target must move before the path is recomputed
local MIN_REPATH_INTERVAL = 0.5 -- never compute paths more often than this
local WAYPOINT_TIMEOUT = 2 -- give up on a path if one waypoint takes longer than this
local CHASE_SPEED = 14 -- a player walks at 16 by default, so they can outrun this NPC
local HOME_RADIUS = 4 -- this close to home counts as being home
local RAY_HEIGHTS = { 0, -2 } -- line-of-sight rays at waist height and near the knees

local HOME = rootPart.Position -- where the NPC goes back to when nobody is in range

humanoid.WalkSpeed = CHASE_SPEED

-- Let the server simulate the NPC. Otherwise Roblox can hand its physics to a
-- nearby player's device, and the NPC stutters as that changes.
local canSetOwner, ownerProblem = rootPart:CanSetNetworkOwnership()
if not canSetOwner then
	warn(`{npc.Name} cannot move: {ownerProblem}. Untick Anchored on every part of the rig.`)
	return
end
rootPart:SetNetworkOwner(nil)

local path = PathfindingService:CreatePath({
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
	Costs = {
		Water = 20, -- wade through water only to avoid a long detour
		DangerZone = math.huge, -- never enter regions labelled DangerZone
	},
})

-- The path the NPC is following right now
local waypoints: { PathWaypoint } = {}
local nextIndex = 0
local pathGoal: Vector3? = nil
local pathBlocked = false
local lastComputeTime = -math.huge

-- Blocked can fire for a waypoint the NPC has already passed. Only a block ahead matters.
path.Blocked:Connect(function(blockedIndex: number)
	if blockedIndex >= nextIndex then
		pathBlocked = true
	end
end)

local rayParams = RaycastParams.new()
rayParams.RespectCanCollide = true -- parts nothing can collide with do not block the view

local function clearPath()
	waypoints = {}
	nextIndex = 0
	pathGoal = nil
end

local function stopMoving()
	clearPath()
	humanoid:MoveTo(rootPart.Position)
end

-- The HumanoidRootPart of the closest living player within DETECTION_RANGE, or nil
local function findNearestTarget(): BasePart?
	local nearestRoot: BasePart? = nil
	local nearestDistance = DETECTION_RANGE
	for _, player in Players:GetPlayers() do
		local character = player.Character
		local targetHumanoid = character and character:FindFirstChildOfClass("Humanoid")
		local targetRoot = character and character:FindFirstChild("HumanoidRootPart")
		if targetHumanoid and targetHumanoid.Health > 0 and targetRoot and targetRoot:IsA("BasePart") then
			local distance = (targetRoot.Position - rootPart.Position).Magnitude
			if distance < nearestDistance then
				nearestDistance = distance
				nearestRoot = targetRoot
			end
		end
	end
	return nearestRoot
end

-- True when the target is on roughly the same level and nothing solid is in between.
-- A clear ray does not prove the ground is walkable, so this is only used at short range.
local function canSeeDirectly(targetRoot: BasePart): boolean
	local offset = targetRoot.Position - rootPart.Position
	if math.abs(offset.Y) > 3 then
		return false
	end
	local character = targetRoot.Parent
	rayParams.ExcludeInstances = if character then { npc, character } else { npc }
	for _, height in RAY_HEIGHTS do
		local origin = rootPart.Position + Vector3.new(0, height, 0)
		if workspace:Raycast(origin, offset, rayParams) then
			return false
		end
	end
	return true
end

local function computePath(goal: Vector3)
	lastComputeTime = os.clock()
	local ok, err = pcall(function()
		path:ComputeAsync(rootPart.Position, goal)
	end)
	-- Reset after computing, so a Blocked event from the old route cannot mark the new one
	pathBlocked = false
	if ok and path.Status == Enum.PathStatus.Success then
		waypoints = path:GetWaypoints()
		nextIndex = 2 -- waypoint 1 is where the NPC is standing
		pathGoal = goal
	else
		if not ok then
			warn(`{npc.Name}: path request failed: {err}`)
		end
		clearPath() -- no route right now; the main loop tries again shortly
	end
end

local function needsNewPath(goal: Vector3): boolean
	if os.clock() - lastComputeTime < MIN_REPATH_INTERVAL then
		return false -- computed a moment ago: keep walking the current path
	end
	local currentGoal = pathGoal
	if currentGoal == nil or pathBlocked or nextIndex > #waypoints then
		return true
	end
	return (goal - currentGoal).Magnitude > REPATH_DISTANCE
end

-- Waits for MoveToFinished, but gives up after `timeout` seconds instead of the
-- built-in 8, so a stuck NPC gets a new path quickly
local function waitForMoveToFinished(timeout: number): boolean
	local finished = false
	local reachedGoal = false
	local connection = humanoid.MoveToFinished:Connect(function(reached: boolean)
		finished = true
		reachedGoal = reached
	end)
	local deadline = os.clock() + timeout
	while not finished and os.clock() < deadline do
		task.wait()
	end
	connection:Disconnect()
	return reachedGoal
end

local function walkToNextWaypoint()
	local waypoint = waypoints[nextIndex]
	humanoid:MoveTo(waypoint.Position)
	if waypoint.Action == Enum.PathWaypointAction.Jump then
		humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
	end
	if waitForMoveToFinished(WAYPOINT_TIMEOUT) then
		nextIndex += 1
	else
		clearPath() -- stuck or pushed off course: compute a fresh path
	end
end

while humanoid.Health > 0 do
	local targetRoot = findNearestTarget()
	local goal: Vector3

	if targetRoot then
		local distance = (targetRoot.Position - rootPart.Position).Magnitude
		if distance <= STOP_DISTANCE then
			stopMoving() -- close enough: stay next to the target
			task.wait(0.2)
			continue
		end
		if distance <= DIRECT_CHASE_RANGE and canSeeDirectly(targetRoot) then
			clearPath()
			-- Passing the part as well makes the NPC follow it as it moves
			humanoid:MoveTo(targetRoot.Position, targetRoot)
			task.wait(0.15)
			continue
		end
		goal = targetRoot.Position
	else
		if (HOME - rootPart.Position).Magnitude <= HOME_RADIUS then
			stopMoving() -- nobody around and already home
			task.wait(0.5)
			continue
		end
		goal = HOME
	end

	if needsNewPath(goal) then
		computePath(goal)
	end

	if nextIndex >= 2 and nextIndex <= #waypoints then
		walkToNextWaypoint()
	else
		task.wait(0.1) -- no usable path yet, for example the target is out of reach
	end
end

Every setting is at the top. For more NPCs, duplicate Chaser: each copy runs its own ChaseAI and remembers its own home. Older scripts filter rays with FilterDescendantsInstances; Roblox's reference now recommends ExcludeInstances for new work, which is what this script uses.

How the chase script works

Each pass of the main loop makes one decision, then does one small thing: stand still, step straight at the player, or walk to the next waypoint. Nothing is committed for longer than one waypoint, so the NPC keeps reacting.

Finding the nearest player

findNearestTarget loops through Players:GetPlayers(), skips anyone without a character, a living Humanoid or a HumanoidRootPart (players who are respawning or leaving), and returns the closest root part within DETECTION_RANGE. The search runs on every pass, so the NPC switches to a closer player by itself, and a player who leaves is simply not found again: there is no per-player state to clean up.

Recomputing as the player moves, without doing it every frame

Computing a path yields and costs server time. needsNewPath asks for one only when there is no path, the old one is finished or blocked, or the player has moved more than REPATH_DISTANCE studs from where the current path ends, and never within MIN_REPATH_INTERVAL seconds of the last one. While it computes, the NPC keeps walking to its current waypoint, and the new path is followed from waypoint 2, so the NPC does not turn back to the spot where the path started.

Blocked paths

Doors close and parts fall. Path.Blocked fires with the index of the blocked waypoint, which may be behind the NPC, so the script only reacts when blockedIndex >= nextIndex: it sets a flag and the loop computes a new route. A waypoint that takes longer than WAYPOINT_TIMEOUT seconds to reach, because the NPC was pushed or snagged, also triggers a new route.

Going straight at a visible player

Pathfinding is wasted when the player is in plain sight on open ground. Within DIRECT_CHASE_RANGE, canSeeDirectly casts two rays at the player, at waist height and near the knees to catch low walls, ignoring both characters. If neither hits anything and the player is on about the same level, the NPC calls MoveTo(targetRoot.Position, targetRoot). The second argument makes the goal move with the player, and repeating the call every 0.15 seconds keeps resetting the 8-second timeout.

A clear ray proves that nothing solid is in the way, not that there is floor to walk on. If your map has gaps or drops between the NPC and players, lower DIRECT_CHASE_RANGE, or set it to 0 to always use pathfinding.

Stopping at the right distance

When the player is within STOP_DISTANCE, the NPC calls MoveTo on its own position, which stops it. 4 studs suits an NPC that should stand beside a player; raise it to 6 or 8 for a pet or escort that keeps some space. For an enemy that hurts on touch, set it to 0 so it walks into the player, as in Step 4.

Why NPCs stutter, and how to fix it

An NPC that jerks, pauses at every waypoint or twitches backwards usually has one of the causes below. The chase script already handles each of them; the table is for fixing a script you wrote yourself.

Pathfinding stutter: causes and fixes
What you seeCauseFix
Smooth when alone, jerky once a player gets closeNetwork ownership. Roblox gives the physics of unanchored parts to the device of a nearby player, so the NPC can end up simulated on that player's device while the server gives the orders, and each handover can show as a hitch.Call SetNetworkOwner(nil) on the HumanoidRootPart once, from the server. It applies to every part connected to it.
Walks straight at the last waypoint and into wallsMoveTo called for every waypoint in a loop with no wait. Each call replaces the previous goal.Wait for MoveToFinished before moving to the next waypoint.
Stops briefly at every waypoint, or cuts cornersA fixed task.wait() between waypoints instead of waiting for arrival. Too long and the NPC stands still; too short and it skips ahead.Wait for MoveToFinished, with a timeout.
Twitches or steps backwards several times a secondA new path every frame. Each one starts at the NPC's own position, so walking to waypoint 1 turns it back.Recompute only when the target has moved a few studs, at most twice a second, and start from waypoint 2.
Freezes for several seconds, then carries onMoveToFinished:Wait() on a waypoint it cannot reach waits for the 8-second timeout.Use your own shorter timeout and compute a new path when it runs out.

To see who owns the NPC, open Visualization Options during a playtest and turn on Network owners. Parts your client simulates get a green outline; parts owned by the server or another client are outlined in white or grey.

Step 4: Damage players on touch (optional)

For an enemy, add a second script to the NPC that damages players it touches. It runs on the server, so a player's client cannot change the amount or skip the cooldown.

  1. In ChaseAI, set STOP_DISTANCE to 0 so the NPC walks into players instead of stopping beside them.
  2. Insert a Script named TouchDamage into Chaser and paste the code below.
Script in the Chaser model named TouchDamage
local Players = game:GetService("Players")

local npc = script.Parent :: Model
local humanoid = npc:WaitForChild("Humanoid") :: Humanoid

local DAMAGE = 10
local COOLDOWN_SECONDS = 1

local lastHit: { [Player]: number } = {}

local function onTouched(hit: BasePart)
	if humanoid.Health <= 0 then
		return -- a defeated NPC does no damage
	end
	local character = hit:FindFirstAncestorOfClass("Model")
	local player = character and Players:GetPlayerFromCharacter(character)
	local targetHumanoid = character and character:FindFirstChildOfClass("Humanoid")
	if not player or not targetHumanoid or targetHumanoid.Health <= 0 then
		return
	end

	local now = os.clock()
	local last = lastHit[player]
	if last and now - last < COOLDOWN_SECONDS then
		return -- Touched fires many times per second while parts overlap
	end
	lastHit[player] = now

	targetHumanoid:TakeDamage(DAMAGE)
end

for _, descendant in npc:GetDescendants() do
	if descendant:IsA("BasePart") then
		descendant.Touched:Connect(onTouched)
	end
end

Players.PlayerRemoving:Connect(function(player: Player)
	lastHit[player] = nil
end)

TakeDamage respects force fields, so players are safe while their spawn force field lasts. To ignore force fields, lower Humanoid.Health directly instead.

The cooldown is kept per player. Touched fires many times a second while parts rub together, and without the cooldown one bump could empty a player's health in a moment. Each entry is removed when its player leaves, so the table does not grow for the life of the server.

Pathfinding modifiers and links

By default a path is the shortest route, avoiding jumps where it can. Three tools change that. Costs and modifiers work with the chase script as it is, because it already passes a Costs table to CreatePath; links need a few lines of your own.

Material costs

Keys in Costs can be material names such as Water, Mud or CrackedLava, for terrain and parts alike. Every material costs 1 by default. A higher number makes the NPC avoid that material unless the detour is worse, and math.huge makes it impassable. The chase script sets Water = 20.

Regions to avoid: PathfindingModifier

  1. Cover the area with a part, with Anchored on and CanCollide off.
  2. Insert a PathfindingModifier into that part and set its Label to DangerZone.
  3. That is all. The chase script already gives DangerZone a cost of math.huge, so the NPC routes around the area. Turn on Pathfinding modifiers in Visualization Options to see the label on the navigation mesh.

A modifier with PassThrough turned on does the opposite: the route goes through a solid obstacle, such as a closed door, as if it were not there. The NPC still cannot walk through the door, but it heads for it, as if it heard the player on the other side.

A PathfindingLink connects two points the NPC could not normally travel between, such as the banks of a river it crosses by boat. Put an Attachment at each end, set the link's Attachment0 and Attachment1, give it a Label such as UseBoat, and add UseBoat to Costs with a lower cost than Water. The path then includes a waypoint with that label: check waypoint.Label in walkToNextWaypoint and run your own action, such as seating the NPC in the boat, instead of MoveTo. Links work both ways unless you turn off IsBidirectional.

Common errors and fixes

Check the Output window first. Open it from the Window menu or the Script tab. The scripts on this page print a warning when a path request fails or the rig cannot move.

The NPC does not move at all

If Output says the NPC cannot move, part of the rig is anchored: untick Anchored on HumanoidRootPart and the other parts. Otherwise, check that the code is a Script, not a LocalScript, that it sits inside the model in Workspace, and that the Humanoid has not died.

Output says "Infinite yield possible" on WaitForChild

The script is waiting for an object that does not exist: a Humanoid or HumanoidRootPart with a different name, or no part named Goal for the Step 2 script. Rename the object or the string in the script. The script errors guide explains this warning.

The path fails, or the NPC ignores a player it could reach

Path.Status is NoPath after ComputeAsync when the start or end is inside a solid part, when the only route needs a jump and AgentCanJump is false, or when AgentHeight or AgentRadius is too big for a gap on the way. Roblox also documents hard limits: at most 3,000 studs in a straight line from start to finish, and a 20,000-node search budget that huge open worlds or mazes can run out of. Turn on Navigation mesh to check the route.

The NPC slides along without moving its legs

That is an animation problem, not a pathfinding one. Player characters get an Animate script automatically, but an NPC needs its own script to play walk animations. Each NPC in Roblox's NPC kit includes a Script named Animate that you can study.

The NPC gets stuck on corners or jumps without clearing an obstacle

Raise AgentRadius so routes keep further from walls, especially for wide NPCs. If it jumps but never gets over, the obstacle is higher than the Humanoid can jump: raise the Humanoid's JumpHeight or JumpPower, or change the level.

Touch damage hits too often, or never

Too often: the cooldown is missing, or it is stored per part instead of per player. Never: STOP_DISTANCE is above 0, so the NPC usually stops before it touches anyone, or the player still has a spawn force field.

Build it with RoCode

If you would rather describe the NPC than wire it up by hand, RoCode can write these scripts into the place you have open. It is an independent AI agent for Roblox Studio: you type in a web chat, and its Studio plugin creates and edits the scripts in your Explorer.

You type

Make the Chaser model in Workspace chase the nearest player within 80 studs with pathfinding, walk home when nobody is near, and deal 10 damage on touch with a 1 second cooldown.

RoCode does
  • Can look through the model first, using Get Properties to read key values such as Anchored on its parts and WalkSpeed on its Humanoid. Type / in the chat to attach the model's exact path
  • Writes the chase and damage scripts inside the model with Create Script, and can untick Anchored with Set Property if the rig needs it
  • Compile-checks both scripts with Check Script inside Studio. That catches compile errors and patterns such as wait() instead of task.wait(), not logic bugs like a route that never reaches the player
  • Sends its changes in batches, and each batch is an undo point in Studio's history

RoCode does not start playtests or run pathfinding itself, so press Play and watch the NPC yourself. If it misbehaves, tell RoCode what you saw: Check Output can read the Server and Client lines from your last playtest. How RoCode connects to Studio.

Questions

Should NPC pathfinding run on the server or the client?

On the server, in a Script. Roblox's own pathfinding example runs in a LocalScript, but it moves the player's own character. An NPC is shared by everyone, and damage it deals has to be decided by the server. See Script vs LocalScript vs ModuleScript.

How many chasing NPCs can one server handle?

Roblox's docs give no fixed number; it depends on your map and how often paths are computed. Path computation is the expensive part, which is why the chase script limits it to twice a second per NPC and skips it when the player is in sight. With many NPCs, Roblox's script-location guidance suggests tagging them and handling them with CollectionService from a single ModuleScript, instead of a script in every model.

How do I make the NPC respawn after it dies?

Keep a copy of the NPC in ServerStorage, and handle respawning from a script in ServerScriptService, since a script inside the NPC is destroyed with it. When the Humanoid's Died event fires, wait a few seconds, destroy the old model and parent a Clone() of the copy to Workspace. The ChaseAI script inside the clone starts by itself, because server scripts run once they are in Workspace.

Can NPCs chase players only during a round?

Yes. Have your round script clone the NPCs into Workspace when a round starts and destroy them when it ends. The round system guide builds the intermission and round loop you would hook this into.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 30 September 2026). A type check catches misspelt APIs and wrong types, not game logic, so play-test in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.