What Is an NPC in Roblox?
An NPC (Non-Player Character) is any character in your game that is not controlled by a real person. Think zombies, shopkeepers, quest givers, or roaming guards. In Roblox, an NPC is built as a Model that contains a few essential parts:
- Humanoid — the object that gives the model health, walk speed, jump power, and the ability to play animations. Without it, the model is just a static prop.
- HumanoidRootPart — the primary part that acts as the root of the character's physics. All movement goes through this part.
- Body parts and meshes — the head, torso, arms, and legs that make up the visual appearance. You can use R6 (six parts) or R15 (fifteen parts) rigs.
- Animations — idle, walk, run, and attack animations loaded through an
Animatorinside the Humanoid.
The simplest way to get started is to duplicate your character in Studio, remove the scripts that come with a player character, and drop a new server Script inside the model. That gives you a fully rigged NPC ready for custom AI.
Basic NPC Setup
Before writing any AI code, you need a properly structured NPC model in the workspace. Here is the minimum hierarchy:
Model(named "Zombie" or whatever you like)-
HumanoidRootPart(anchored = false, primary part of the model) -
Humanoid(WalkSpeed = 16, MaxHealth = 100) -
Head,Torso,Left Arm,Right Arm,Left Leg,Right Leg -
Script(server-side AI logic)
Set the model's PrimaryPart to the HumanoidRootPart. This is critical because MoveTo, pathfinding, and many other APIs rely on PrimaryPart being set. If you skip this step, your NPC will not move.
Tip: Use the Rig Builder plugin in Studio (Avatar tab → Rig Builder) to generate a properly configured R6 or R15 rig in one click. It saves a lot of manual setup.
Simple Chase Behavior with MoveTo
The fastest way to get an NPC chasing players is Humanoid:MoveTo(). This method tells the Humanoid to walk toward a target position. It does not do any pathfinding — the NPC walks in a straight line. That means it will get stuck on walls, but it is a great starting point.
-- ServerScript inside the NPC model
local npc = script.Parent
local humanoid = npc:WaitForChild("Humanoid")
local rootPart = npc:WaitForChild("HumanoidRootPart")
local function getClosestPlayer()
local closest = nil
local closestDist = 50 -- detection range in studs
for _, player in pairs(game.Players:GetPlayers()) do
local character = player.Character
if character and character:FindFirstChild("HumanoidRootPart") then
local dist = (character.HumanoidRootPart.Position - rootPart.Position).Magnitude
if dist < closestDist then
closest = character
closestDist = dist
end
end
end
return closest
end
while humanoid.Health > 0 do
local target = getClosestPlayer()
if target then
humanoid:MoveTo(target.HumanoidRootPart.Position)
end
task.wait(0.3)
end
This script checks every 0.3 seconds for the nearest player within 50 studs and walks toward them. Simple, effective, and good enough for open-field games. But for maps with walls, buildings, or obstacles, you need real pathfinding.
PathfindingService: Smart Navigation
PathfindingService is Roblox's built-in navigation system. It computes a path around obstacles so your NPC can walk from point A to point B without getting stuck on every wall. Here is how it works step by step:
- Call
PathfindingService:CreatePath()to create a Path object. - Call
Path:ComputeAsync(startPosition, endPosition)to calculate the route. - Call
Path:GetWaypoints()to get an array of waypoints along the path. - Loop through the waypoints and use
Humanoid:MoveTo()on each one.
local PathfindingService = game:GetService("PathfindingService")
local npc = script.Parent
local humanoid = npc:WaitForChild("Humanoid")
local rootPart = npc:WaitForChild("HumanoidRootPart")
local path = PathfindingService:CreatePath({
AgentRadius = 2,
AgentHeight = 5,
AgentCanJump = true,
AgentCanClimb = false,
})
local function walkTo(destination)
path:ComputeAsync(rootPart.Position, destination)
if path.Status == Enum.PathStatus.Success then
local waypoints = path:GetWaypoints()
for _, waypoint in ipairs(waypoints) do
if waypoint.Action == Enum.PathWaypointAction.Jump then
humanoid.Jump = true
end
humanoid:MoveTo(waypoint.Position)
humanoid.MoveToFinished:Wait()
end
end
end
Understanding AgentParameters
When you call CreatePath(), you can pass a table of agent parameters that describe the physical size and abilities of your NPC:
- AgentRadius — how wide the NPC is. A larger radius keeps the path further from walls. Set this to roughly half the width of your NPC's torso.
- AgentHeight — how tall the NPC is. Paths will avoid low ceilings that the NPC cannot fit through.
- AgentCanJump — whether the NPC is allowed to jump over gaps or onto ledges. Set to
truefor agile enemies,falsefor slow zombies. - AgentCanClimb — whether the NPC can climb TrussParts. Useful for ladder-heavy maps.
Getting these values wrong is the number one reason NPCs get stuck. If your AgentRadius is too large, the path will fail in tight corridors. If it is too small, the NPC will clip through walls. Test and tweak until it feels right.
Handling Blocked Paths
Paths can become blocked at runtime. A door might close, a player might build a wall, or a moving platform might shift. The Path.Blocked event fires when an obstacle appears along the computed path. You should listen for it and recompute.
path.Blocked:Connect(function(blockedWaypointIndex)
-- Recompute the path when something blocks the way
walkTo(currentTarget.HumanoidRootPart.Position)
end)
Without this, your NPC will walk into the obstacle and get stuck forever. Always connect the Blocked event for any NPC that navigates dynamic environments.
State Machines for NPC AI
Real game AI does not just chase the player endlessly. NPCs need different behaviors: standing idle, patrolling a route, chasing a detected player, and attacking when close enough. The cleanest way to organize this is a state machine.
A state machine is just a variable that tracks what the NPC is currently doing, plus a set of rules for when to switch states. Here is a minimal pattern:
local state = "idle"
local target = nil
local DETECTION_RANGE = 50
local ATTACK_RANGE = 5
while humanoid.Health > 0 do
if state == "idle" then
-- Stand still, look around
local found = getClosestPlayer()
if found then
target = found
state = "chase"
end
elseif state == "chase" then
-- Pathfind toward the target
if target and target:FindFirstChild("HumanoidRootPart") then
local dist = (target.HumanoidRootPart.Position - rootPart.Position).Magnitude
if dist <= ATTACK_RANGE then
state = "attack"
elseif dist > DETECTION_RANGE then
target = nil
state = "idle"
else
walkTo(target.HumanoidRootPart.Position)
end
else
target = nil
state = "idle"
end
elseif state == "attack" then
-- Deal damage, play animation, then go back to chase
if target and target:FindFirstChild("Humanoid") then
target.Humanoid:TakeDamage(10)
end
task.wait(1) -- attack cooldown
state = "chase"
end
task.wait(0.2)
end
This pattern scales well. You can add a "patrol" state that walks between predefined points, a "flee" state for low-health NPCs, or a "dead" state that plays a ragdoll animation and cleans up the model.
Detection Systems
How your NPC detects players matters a lot for gameplay feel. There are three common approaches:
Magnitude Check (Distance)
The simplest method. Calculate the distance between the NPC and each player. If the distance is below a threshold, the player is "detected." This is what we used in the chase example above. It works well for most games, but it detects players through walls.
Raycasting for Line of Sight
Cast a ray from the NPC's head toward the player. If the ray hits the player without hitting a wall first, the NPC can see them. This gives you realistic line-of-sight detection.
local function canSeeTarget(targetPart)
local origin = npc.Head.Position
local direction = (targetPart.Position - origin).Unit * DETECTION_RANGE
local params = RaycastParams.new()
params.FilterDescendantsInstances = {npc}
params.FilterType = Enum.RaycastFilterType.Exclude
local result = workspace:Raycast(origin, direction, params)
if result and result.Instance:IsDescendantOf(targetPart.Parent) then
return true
end
return false
end
Hearing Radius
Give your NPC a short "hearing" range (maybe 15 studs) where it detects players regardless of line of sight, plus a longer "vision" range (maybe 60 studs) that requires line of sight. This combination feels natural — players can sneak up from behind but not walk right next to the NPC unnoticed.
Combat NPCs
Once your NPC can detect and chase players, adding combat is straightforward. The key pieces are:
- Dealing damage — call
target.Humanoid:TakeDamage(amount)when the NPC is within attack range. Always validate that the target still exists and has health remaining. - Attack cooldowns — use a simple timestamp check (
tick() - lastAttack > cooldown) to prevent the NPC from dealing damage every frame. - Health bars — Roblox automatically shows a health bar above any Humanoid whose health drops below max. You can customize this with a BillboardGui if you want a styled health bar.
- Death and respawn — listen for
Humanoid.Died, play a death animation or ragdoll effect, then either destroy the model or respawn it after a delay usingtask.delay()and cloning from ServerStorage.
Warning: Never trust the client with damage calculations. All NPC health and damage logic should run in server-side Scripts, not LocalScripts. If you handle damage on the client, exploiters can make themselves invincible or one-shot your NPCs.
Performance with Many NPCs
A single NPC is easy. Fifty NPCs running pathfinding every frame will destroy your server's performance. Here are the rules to follow:
- Run AI on the server. NPC logic belongs in server Scripts, not LocalScripts. The server is the authority on NPC positions, health, and behavior.
- Stagger updates. Do not update all 50 NPCs on the same frame. Spread them out — update 5 NPCs per heartbeat, cycling through the full list over multiple frames.
- Do not pathfind every frame. Recompute paths every 1-2 seconds, not every 0.03 seconds. Between pathfinding updates, the NPC can walk toward its current waypoint.
- Use magnitude checks before pathfinding. If a player is 200 studs away, skip the expensive
ComputeAsynccall entirely. Only pathfind toward players who are within a reasonable range. - Pool and reuse NPC models. Instead of destroying and creating new models, move dead NPCs to ServerStorage and reuse them on respawn.
Common Bugs and How to Fix Them
NPC Gets Stuck on Corners
This usually means your AgentRadius is too small, so the path hugs walls too closely. Increase it by 1-2 studs. Also make sure you are walking through every waypoint in order and waiting for MoveToFinished before moving to the next one.
NPC Falls Through the Floor
Check that the HumanoidRootPart is not anchored and that all body parts have CanCollide set correctly. If you are spawning NPCs from ServerStorage, make sure the clone is placed above the ground, not inside it. A quick fix is to set the CFrame 3 studs above the intended spawn point.
NPC Spins in Place
This happens when MoveTo targets a position the NPC is already standing on, or when the waypoint is directly below the NPC. Add a minimum distance check — if the NPC is already within 2 studs of the waypoint, skip it and move to the next one.
NPC Walks Into Walls After Path Expires
MoveTo has an 8-second timeout. If the NPC does not reach the target in 8 seconds, MoveToFinished fires with reached = false. You should check that return value and recompute the path if the NPC did not actually arrive.
NPC AI Breaks After Player Dies
Always check that your target's character and Humanoid still exist before accessing them. Players respawn with a completely new character model, so any stored references become nil. Re-acquire the target each cycle of your AI loop.
Next steps: Once you have solid NPC AI, look into crowd simulation (having NPCs avoid each other), navigation mesh modifiers for custom path costs, and animation blending to make movement transitions feel smooth. Good AI is what separates a forgettable game from one players keep coming back to.