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
ActionisJump, and compute a new path whenPath.Blockedfires for a waypoint ahead of the NPC. - When the player is in plain sight, skip pathfinding and
MoveTothem directly. Stop a few studs away, or keep going if the NPC damages on touch.
On this page
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.
| API | What 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.Blocked | Fires with a waypoint index when something blocks the computed route. |
Humanoid:MoveTo(position) | Walks the NPC towards a point. |
Humanoid.MoveToFinished | Fires 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.
| Requirement | Why it matters |
|---|---|
A Humanoid named Humanoid | MoveTo, WalkSpeed and jumping all belong to the Humanoid. |
A HumanoidRootPart | The part the Humanoid drives. Paths start from its position. |
| Nothing anchored | An 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. |
- 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.
- Rename the rig
Chaserand move it onto open ground. - 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.
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.
- 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. - In the Explorer, hover over Chaser, click the + button and insert a Script. Name it
WalkToGoaland paste the code below. - Press Play. The NPC walks around the wall to the Goal part.
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.
| Key | Default | What it controls |
|---|---|---|
AgentRadius | 2 | The NPC's radius in studs. Raise it for wide NPCs so paths keep clear of walls. |
AgentHeight | 5 | The NPC's height. Spaces lower than this, such as under stairs, count as blocked. |
AgentCanJump | true | Allows routes that need a jump. Those waypoints have Action set to Jump. |
AgentCanClimb | false | Allows climbing TrussParts. Those waypoints have the label Climb. |
WaypointSpacing | 4 | Spacing in studs between the intermediate waypoints. math.huge removes them. |
Costs | none | How 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.
- Delete
WalkToGoalfrom Chaser if you added it. - Hover over Chaser, click +, insert a Script named
ChaseAIand paste the code below. - Press Play, walk behind a wall, then run out of range. The NPC follows you around the wall, then returns to where it started.
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.
| What you see | Cause | Fix |
|---|---|---|
| Smooth when alone, jerky once a player gets close | Network 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 walls | MoveTo 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 corners | A 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 second | A 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 on | MoveToFinished: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.
- In ChaseAI, set
STOP_DISTANCEto0so the NPC walks into players instead of stopping beside them. - Insert a Script named
TouchDamageinto Chaser and paste the code below.
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
- Cover the area with a part, with Anchored on and CanCollide off.
- Insert a PathfindingModifier into that part and set its
LabeltoDangerZone. - That is all. The chase script already gives
DangerZonea cost ofmath.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.
Custom crossings: PathfindingLink
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.
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.
- 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
- Roblox Creator Docs: Pathfinding. agent parameters, waypoints, blocked paths, modifiers, links and limits
- Roblox Creator Docs: PathfindingService. CreatePath and its defaults
- Roblox Creator Docs: Path. ComputeAsync, GetWaypoints, Status and the Blocked event
- Roblox Creator Docs: Humanoid. MoveTo, the 8-second MoveToFinished timeout, ChangeState and TakeDamage
- Roblox Creator Docs: Network ownership. automatic ownership, SetNetworkOwner and the Network owners view
- Roblox Creator Docs: RaycastParams. ExcludeInstances and RespectCanCollide
- Roblox Creator Docs: NPC kit. example NPCs with their own Animate scripts
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.