NPC pathfinding animation

I made a simple civilian NPC where they use pathfinding to navigate through the city, but its either the animation is smooth, but they stop before completing the whole path, or they complete the whole path as intended, but the animation is bugging.

Before you say it, yes, I set the NetworkOwner to nil

Follows path but not smooth
--// Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PathfindingService = game:GetService("PathfindingService")

--// Variables
local BinFolder = ReplicatedStorage:WaitForChild("Bin")
local NPCSource = BinFolder:WaitForChild("NPC")
local AnimateSource = BinFolder:WaitForChild("Animate")

local PathsFolder = workspace:WaitForChild("Waypoints"):WaitForChild("Outside")
local NPCSFolder = workspace:FindFirstChild("NPCS")

if not NPCSFolder then
	NPCSFolder = Instance.new("Folder", workspace)
	NPCSFolder.Name = "NPCS"
end

local LAST_PATH = nil

--// Settings
local NPCS_CAP = 20
local SPAWN_MIN = 1
local SPAWN_MAX = 5

--// Functions
local function getOrderedWaypoints(PathFolder: Folder)
	local waypoints = {}
	for _, path in ipairs(PathFolder:GetChildren()) do
		if path:IsA("BasePart") and tonumber(path.Name) then
			table.insert(waypoints, path)
		end
	end
	table.sort(waypoints, function(a, b)
		return tonumber(a.Name) < tonumber(b.Name)
	end)
	return waypoints
end

