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
"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()
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.
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)
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.
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.?
--[[
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)
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