Help with NPC locomotion architecture (server-driven R15 humanoids)
Hi all, I run a game with a handful of server-driven R15 NPCs (real Humanoid rigs built to look like players). They walk around, path to points of interest, approach players, etc. I’ve gotten them mostly working, but I’ve hit a wall on locomotion polish and I’d love advice from people who’ve shipped believable NPCs.
What I want
- Stutter-free movement — smooth walking, no micro-pauses, no hitching when they get near a waypoint or near a player.
- Real map traversal — they currently can’t climb ladders, and jumps over ledges/gaps look janky.
- Scales to a small crowd (5–10 NPCs) without each one being expensive.
The core of what I have
Server-authoritative, PathfindingService (via a SuperPath/SimplePath-style wrapper) driving Humanoid:MoveTo waypoint-by-waypoint.
1) Calculating the path. Create an agent path and ComputeAsync from the NPC’s current position to the goal:
local path = PathfindingService:CreatePath({
AgentRadius = 2.75, -- deliberately fat: full R15 bodies + shoulder/hat clearance
AgentHeight = 5.25,
AgentCanJump = true,
AgentJumpHeight = 8,
AgentMaxSlope = 35,
WaypointSpacing = 3.5,
})
local ok = pcall(function()
path:ComputeAsync(origin, goal) -- origin = NPC root position, goal = target
end)
if not ok or path.Status == Enum.PathStatus.NoPath then
return -- fail / retry
end
local waypoints = path:GetWaypoints()
2) Moving the NPC along the waypoints. This is the actual movement primitive — walk to each waypoint with Humanoid:MoveTo, advance on MoveToFinished, jump if the waypoint says so, and bail via a timeout if it never arrives:
local function travelTo(index)
local waypoint = waypoints[index]
if not waypoint then return end
-- Timeout so a waypoint that never completes doesn't hang the route.
observer = task.delay(WAYPOINT_TIMEOUT, function()
onArrived(false)
end)
humanoid:MoveTo(waypoint.Position)
if waypoint.Action == Enum.PathWaypointAction.Jump and not agentInAir then
humanoid.Jump = true
end
moveConn = humanoid.MoveToFinished:Once(function(reached)
onArrived(reached)
end)
end
function onArrived(reached)
if observer then task.cancel(observer) end
if moveConn then moveConn:Disconnect() end
-- MoveToFinished reports false at tight corners even when we effectively
-- arrived, so accept "close enough" instead of failing the route.
if not reached then
local d = waypoints[currentIndex].Position - root.Position
if Vector3.new(d.X, 0, d.Z).Magnitude <= 2.5 then reached = true end
end
if reached and waypoints[currentIndex + 1] then
currentIndex += 1
travelTo(currentIndex) -- next waypoint
elseif reached then
-- goal reached
else
-- TargetUnreachable -> repath (see below)
end
end
Things I’ve already tried
Network ownership. I re-assert server ownership every frame while a route is active:
-- inside the route coroutine
while routeActive do
local ok, owner = pcall(function() return root:GetNetworkOwner() end)
if ok and owner ~= nil then
pcall(function() root:SetNetworkOwner(nil) end) -- keep the server driving it
end
RunService.Heartbeat:Wait()
end
Bounded repath on stuck:
path.Error:Connect(function(errorType) -- TargetUnreachable / AgentStuck
if attempts >= REPATH_LIMIT then finish(true) return end
attempts += 1
task.spawn(function()
task.wait(0.2) -- clear the lib's rate-limit
pcall(function() agent:Run(target) end) -- recompute from current position
end)
end)
Where I’m still stuck
-
Residual stutter. There’s still occasional micro-hitching, most noticeable as an NPC closes in on a target/player and around dense waypoints near the goal. (In the logs below you can also see the NPC repeatedly
TargetUnreachable-ing while trying to reach a player standing on open floor.) -
Ladders / vertical traversal.
PathfindingServiceroutes ignore my ladders entirely. -
Overall architecture. Is per-waypoint
Humanoid:MoveToeven the right primitive for smooth movement, or should I be steering withHumanoid:Move(direction)continuously toward the next waypoint? Should I consider another approach entirely?
Video showing the stutter
Console logs for the session above
Please, if you have any ideas or suggestions comment below.
Happy to share more code. Thanks!