local function getRandomPathFolder()
	local folders = {}
	for _, child in ipairs(PathsFolder:GetChildren()) do
		if child:IsA("Folder") then
			table.insert(folders, child)
		end
	end
	if #folders == 0 then return nil end

	if #folders == 1 then
		return folders[1]
	end

	local chosen
	repeat
		chosen = folders[math.random(1, #folders)]
	until chosen ~= LAST_PATH

	LAST_PATH = chosen
	return chosen
end

local function moveNPCAlongWaypoints(npc, waypoints)
	local humanoid = npc:FindFirstChildWhichIsA("Humanoid")
	local npcPrimary = npc.PrimaryPart
	if not humanoid or not npcPrimary then return end

	for i, waypointPart in ipairs(waypoints) do
		if not waypointPart or not waypointPart:IsA("BasePart") then
			warn(("Invalid waypoint %d for NPC %s"):format(i, tostring(npc.Name)))
		else
			local destination = waypointPart.Position

			if (npcPrimary.Position - destination).Magnitude <= 1.5 then
				task.wait(0.1)
			else
				local path = PathfindingService:CreatePath({
					AgentRadius = 2.5,
					AgentHeight = 6,
					AgentCanJump = true,
				})

				local success, err = pcall(function()
					path:ComputeAsync(npcPrimary.Position, destination)
				end)
				
				if success and path.Status == Enum.PathStatus.Success then
					local points = path:GetWaypoints()
					for _, point in ipairs(points) do
						humanoid:MoveTo(point.Position)
						humanoid.MoveToFinished:Wait()
					end
				else
					warn(("ComputeAsync error for NPC %s at waypoint %d: %s"):format(npc.Name, i, tostring(err)))
				end
			end
		end
		task.wait()
	end

	task.wait(1)
	if npc and npc.Parent then npc:Destroy() end
end

local function SpawnCivilian()
	local npcList = NPCSource:GetChildren()
	if #npcList == 0 then return end

	local npc = npcList[math.random(1, #npcList)]:Clone()
	npc.Parent = NPCSFolder
	npc.PrimaryPart:SetNetworkOwner(nil)

	local RandomPathFolder = getRandomPathFolder()
	if not RandomPathFolder then
		npc:Destroy()
		return
	end

	local waypoints = getOrderedWaypoints(RandomPathFolder)
	if #waypoints == 0 then
		npc:Destroy()
		return
	end

	if math.random() < 0.5 then
		local reversed = {}
		for i = #waypoints, 1, -1 do
			table.insert(reversed, waypoints[i])
		end
		waypoints = reversed
	end

	local humanoidRoot = npc:FindFirstChild("HumanoidRootPart") or npc.PrimaryPart
	if humanoidRoot then
		humanoidRoot.CFrame = waypoints[1].CFrame + Vector3.new(0, 3, 0)
	end

	table.remove(waypoints, 1)

	task.spawn(function()
		moveNPCAlongWaypoints(npc, waypoints)
	end)
end

--// Initialize
while true do
	if #NPCSFolder:GetChildren() < NPCS_CAP then
		SpawnCivilian()
	end
	task.wait(math.random() * (SPAWN_MAX - SPAWN_MIN) + SPAWN_MIN)
end
Smooth but does not follow the whole path
--// Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PathfindingService = game:GetService("PathfindingService")

--// Variables
local BinFolder = ReplicatedStorage:WaitForChild("Bin")
local NPCSource = BinFolder:WaitForChild("NPC")
local AnimateSource = BinFolder:WaitForChild("Animate")

local PathsFolder = workspace:WaitForChild("Waypoints"):WaitForChild("Outside")
local NPCSFolder = workspace:FindFirstChild("NPCS")

if not NPCSFolder then
	NPCSFolder = Instance.new("Folder", workspace)
	NPCSFolder.Name = "NPCS"
end

local LAST_PATH = nil

--// Settings
local NPCS_CAP = 20
local SPAWN_INTERVAL = 1

--// Functions
local function getOrderedWaypoints(PathFolder: Folder)
	local waypoints = {}
	for _, path in ipairs(PathFolder:GetChildren()) do
		if path:IsA("BasePart") and tonumber(path.Name) then
			table.insert(waypoints, path)
		end
	end
	table.sort(waypoints, function(a, b)
		return tonumber(a.Name) < tonumber(b.Name)
	end)
	return waypoints
end

local function getRandomPathFolder()
	local folders = {}
	for _, child in ipairs(PathsFolder:GetChildren()) do
		if child:IsA("Folder") then
			table.insert(folders, child)
		end
	end
	if #folders == 0 then return nil end

	if #folders == 1 then
		return folders[1]
	end

	local chosen
	repeat
		chosen = folders[math.random(1, #folders)]
	until chosen ~= LAST_PATH

	LAST_PATH = chosen
	return chosen
end

local function moveNPCAlongWaypoints(npc, waypoints)
	local humanoid = npc:FindFirstChildWhichIsA("Humanoid")
	local npcPrimary = npc.PrimaryPart
	if not humanoid or not npcPrimary then return end
	
	local WAYPOINT_INDEX = 1
	local REACHED_CONNECTION
	
	local function moveToNext()
		if WAYPOINT_INDEX > #waypoints then
			if REACHED_CONNECTION then REACHED_CONNECTION:Disconnect() end
			task.wait(1)
			if npc then npc:Destroy() end
			return
		end
		
		local path = PathfindingService:CreatePath({
			AgentRadius = 2.5,
			AgentHeight = 6,
			AgentCanJump = true,
		})
		
		local destination = waypoints[WAYPOINT_INDEX].Position
		
		local success, err = pcall(function()
			path:ComputeAsync(npcPrimary.Position, destination)
		end)
		
		if success and path.Status == Enum.PathStatus.Success then
			local points = path:GetWaypoints()

			if not REACHED_CONNECTION then
				REACHED_CONNECTION = humanoid.MoveToFinished:Connect(function(reached)
					if reached then
						WAYPOINT_INDEX += 1
						moveToNext()
					end
				end)
			end

			WAYPOINT_INDEX = 1
			for _, point in ipairs(points) do
				humanoid:MoveTo(point.Position)
			end
		else
			warn(("Path not computed for NPC %s at waypoint %d"):format(npc.Name, WAYPOINT_INDEX))
		end
	end
	
	moveToNext()
end

local function SpawnCivilian()
	local npcList = NPCSource:GetChildren()
	if #npcList == 0 then return end

	local npc = npcList[math.random(1, #npcList)]:Clone()
	npc.Parent = NPCSFolder

	npc.PrimaryPart:SetNetworkOwner(nil)

	local RandomPathFolder = getRandomPathFolder()
	if not RandomPathFolder then
		npc:Destroy()
		return
	end

	local waypoints = getOrderedWaypoints(RandomPathFolder)
	if #waypoints == 0 then
		npc:Destroy()
		return
	end

	if math.random() < 0.5 then
		local reversed = {}
		for i = #waypoints, 1, -1 do
			table.insert(reversed, waypoints[i])
		end
		waypoints = reversed
	end

	local humanoidRoot = npc:FindFirstChild("HumanoidRootPart") or npc.PrimaryPart
	if humanoidRoot then
		humanoidRoot.CFrame = waypoints[1].CFrame + Vector3.new(0, 3, 0)
	end

	table.remove(waypoints, 1)

	task.spawn(function()
		moveNPCAlongWaypoints(npc, waypoints)
	end)
end

--// Initialize
while true do
	if #NPCSFolder:GetChildren() < NPCS_CAP then
		SpawnCivilian()
	end
	task.wait(SPAWN_INTERVAL)
end

what I meant by “not smooth”

heres what the “smooth but broken” code looks like

(the path are numbered from 1 to the last possible number)

1 Like

I believe that in the “not smooth” example, the issue mainly stems from the NPC stopping for a small amount of time, at least until the next Heartbeat, before they begin moving to the next waypoint.
The “smooth” one has some questionable code, ex.

for _, point in ipairs(points) do
	humanoid:MoveTo(point.Position)
end

This is being done within your moveToNext function, alongside your path creation. Shouldn’t you be creating just one path, and then iterating through the waypoints? Re-computing the waypoints after reaching the previous one would cause it to infinitely follow a path

1 Like

after changing things, I came upon this code

local function walkAlongPath(NPC:Model, Waypoints)
	local humanoid = NPC:FindFirstChildWhichIsA("Humanoid")
	if not humanoid then return end

	for i = 1, #Waypoints do
		local waypointPart = Waypoints[i]
		if waypointPart and waypointPart:IsA("BasePart") then
			humanoid:MoveTo(waypointPart.Position)
			humanoid.MoveToFinished:Wait()
		end
	end
	
	task.wait(1)
	if NPC and NPC.Parent then
		NPC:Destroy()
	end

end

the problem with this one is they skip to the next point before reaching their current point (skips to 3 halfway to 2)

2 Likes

lets use the not smooth but follows path one. are you looping the animation or replaying it every few seconds?

im using the default animate script, I just changed their animations

I tested it out and your right, they stop for a brief amount of time each waypoint

Ok, I found a solution. I saw a script where they used repeat loop to move the humanoid into position until the magnitude of the torso and the destination is <= a value, and it worked how I imagine it would.

It also made the whole script much simpler

local function walkAlongPath(NPC: Model, Path)
	local humanoid = NPC:FindFirstChildWhichIsA("Humanoid")
	if not humanoid then return end

	local torso = NPC:FindFirstChild("Torso") or NPC:FindFirstChild("HumanoidRootPart")
	if not torso then return end
	
	for i, point in ipairs(Path) do
		repeat
			humanoid:MoveTo(point .Position)
			task.wait(0.5)
		until (torso.Position - point.Position).Magnitude <= 3
	end

	task.wait(1)
	if NPC and NPC.Parent then
		NPC:Destroy()
	end
end

though it still stops for a small amount of time each destination, least it actually goes to the destination

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.