Npcs lags too much in larger numbers

I am making rts style game with main focus on indirectly controling NPCs, but when there are too much npcs in the game, it starts extremely lagging. I have done some testing, and came to conclusion that pathfinding is the heaviest part of the whole ai script, but I dont really know how to optimalise that since its official roblox made. Anyways I would like as much suggestions as possible to make my script run faster. Any feedback will be appriciated.
My script:

local humanoid = script.Parent:WaitForChild("Humanoid")
local npcRoot = script.Parent:WaitForChild("HumanoidRootPart")
local team = script.Parent:WaitForChild("Team")
local inDanger = script.Parent:WaitForChild("inDanger")
local pathService = game:GetService("PathfindingService")
local stats = script.Parent.stats:GetChildren()
local walkStatus = 0
local requireToLook = true
local FoodGatherAnim = humanoid.Animator:LoadAnimation(script.Parent.Animations:WaitForChild("FoodSource"))
local waveAnim = humanoid.Animator:LoadAnimation(script.Parent.Animations:WaitForChild("WaveAnim"))
local PhysicsService = game:GetService("PhysicsService")
local blockedConnection
local nextWaypointIndex
local userdata = game:GetService("ReplicatedStorage"):WaitForChild("UserData")
local owner = script.Parent:WaitForChild("Owner")
npcRoot:SetNetworkOwner(nil)
local home = workspace:WaitForChild("Home2")

task.wait(2)
for _, child in ipairs(script.Parent:GetChildren()) do
	if child:IsA("MeshPart") or child:IsA("Part") or child:IsA("BasePart") then
		PhysicsService:SetPartCollisionGroup(child, "npcGroup")
	end
end


local function getStat(stat)
	for i,v in pairs(stats) do
		if v.Name == stat then
			return v
		end
	end
end

local GatherType = getStat("GatherType")

local function findNearestGather(object)
	local currentNearest = nil
	local objects = workspace:FindFirstChild(object)
	if objects == nil then
		return nil
	end
	objects = objects:GetChildren()
	for i,v in pairs(objects) do
		if v:FindFirstChild("Amount").Value > 0 then
			if currentNearest == nil then
				currentNearest = v
			elseif (npcRoot.Position - v.PrimaryPart.Position).Magnitude < (npcRoot.Position - currentNearest.PrimaryPart.Position).Magnitude then
				currentNearest = v
			end
		end
	end
	return currentNearest
end

local function pathFind(destination)
	requireToLook = true
	if walkStatus ~= 0 then walkStatus = 2 end
	repeat task.wait(0.1) until walkStatus == 0
	nextWaypointIndex = 1
	walkStatus = 1
	local path = pathService:CreatePath({
		AgentCatJump = false
	})
	local success, errorMessage = pcall(function()
		path:ComputeAsync(npcRoot.Position, destination.Position - CFrame.new(npcRoot.Position, destination.Position).LookVector * 4)
	end)
	if success and path.Status == Enum.PathStatus.Success then
		local waypoints = path:GetWaypoints()
		blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
			if blockedWaypointIndex >= nextWaypointIndex then
				blockedConnection:Disconnect()
				pathFind(destination)
			end
		end)
		for i, waypoint in ipairs(waypoints) do
			if walkStatus == 2 then
				walkStatus = 0
				break
			end
			humanoid:MoveTo(waypoint.Position)
			humanoid.MoveToFinished:Wait()
			nextWaypointIndex += 1
		end
		walkStatus = 0
	elseif path.Status == Enum.PathStatus.NoPath then
		waveAnim:Play()
		task.wait(1.5)
		waveAnim:Stop()
		walkStatus = 0
	end
end

local function isFull(resource)
	if resource == "FoodSource" then
		local maxfood = getStat("Food")
		local food = getStat("MaxFood")
		if maxfood.Value == food.Value then
			return true
		else
			return false
		end
	elseif resource == "WoodSource" then
		local maxfood = getStat("Wood")
		local food = getStat("MaxWood")
		if maxfood.Value == food.Value then
			return true
		else
			return false
		end
	end
end

local function gather(Source)
	if requireToLook == true then
		npcRoot.CFrame = CFrame.lookAt(npcRoot.Position, Source.PrimaryPart.Position)
		requireToLook = false
	end
	if GatherType.Value == "FoodSource" then
		FoodGatherAnim:Play()
	elseif GatherType.Value == "WoodSource" then
		FoodGatherAnim:Play()
	end
	task.wait(getStat("GatherTime").Value)
	if Source:FindFirstChild("Amount").Value <= 0 then
		return
	end
	Source:FindFirstChild("Amount").Value -= 1
	if GatherType.Value == "FoodSource" then
		local food = getStat("Food")
		food.Value += 1
	elseif GatherType.Value == "WoodSource" then
		local wood = getStat("Wood")
		wood.Value += 1
	end
end

local function returnResources()
	local food = getStat("Food")
	userdata:FindFirstChild(owner.Value.Name):FindFirstChild("Food").Value += food.Value
	food.Value = 0
	local wood = getStat("Wood")
	userdata:FindFirstChild(owner.Value.Name):FindFirstChild("Wood").Value += food.Value
	wood.Value = 0
end


