-
What do you want to achieve? I need the NPCs to follow their pathfinding waypoints without pausing at each waypoint
-
What is the issue? See video below. The NPCs pathfind perfectly fine when the game has just begun, but overtime, their pathfinding gets progressively laggier - as in, the NPC stalls when beginning the path, and then pauses at each path waypoint.
-
What solutions have you tried so far? I found a few forum posts that mention this exact issue. The only solution ever provided is to SetNetworkOwner to nil, but that does not solve the problem.
Posted are two videos: one showing the problem, and one walking through the script I use for my NPCs. If you could give me any guidance on fixing this issue, it would be appreciated immensely. I will also be pasting the module script in a codeblock, since the videos will not last on Streamable forever.
https://streamable.com/x4jey2
https://streamable.com/k8q1oz
local npcHandler = {}
local PathfindingService = game:GetService("PathfindingService")
local ServerStorage = game:GetService("ServerStorage")
local PhysicsService = game:GetService("PhysicsService")
local ChatService = game:GetService("Chat")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local GiveCustomerItemEvent = ReplicatedStorage.RemoteEvents.GiveCustomerItem
local npcSpawnPart = workspace.NPCSpawnPart
local unoccupiedSeats = {}
local friendUserIds = {}
local friendDisplayNames = {}
local allFriendsPages = {}
function npcHandler.Main(npc)
local humanoid:Humanoid = npc:WaitForChild("Humanoid")
for i, obj in npc:GetChildren() do
if obj:IsA("BasePart") then
obj:SetNetworkOwner(nil)
end
end
--Making sure the players actually have friends to load data from
if #friendUserIds ~= 0 then
--Dress up the npc like a friend of a player
--Pick a randomly userId from the friendUserIds list
local randomFriendUserId = friendUserIds[math.random(#friendUserIds)]
--The humanoidDescription is an object that defines a rig's clothes, skintone and accessories
humanoid:ApplyDescription(Players:GetHumanoidDescriptionFromUserId(randomFriendUserId))
humanoid.DisplayName = friendDisplayNames[randomFriendUserId]
end
--First, choose a place to sit
--The second argument is inclusive, unlike in other languages
local randomSeat = math.random(#unoccupiedSeats)
--I dont know if this is assigned as a reference or as a value. I guess I will find out!
local chosenSeat = unoccupiedSeats[randomSeat]
--Needs to happen ASAP because as we're removing it, other NPCs are reading from that table, and that's scary
table.remove(unoccupiedSeats, randomSeat)
local startTime = npcHandler.WalkTo(npc, chosenSeat)
npcHandler.OrderFood(npc, startTime, chosenSeat)
end
function npcHandler.WalkTo(npc, destination)
--Roblox can do explict typecasting using :TypeName
--This will allow the IDE to give you code suggestions based on the type
local humanoid:Humanoid = npc.Humanoid
local humanoidRootPart = npc:WaitForChild("HumanoidRootPart")
--Allows the NPC to move if they were sitting before calling WalkTo
humanoid.Sit = false
--We must stop the npc from sitting preemptively on its way to a seat
humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, false)
--local path = npcHandler.GetPath(npc, destination)
--Moved GetPath into this function because you would never do one but not the other
local pathParams = {
["AgentCanJump"] = false,
["Costs"] = {
--Makes any path that goes over the Wood texture (such as our tables) completely intransversible
--However, our NPCs will thankfully ignore this once they reach the seat they need to be in...
--Perhaps the seats are not actually considered as being pathed over, and instead we're just avoiding the tables?
--Even still, setting the pathing cost to 500 still has them path over the tables, despite the high cost and nearby alternate
--pathes over the floor
--Incredibly, this also fixed our issues of npcs soemtimes not getting close enough to sit on the chairs. Somehow.
Wood = math.huge
}
}
local path = PathfindingService:CreatePath(pathParams)
--We must put the Y value at floor level because the pathfinding system thinks the npc cant jump onto the seat
local flooredDestination = Vector3.new(destination.Position.X, 1, destination.Position.Z)
path:ComputeAsync(humanoidRootPart.Position, flooredDestination)
for i, waypoint in pairs(path:GetWaypoints()) do
humanoid:MoveTo(waypoint.Position)
humanoid.MoveToFinished:Wait()
end
--After the npc reaches a seat, allows it to sit
--This does not force it to sit - only allows it to happen
--We do this after the NPC has moved so that it doesnt accidentally sit in a different seat on the way to its intended seat
humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, true)
--Tells the npc to move to the seat again, just incase its not close enough to sit
humanoid:MoveTo(flooredDestination)
return os.time()
end
function npcHandler.OrderFood(npc, startTime, chosenSeat)
local connection
ChatService:Chat(npc.Head, "I want a water!", Enum.ChatColor.White)
connection = GiveCustomerItemEvent.OnServerEvent:Connect(function(sender, cupTool, recievingNPC)
if npc ~= recievingNPC then return end
--We manually disconnect this connection because it is made by a coroutine within a serverscript
--Thus it will never be automatically disconnected, even when the NPC is destroyed
connection:Disconnect()
ChatService:Chat(npc.Head, "Thanks Buddy!", Enum.ChatColor.White)
cupTool.Parent = npc
npcHandler.PayAndLeave(npc, startTime, chosenSeat)
end)
end
function npcHandler.PayAndLeave(npc, startTime, chosenSeat)
local totalTime = os.time() - startTime
print("It took ", totalTime, " seconds for this NPC to be served!")
task.wait(1)
npcHandler.WalkTo(npc, npcSpawnPart)
table.insert(unoccupiedSeats, chosenSeat)
task.wait(1)
npc:Destroy()
end
--*** FUNCTIONS NOT SPECIFIC TO ANY 1 NPC
function npcHandler.Spawn(name, quantity)
local npcExists = ServerStorage.NPCs:FindFirstChild(name)
if npcExists then
for i=1, quantity do
local newNpc = npcExists:Clone()
newNpc.Parent = workspace.SpawnedNPCs
newNpc.HumanoidRootPart.CFrame = workspace.NPCSpawnPart.CFrame
coroutine.wrap(npcHandler.Main)(newNpc)
task.wait(1)
end
else
warn("NPC does not exist: " .. name)
end
end
function npcHandler.SetUpSeats()
for i, obj in pairs(workspace.Map:GetDescendants()) do
if obj:IsA("Seat") then
table.insert(unoccupiedSeats, obj)
end
end
end
--*** CONNECTIONS
Players.PlayerAdded:Connect(function(player)
--Getting friends returns a FriendsPage object, which is a Page object containing tables of displayname, id, isOnline, and username
--Its like a 2 dimensional array, but accessed differently
local friendsPages = Players:GetFriendsAsync(player.UserId)
local cachedPages = {}
--Inserting the friendsPages object into a table for quick, easy access for when the player leaves
--allFriendsPages[player.UserId] = friendsPages
while true do
--Cacheing the each page for future use
--This creates a 2 dimensional table with the structure: pageNumber, friendNumber, contents
table.insert(cachedPages, friendsPages:GetCurrentPage())
--We must do this before advancing because if we did it the other way (or had this statement in the while condition)
--it would skip the last page of friends
--since it'd technically have reached the end of the pages, but never had run them through the loop
if friendsPages.IsFinished then break end
friendsPages:AdvanceToNextPageAsync()
end
for i, page in cachedPages do
--cachedPages[i] is looping through every friendNumber (aka user table) in a specific page
--i2 is the index of the Page's table, which contains friendNumbers which contain the userIds we're after
for i2, user in cachedPages[i] do
--Can also be accessed like user["Id"], since these are really just dictionaries
table.insert(friendUserIds, user.Id)
--Since we're here, I'll also grab their display names, to name the npcs just like their friends!
--For ease of access, we'll need to make this a dictionary
friendDisplayNames[user.Id] = user.DisplayName
end
end
--This creates a 3 dimensional table with the structure: playerUserId, pageNumber, friendNumber, contents
allFriendsPages[player.UserId] = cachedPages
end)
Players.PlayerRemoving:Connect(function(player)
--grabs the 2 Dimensional Table that stores the pages of friend data tables of the user that's leaving
local cachedPages = allFriendsPages[player.UserId]
--removing this player's cachedPages from the allFriendsPages table since theyre leaving
allFriendsPages[player.UserId] = nil
for i, page in cachedPages do
for i2, user in cachedPages[i] do
local indexInTable = table.find(friendUserIds, user.Id)
table.remove(friendUserIds, indexInTable)
friendDisplayNames[user.Id] = nil
end
end
end)
return npcHandler




