RoCreatorLog in

How to make an NPC that chases the player in Roblox Studio

A pathfinding chase NPC that re-paths while the target moves, jumps obstacles, and attacks on contact. Starts with the single most common reason a chase NPC just stands there.

Written 11 August 2026 · about 11 minutes · every script here was written against Roblox Studio as it shipped that week

Before any code: check the model has a Humanoid

This is not a preamble, it is the answer for most people who search this. An NPC that just stands there, with no errors in the Output window, is almost always a model with no Humanoid inside it. Plenty of things on the Toolbox that look exactly like characters are static display meshes — a zombie sculpture, not a zombie — and your script sits on WaitForChild("Humanoid") forever. Waiting is not an error, so nothing is printed and nothing is wrong.

-- Run this in the command bar (View > Command Bar) with your model selected.
-- It answers, in one line, the question that stops most chase NPCs working.
local model = game.Selection:Get()[1]
print(model.Name,
"Humanoid:", model:FindFirstChildOfClass("Humanoid") ~= nil,
"HumanoidRootPart:", model:FindFirstChild("HumanoidRootPart") ~= nil,
"PrimaryPart:", model.PrimaryPart ~= nil)

You need true for Humanoid and HumanoidRootPart. If either is false, the fastest fix is not to repair the model: insert a Rig from the Avatar → Rig Builder tab, then move the mesh parts onto it, or just pick a different model. We hit exactly this while testing our own tooling — the best-rated result for a monster search turned out to be a static mesh, the chase script waited on a Humanoid that never appeared, and the whole thing looked like a broken script for an hour.

The simplest thing that works

If your NPC and your player are in the same open space with nothing between them, you do not need pathfinding at all.

-- The version everyone writes first. Keep it: for an open room it is correct,
-- it is four lines, and pathfinding is not free.
local npc = workspace:WaitForChild("Chaser")
local humanoid = npc:WaitForChild("Humanoid")
while task.wait(0.25) do
local target = findNearestPlayer() -- defined in the next section
if target then
humanoid:MoveTo(target.Position)
end
end

Humanoid:MoveTo walks in a straight line and steers around nothing. In a room, that is correct behaviour and costs nothing. The moment there is a wall, a pillar or a staircase, the NPC pushes into it forever — and that is when you want the rest of this page.

1. Find something to chase

Put this in a Script in ServerScriptService. Server, not client: the NPC has to move the same way for everybody, and an NPC driven from one player’s machine is in a different place for everyone else.

-- ServerScriptService > ChaseAI (a Script, not a LocalScript)
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local npc = workspace:WaitForChild("Chaser")
local humanoid = npc:WaitForChild("Humanoid")
local root = npc:WaitForChild("HumanoidRootPart")
local DETECT_RANGE = 120 -- studs. Past this the NPC ignores you.
local GIVE_UP_RANGE = 200 -- studs. Chase further than this and it goes home.
local REPATH_INTERVAL = 0.4 -- seconds between recalculating the route
local ATTACK_RANGE = 6
local ATTACK_DAMAGE = 10
local ATTACK_COOLDOWN = 1
local homePosition = root.Position
local function findNearestTarget()
local best, bestDistance = nil, DETECT_RANGE
for _, player in Players:GetPlayers() do
local character = player.Character
if not character then continue end
local targetHumanoid = character:FindFirstChildOfClass("Humanoid")
local targetRoot = character:FindFirstChild("HumanoidRootPart")
if not targetHumanoid or not targetRoot then continue end
if targetHumanoid.Health <= 0 then continue end
local distance = (targetRoot.Position - root.Position).Magnitude
if distance < bestDistance then
best, bestDistance = targetRoot, distance
end
end
return best, bestDistance
end

Every one of those continue guards is a real crash. A player in the join-and-not-yet-spawned window has no character; a character mid-respawn has no HumanoidRootPart; a dead player still has both and should not be hunted. Indexing through any of them errors, the script dies, and the NPC stops permanently — from one player joining at the wrong moment.

2. Ask the engine for a route

