- What do you want to achieve?
- I want to be able to manage a maximum amount of NPCs (very basic roaming zombies) whilst being able to keep a steady 60-65fps on BOTH Client and Server (currently only working on server, but a client compatibility is necessary for what I want to achieve). This system also needs to be able to adjust quickly: For example, if a player’s fps changes to 110, because of streaming, I want to be able to use their extra computational power OR on the contrary, if a player’s fps lowers, I want to be able to lower its computation load.
- What have you tried so far?
- Currently, my system tries to find the amount of time it takes, within each frame, to calculate everything (work time), and also tries to find the amount of time that is considered as sleep (no work done within the frame, just waiting). To figure these two values, I use the events from
RunService:RunService.PreAnimationandRunService.PostSimulation. I have not been able to think of any other non-waky way of proceding with this task.
- What is the issue?
- The freetime is completely wrong, since when there is an overload of NPCs, my frames start dropping down under 60fps, yet it says there is still ~4ms of freetime, making my script think it can still load more NPCs. NOTE: It is easy to calculate the average processing time per NPC (Work time / NPCCount) IF the numbers were correct. IMPORTANT: On roblox, the server’s fps (or heartbeat) is capped at 60fps, but on roblox studio, the server fps can get up to 240 fps (so same as client). That is why this problem is so complicated.
- What solutions have you tried so far?
- I have, of course, looked for forums with the intent on being able to better find the values I mensionned above: work and sleep time, to no avail.
- What does your code look like?
Here is the full script so that you can try it out. Note that you NEED to input a valid R6 character (or R15 if you want) AND that it’s a server script.
local RunService = game:GetService("RunService")
local PhysicsService = game:GetService("PhysicsService")
local GroupName = "Zombies"
PhysicsService:RegisterCollisionGroup(GroupName)
PhysicsService:CollisionGroupSetCollidable(GroupName, GroupName, false)
--SETTINGS--
local FrameSafetyPercentage = 5 --The computational should take 1/(60 + FrameSafetyPercentage)
local MovePerFrame = 300 --The average move time in frames
local ZoneSize = 500 --The size eof the zone in which the NPCs walk in
local R6 = workspace.Humanoids["1"].R6
--ends here--
local FrameSafety = 60 * (1 + FrameSafetyPercentage/100)
ZoneSize /= 2
for _, part in pairs(R6:GetChildren()) do
if part:IsA("Part") then
part.CollisionGroup = GroupName
end
end
local function WalkTo(humanoid)
task.spawn(function()
local goal = Vector3.new(
Random.new():NextNumber(-ZoneSize,ZoneSize),
1,
Random.new():NextNumber(-ZoneSize,ZoneSize)
)
repeat
humanoid:MoveTo(goal)
local success = humanoid.MoveToFinished:Wait()
until success
humanoid.Parent:SetAttribute("WalkingToGoal", false)
end)
end
local NPCCount = 0
local lastRemovedAI = 0
local lastAddedAI = 0
local function NewAI()
print("NewAI Added!")
local new = R6:Clone()
new.Parent = workspace
new:MoveTo(Vector3.yAxis * 3)
NPCCount += 1
lastAddedAI = tick()
end
local function RemoveAI()
print("Removing an AI")
local ToDestroy = workspace:FindFirstChild("R6")
if ToDestroy ~= nil then
ToDestroy:Destroy()
NPCCount -= 1
lastRemovedAI = tick()
end
end
local TotalWorkFrames = 0
local TotalWorkTime = 0
local TotalFreeTime = 0
local TotalFreeFrames = 0
local StartedWork = 0
local StartedFree = 0
local WorkTime
local FreeTime
--Started Simulation
RunService.PreAnimation:Connect(function(deltaTime)
local now = os.clock()
StartedWork = now
local LocalFreeTime = now - StartedFree
FreeTime = LocalFreeTime
if StartedFree ~= 0 then
TotalFreeTime += LocalFreeTime
TotalFreeFrames += 1
task.delay(1, function()
TotalFreeTime -= LocalFreeTime
TotalFreeFrames -= 1
end)
end
end)
--Finished Simulation
RunService.PostSimulation:Connect(function(deltaTimeSim)
local now = os.clock()
WorkTime = now - StartedWork
StartedFree = now
if StartedWork ~= 0 then
local LocalWorkTime = WorkTime
TotalWorkTime += LocalWorkTime
TotalWorkFrames += 1
task.delay(1, function()
TotalWorkTime -= LocalWorkTime
TotalWorkFrames -= 1
end)
end
end)
task.wait(3) --Waiting for everything to load
local TotalTimePerNPCs = 0
local TotalFrames = 0
RunService.Heartbeat:Connect(function(deltaTime)
if NPCCount > 0 then
TotalTimePerNPCs += WorkTime/NPCCount
TotalFrames += 1
task.delay(1, function()
TotalTimePerNPCs -= WorkTime/NPCCount
TotalFrames -= 1
end)
end
local AverageTimePerNPCs = TotalTimePerNPCs/TotalFrames
local AverageFreeTime = TotalFreeTime/TotalFreeFrames
print("Space left for "..AverageFreeTime/AverageTimePerNPCs.." more NPCs", "Average Free Time (in ms): "..AverageFreeTime * 1000, "Average Simulation Time per NPC (in ms) : "..AverageTimePerNPCs * 1000)
if NPCCount < 100 then
NewAI()
elseif AverageFreeTime/AverageTimePerNPCs > 20 then
NewAI()
end
--Moves the NPCs if the have finished their previous goal or if a 1/MovePerFrame chance is hit
for _, char in pairs(workspace:GetChildren()) do
if char:FindFirstChild("Humanoid") and char:GetAttribute("WalkingToGoal") == false or math.random(1, math.ceil((MovePerFrame/60)/deltaTime)) == 1 then
char:SetAttribute("WalkingToGoal", true) --Attribute to detect if it has finished its goal
local humanoid = char.Humanoid
WalkTo(humanoid)
end
end
end)
This post would have to be my last solution to this complicated problem, and I am hoping to be able to get help to find these two values or to get other ideas on how to make this work. Any help is greatly appreciated and I hope I can answer all of your questions as quickly as possible!