local function mainLoop()
	local Source = findNearestGather(GatherType.Value)
	if inDanger.Value == true then
		pathFind(home)
		
	elseif isFull(GatherType.Value) == false then
		if Source ~= nil and (npcRoot.Position - Source.PrimaryPart.Position).Magnitude > 6 then
			pathFind(Source.PrimaryPart)
		end
		if (npcRoot.Position - Source.PrimaryPart.Position).Magnitude < 7 then
			gather(Source)
		end
	elseif isFull(GatherType.Value) == true then
		pathFind(home)
		if (npcRoot.Position - home.Position).Magnitude < 5 then
			returnResources()
		end
	end
end


while true do
	mainLoop()
end
1 Like

NPCs should run on OOP therefore lots of functions are not being created and they work independently.

What exactly is better on OOP? I find it very confusing

If Pathfinding is the bottleneck, you can’t optimize Roblox’s code, so you’ll have to find another way. A few ideas:

  • Have multiple NPCs follow the same path
  • Don’t use pathfinding for trivial paths
  • Custom pathfinding (i.e. maybe you only need to avoid walls)
  • Load a bunch of common paths and reuse them
3 Likes

"Object-Oriented Programming is a programming paradigm based on the concept of objects, which may contain data, in the form of fields, often known as attributes; and code, in the form of procedures, often known as methods. "

local npc = {
  name = "Bob",
  health = 100,
  speak = function(self)
    print("Hi, my name is " .. self.name)
  end
}

npc:speak()
1 Like

is using this method really better than using classic functions? I mean I can’t really see that big of a difference. I see this only just a different way to write functions

There are several reasons why using OOP to create NPCs is better than using regular functions. First, OOP allows for better organization of code. Functions can be organized into classes, and each class can be responsible for a different aspect of the NPC’s behavior. This makes the code easier to understand and maintain.

Second, OOP makes it easier to reuse code. If you need to create another NPC that behaves similarly to one you’ve already created, you can simply inherit the original NPC’s behavior. This saves you from having to rewrite the code for the new NPC from scratch.

Finally, OOP makes it easier to implement complex behavior. Functions are limited in what they can do, but objects can interact with each other in more complex ways. This makes it possible to create NPCs that behave in more realistic and believable ways.

2 Likes
function npc:onCreate()
  self.name = "Bob"
  self.age = 20
  self.job = "farmer"
end

function npc:onUpdate(dt)
  self.age = self.age + dt
end

function npc:onInteract(player)
  print("Hello, my name is " .. self.name)
end

I will try not to use pathfinding for trivial paths but all other suggestions are not possible because of my game design(loading common paths) or actual scripting experience(custom pathfinding)

You shouldn’t use Humanoids for NPCs, as Humanoid are expensive and for your use case pretty much replaceable.

how am I supposed to replace humanoid when roblox’s pathfinding is only for humanoids?

It isn’t Pathfinding, is only based on a BasePart’s Position and not Humanoids.

PathfindingService is used to find paths between two points.

2 Likes

I understand this better organisation of code and stuff, but we are talking about performance issues. How does using this method affect the performance ot the code?

It could be argued that using OOP to create NPCs is better performance wise because it is easier to manage code and data when it is organized in an object-oriented manner. However, it could also be argued that using regular functions is better performance wise because it is easier to optimize code that is not organized in an object-oriented manner.

1 Like

I will look into it but even if it is “expensive” as you say, is it worth the trouble having to rewrite animation script I use for the npc, writing new function to move the npc because I cant use humanoid:MoveTo()
and things like ragdoll on death etc. etc.?

Which is better?

--[[
    Create an example in Lua of OOP
]]

--[[
    Create a class
]]

local class = {}

function class:new(name, age)
    local object = {}
    setmetatable(object, self)
    self.__index = self
    object.name = name
    object.age = age
    return object
end

function class:print()
    print("Name: " .. self.name)
    print("Age: " .. self.age)
end

--[[
    Create a subclass
]]

local subclass = class:new()

function subclass:new(name, age, address)
    local object = class:new(name, age)
    setmetatable(object, self)
    self.__index = self
    object.address = address
    return object
end

function subclass:print()
    class.print(self)
    print("Address: " .. self.address)
end

--[[
    Create an object
]]

local object = subclass:new("John", 20, "New York")
object:print()

--[[
    Compare this to using regular functions
]]

local function class(name, age)
    local object = {}
    object.name = name
    object.age = age
    return object
end

local function subclass(name, age, address)
    local object = class(name, age)
    object.address = address
    return object
end

local object = subclass("John", 20, "New York")
print("Name: " .. object.name)
print("Age: " .. object.age)
print("Address: " .. object.address)
1 Like

yeah… I find using regular functions better, and also more time efficient

You’re obviously not thinking about the future. Regular functions are fine for now, but what about when your program needs to scale up? OOP is the only way to go if you want your code to be maintainable in the long run.

Think about all of the time you’ll save in the future by using OOP. You’ll be able to add new features and functionality much faster and with less code. Not to mention, your code will be much more organized and easier to read.

Don’t make the mistake of thinking regular functions are the way to go. OOP is the only way to ensure your code is future-proof.

Hopefully you can do some optimizing with trivial paths, but maybe some optimization can be done with agentParameters (PathfindingService:CreatePath), I haven’t tested, but maybe increasing WaypointSpacing? or even if you can eliminate jumping (and setting AgentCanJump) may help?
I really can’t think of a better way besides making your own unfortunately

I have already disabled agent jumping and having longer waypointSpacing probably wouldnt work because things like trees would then become killers for the npcs

1 Like