local path = PathfindingService:CreatePath({
-- These four decide where the NPC believes it can go. Get them wrong and
-- the path is computed for a differently shaped creature than the one you
-- have, which produces routes it cannot physically walk.
AgentRadius = 2, -- half the widest part of the NPC, rounded up
AgentHeight = 5, -- floor to top of head
AgentCanJump = true,
AgentCanClimb = false,
Costs = {
-- Not a barrier, a preference: the NPC will still cross water, but only
-- if going round costs more than 20x as much.
Water = 20,
},
})
local function stepAlong(waypoints)
-- Start at 2. Waypoint 1 is where the NPC already is, and walking to it
-- makes it hesitate for a frame at the start of every single re-path.
for index = 2, math.min(#waypoints, 4) do
local waypoint = waypoints[index]
if waypoint.Action == Enum.PathWaypointAction.Jump then
humanoid.Jump = true
end
humanoid:MoveTo(waypoint.Position)
-- MoveToFinished has an eight-second internal timeout, so a blocked NPC
-- resumes rather than hanging forever. Do not rely on the eight seconds
-- as your recovery mechanism though — that is eight seconds of an NPC
-- shoving into a wall while the player walks away.
local reached = humanoid.MoveToFinished:Wait()
if not reached then return false end
end
return true
end

Follow three or four waypoints, then re-path

The obvious loop walks the whole waypoint list and then computes a new one. That gives you an NPC that commits to a route the player left ten seconds ago — it runs confidently to where you used to be, and only then notices. It reads as stupid, and it is the single biggest difference between a chase that feels alive and one that does not.

Following the first few and recomputing costs more CPU and looks vastly better. Widen the slice or lengthen REPATH_INTERVAL if you have many NPCs; ComputeAsync is the expensive call here, not the walking.

The index = 2start is worth keeping too. Waypoint 1 is the NPC’s current position, so walking to it produces a visible stutter at the start of every recalculation — which, at REPATH_INTERVAL of 0.4, is a stutter twice a second forever.

3. The loop

local lastAttack = 0
local function tryAttack(targetRoot, distance)
if distance > ATTACK_RANGE then return end
if os.clock() - lastAttack < ATTACK_COOLDOWN then return end
local targetHumanoid = targetRoot.Parent:FindFirstChildOfClass("Humanoid")
if not targetHumanoid or targetHumanoid.Health <= 0 then return end
lastAttack = os.clock()
targetHumanoid:TakeDamage(ATTACK_DAMAGE)
end
while true do
task.wait(REPATH_INTERVAL)
if humanoid.Health <= 0 then break end
local targetRoot, distance = findNearestTarget()
if not targetRoot then
-- Nothing to chase. Walk home if we wandered, otherwise stand still.
if (root.Position - homePosition).Magnitude > 5 then
humanoid:MoveTo(homePosition)
end
continue
end
if distance > GIVE_UP_RANGE then
humanoid:MoveTo(homePosition)
continue
end
tryAttack(targetRoot, distance)
-- ComputeAsync throws on a malformed request rather than returning a
-- status, so it needs the pcall. A failure here is normal — a target
-- standing somewhere unreachable produces one every cycle.
local ok = pcall(function()
path:ComputeAsync(root.Position, targetRoot.Position)
end)
if ok and path.Status == Enum.PathStatus.Success then
stepAlong(path:GetWaypoints())
else
-- No route exists. Walking straight at the target is the right
-- fallback: it looks like a creature trying, and it closes the gap if
-- the obstruction was temporary.
humanoid:MoveTo(targetRoot.Position)
end
end

The give-up range and the walk home are what stop your map filling with NPCs that were aggravated once in the first minute and have been slowly migrating toward spawn ever since.

Things that will bite you

MoveToFinished can return before it arrives

It fires with false on the eight-second timeout as well as on arrival, which is why the loop above checks the return value. If you ignore it, a stuck NPC silently advances through its waypoint list without moving and you get an NPC that appears to be pathing correctly while standing still.

AgentRadius is the reason it walks into corners

Pathfinding computes routes for a cylinder of the size you declared, not for your actual model. Declare a radius of 2 for a model that is 6 studs wide and the engine will happily route it through a gap it cannot fit through, and the NPC will wedge there. Measure the widest horizontal dimension of the model, halve it, round up.

Anything unanchored the NPC can push

Chase NPCs shove props around, and a heavy prop shoved into a doorway becomes a permanent obstacle the pathfinder does not know about — it navigates the static geometry, not whatever moved since. If your level has loose furniture, anchor what does not need to move.

Making it feel less robotic

Three cheap changes, in order of how much they buy you. Give it a wander state when there is no target — an NPC standing perfectly still until you cross an invisible line is what makes detection ranges obvious. Add a reaction delay of a third of a second between spotting you and moving, so it looks like it noticed rather than like a trigger fired. And play a run animation with Humanoid.Animator:LoadAnimation: a character sliding along the floor in its idle pose undoes all the pathfinding work you just did.

Or skip the typing

Everything above is yours whether or not you ever install anything — that is why the scripts are complete rather than trimmed. RoCreator is a Studio plugin that writes this kind of code into your open place from a sentence, and snapshots the place first so one click puts it back. It is free to try, 5 requests a day, no card.

The honest caveat, since this page has been honest so far: it is good at putting one working feature into a game you already have. It will not build you a game, it will not make terrain, and it does not generate 3D models.

Try it freeHow it works, step by step

Read next