Roblox NPC script: an enemy that chases and attacks

A zombie, guard or monster that walks to the nearest player and hits them. One server script inside the NPC; variations add pathfinding around walls, respawning and friendly talking NPCs.

ScriptUpdated November 9, 2026

Where to put it

Script — Inside the NPC model (Workspace > Zombie > ChaseScript). It runs on the server, so players can't change what it does.

  1. Insert a character rig: in Studio's Avatar tab open the rig builder (Character), pick R15 and insert a rig. Rename the model, for example Zombie.
  2. Check that no part of the rig is Anchored (select the parts and untick Anchored), or it can't walk.
  3. Insert a Script into the model (not into a part) and paste the code.

The script

Inside the NPC model (Workspace > Zombie > ChaseScript)
local Players = game:GetService("Players")

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

local CHASE_RANGE = 60 -- studs
local ATTACK_RANGE = 4.5
local DAMAGE = 15
local ATTACK_COOLDOWN = 1 -- seconds between hits

-- The server moves the NPC, so it doesn't stutter near players.
if not root.Anchored then
	root:SetNetworkOwner(nil)
end

local home = root.Position
local lastAttack = 0

local function findTarget()
	local closest, closestHumanoid, closestDistance = nil, nil, CHASE_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 targetRoot and targetHumanoid.Health > 0 then
			local distance = (targetRoot.Position - root.Position).Magnitude
			if distance < closestDistance then
				closest, closestHumanoid, closestDistance = targetRoot, targetHumanoid, distance
			end
		end
	end
	return closest, closestHumanoid, closestDistance
end

while humanoid.Health > 0 do
	local targetRoot, targetHumanoid, distance = findTarget()
	if targetRoot then
		humanoid:MoveTo(targetRoot.Position)
		if distance <= ATTACK_RANGE and os.clock() - lastAttack >= ATTACK_COOLDOWN then
			lastAttack = os.clock()
			targetHumanoid:TakeDamage(DAMAGE)
		end
	else
		humanoid:MoveTo(home) -- nobody nearby: walk back
	end
	task.wait(0.2)
end

How it works

  • Five times a second the NPC looks for the closest living player within CHASE_RANGE and walks toward them with Humanoid:MoveTo.
  • Damage happens in the same server script with a cooldown, so a player standing next to it loses 15 health a second instead of dying instantly. TakeDamage also respects a ForceField, so freshly spawned players are protected.
  • SetNetworkOwner(nil) keeps the NPC's physics on the server. Without it, Roblox may hand the NPC to a nearby player's device and it starts to jitter.
  • When nobody is in range the NPC walks back to where it started; when it dies, the loop ends.

Variations

Walk around walls with pathfinding

MoveTo walks in a straight line and gets stuck on walls. PathfindingService computes a route. Add this to the script and call walkTo(targetRoot.Position) instead of humanoid:MoveTo(...). It recomputes every few waypoints because the player keeps moving.

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

local function walkTo(position)
	local ok = pcall(function()
		path:ComputeAsync(root.Position, position)
	end)
	if not ok or path.Status ~= Enum.PathStatus.Success then
		humanoid:MoveTo(position) -- no route found: try a straight line
		return
	end
	local waypoints = path:GetWaypoints()
	for i = 2, math.min(#waypoints, 4) do
		local waypoint = waypoints[i]
		if waypoint.Action == Enum.PathWaypointAction.Jump then
			humanoid.Jump = true
		end
		humanoid:MoveTo(waypoint.Position)
		humanoid.MoveToFinished:Wait()
	end
end

Respawn the NPC after it dies

Move the finished NPC into ServerStorage (scripts there don't run), add an anchored, invisible part named ZombieSpawn in Workspace, and put this Script in ServerScriptService. Each clone carries its own chase script.

local ServerStorage = game:GetService("ServerStorage")

local template = ServerStorage:WaitForChild("Zombie")
local spawnPart = workspace:WaitForChild("ZombieSpawn")
local RESPAWN_SECONDS = 5

local function spawnZombie()
	local zombie = template:Clone()
	zombie:PivotTo(spawnPart.CFrame + Vector3.new(0, 3, 0))
	zombie.Parent = workspace
	zombie.Humanoid.Died:Connect(function()
		task.wait(RESPAWN_SECONDS)
		zombie:Destroy()
		spawnZombie()
	end)
end

spawnZombie()

A friendly NPC that talks

For a shopkeeper or quest giver, skip the chase script. Anchor the NPC's HumanoidRootPart, put a ProximityPrompt in it and a BillboardGui with a TextLabel named Speech in its Head, then add this Script to the model.

local npc = script.Parent
local prompt = npc.HumanoidRootPart:WaitForChild("ProximityPrompt")
local speech = npc.Head:WaitForChild("BillboardGui"):WaitForChild("Speech")

local LINES = {
	"Welcome, traveler!",
	"The cave to the north is full of zombies.",
	"Come back when you have 100 coins.",
}
local index = 0

prompt.ActionText = "Talk"
speech.Text = ""

prompt.Triggered:Connect(function()
	index = index % #LINES + 1
	speech.Text = LINES[index]
	task.delay(4, function()
		if speech.Text == LINES[index] then
			speech.Text = ""
		end
	end)
end)

Common mistakes

  • Dealing damage from a LocalScript or trusting a RemoteEvent from the client: exploiters can make themselves immune or hit others. Damage belongs on the server, as here.
  • Damage in Touched with no cooldown: Touched fires many times a second, so the player dies instantly.
  • An anchored HumanoidRootPart: the NPC plays its walk animation but never moves (and SetNetworkOwner errors on anchored parts).
  • Calling MoveTo in a loop with no task.wait: the script freezes the server.
  • Dozens of NPCs each looping over all players every frame. Five checks a second per NPC is plenty; for big waves, use one script that drives every NPC.

FAQ

How do I make an NPC in Roblox Studio?

Open the Avatar tab, use the rig builder (Character) to insert an R15 rig, unanchor its parts, and add a Script to the model. The script on this page makes it chase and attack players.

How do I make an NPC follow the player in Roblox?

Find the player's HumanoidRootPart on the server and call the NPC's Humanoid:MoveTo with its position a few times a second, as this script does. Add PathfindingService if there are walls in the way.

Why is my NPC not moving?

The most common causes: a part of the rig is Anchored, the Humanoid's WalkSpeed is 0, the script is a LocalScript (it must be a Script), or MoveTo is given a position the NPC can't reach in a straight line.

How do I make an NPC talk in Roblox?

Put a ProximityPrompt and a BillboardGui with a TextLabel on the NPC, and change the label's text when the prompt is triggered — see the friendly NPC variation above.

Keep reading