[SOLVED] My game is lagging uncontrollably, and I don't know if its fixable

This is a complex thread, but I really need help.
I don’t expect anyone to rewrite this for me, I just want ideas on how I can optimize it.

My game has been lagging for ages, and I don’t know how to fix it. It doesn’t lag on the client however, it lags on the server. I’m preforming loads of calculations on the server every second, but, there is no other way to do it.

I came here looking for help and if there is anyway I can optimize this.

My game is supposed to be a satisfying farming game, where you collect large amounts of crops instantly and smoothly

Here is footage of my game:

External Media

You can see I’m collecting a lot, and it looks instant, right? So what’s the problem?
Well it only looks instant on the client, the crop gets teleported away out of view as the server preforms its calculations

I’m sending a list of the crops that were collected in that frame to the server every frame using a remote event, and then having each crop calculated on individually.

But with all of these crops being processed each frame, the server slows down, and has less time to compute other game stuff.

I’m going to share all of my code, I know I shouldn’t but this is important to me.
It’s uncommented, so I apologize, but if you can bear through it and help me solve this I would appreciate it.

keep in mind, sometimes crops will regrow instantly, if that may be an issue

tool code (client) :

local module = {}
local neededparent = workspace.crops
local rs = game:GetService('ReplicatedStorage')
local runservice = game:GetService('RunService')
local grow = require(rs.Dependencies.GrowV2)
local harvest = grow.harvest

local connections = {}

function module.removeconnections()
	for _,c in pairs( connections) do
		c:Disconnect()
	end

	connections = {}
end


function module.main(tool)
	tool.Equipped:Connect(function()
		game.ReplicatedStorage.events.CalculatePassives:FireServer()
	end)
	
	
	local c = runservice.Heartbeat:Connect(function()
		rs.events.cropdestoryed:FireServer(tool.hb.CFrame, tool.hb.Size, tool.hb.multiplier.Value)
	end)
	table.insert(connections, c)

	tool.hb.Touched:Connect(function(part)
		if part.Parent == neededparent then

			part.Position = Vector3.new(-100,-100,-100)

		end
	end)
end


script that sends client messages to the harvest module:

local rs = game:GetService('ReplicatedStorage')
local neededparent = workspace:WaitForChild('crops')

local grow = require(rs.Dependencies.GrowV2)
local harvest = grow.harvest


rs.events.cropdestoryed.OnServerEvent:Connect(function(plr,cframe,size, multiplier)
	local crops = workspace:GetPartBoundsInBox(cframe, size)
	for _,crop in pairs(crops) do
		if crop.Parent == neededparent and crop.CanBeDestroyed.Value == true then
			crop.CanBeDestroyed.Value = false
			task.spawn(function()
				harvest(plr, crop, multiplier)end)
		end
		
	end
end)

main harvest module:

local module = {}

local rs = game:GetService('ReplicatedStorage')
local dependencies = require(rs.Dependencies.Dependencies)
local fieldDimensions = dependencies.FeildDimensions
local croptemplate = rs.spawnables.Crop
local cropcontainer = workspace.crops -- folder to hide when debugging ! :D :D
local cropamtlist = rs.globalvalues.CropAmt
local values = rs.globalvalues
local events = rs.events

local cropinfo = {
	['Classic'] = {
		{['cropname'] = 'wheat', ['brickcolor'] = BrickColor.new('Parsley green')};
		{['cropname'] = 'apple', ['brickcolor'] = BrickColor.new('Bright red')};
		{['cropname'] = 'carrot', ['brickcolor'] = BrickColor.new('Neon orange')};
		{['cropname'] = 'diamond', ['brickcolor'] = BrickColor.new('Cyan')};
		{['cropname'] = 'lavarock', ['brickcolor'] = BrickColor.new('Maroon'), ['material'] = Enum.Material.Neon, ['rareity'] = 150, ['effects'] = rs.spawnables.rare};
		{'none'};
		['material'] = Enum.Material.Sand;
		['height'] = 2.2;
		['croplimit'] = 10_000;
	};
	
	['Volcanic'] = {
		{['cropname'] = 'volcanic wheat', ['brickcolor'] = BrickColor.new('Parsley green')};
		{['cropname'] = 'volcanic apple', ['brickcolor'] = BrickColor.new('Bright red')};
		{['cropname'] = 'volcanic carrot', ['brickcolor'] = BrickColor.new('Neon orange')};
		{['cropname'] = 'black diamond', ['brickcolor'] = BrickColor.new('Really black')};
		{['cropname'] = 'lavarock', ['brickcolor'] = BrickColor.new('Maroon'), ['material'] = Enum.Material.Neon, ['rareity'] = 50, ['effects'] = rs.spawnables.rare};
		{'none'};
		['material'] = Enum.Material.Sand;
		['height'] = 2.2;
		['croplimit'] = 10_000;
	};
	
	['Aquatic'] = {
		{['cropname'] = 'aquatic wheat', ['brickcolor'] = BrickColor.new('Parsley green')};
		{['cropname'] = 'aquatic apple', ['brickcolor'] = BrickColor.new('Bright red')};
		{['cropname'] = 'aquatic carrot', ['brickcolor'] = BrickColor.new('Neon orange')};
		{['cropname'] = 'sulfur', ['brickcolor'] = BrickColor.new('New Yeller')};
		{['cropname'] = 'bubble', ['brickcolor'] = BrickColor.new('Dark blue'), ['material'] = Enum.Material.Neon, ['rareity'] = 50, ['effects'] = rs.spawnables.bubblerare};
		{'none'};
		['material'] = Enum.Material.Sand;
		['height'] = 2.2;
		['croplimit'] = 10_000;
	};
	
	['Planetoid'] = {
		{['cropname'] = 'cosmic wheat', ['brickcolor'] = BrickColor.new('Parsley green')};
		{['cropname'] = 'cosmic apple', ['brickcolor'] = BrickColor.new('Bright red')};
		{['cropname'] = 'cosmic carrot', ['brickcolor'] = BrickColor.new('Neon orange')};
		{['cropname'] = 'cosmic diamond', ['brickcolor'] = BrickColor.new('Eggplant')};
		{['cropname'] = 'moondust', ['brickcolor'] = BrickColor.new('Really black'), ['material'] = Enum.Material.Neon, ['rareity'] = 50, ['effects'] = rs.spawnables.moondustrare};
		{'none'};
		['material'] = Enum.Material.Sand;
		['height'] = 2.2;
		['croplimit'] = 30_000;
	};
	
	['Glacier'] = {
		{['cropname'] = 'icy wheat', ['brickcolor'] = BrickColor.new('Parsley green')};
		{['cropname'] = 'icy apple', ['brickcolor'] = BrickColor.new('Bright red')};
		{['cropname'] = 'icy carrot', ['brickcolor'] = BrickColor.new('Neon orange')};
		{['cropname'] = 'icy diamond', ['brickcolor'] = BrickColor.new('Cyan')};
		{['cropname'] = 'icicle', ['brickcolor'] = BrickColor.new('Navy blue'), ['material'] = Enum.Material.Glacier, ['rareity'] = 50, ['effects'] = rs.spawnables.iciclerare};
		{'none'};
		['material'] = Enum.Material.Glacier;
		['height'] = 2.2;
		['croplimit'] = 30_000;
	};
	
	['Desert'] = {
		{['cropname'] = 'deserted wheat', ['brickcolor'] = BrickColor.new('Parsley green')};
		{['cropname'] = 'deserted apple', ['brickcolor'] = BrickColor.new('Bright red')};
		{['cropname'] = 'deserted carrot', ['brickcolor'] = BrickColor.new('Neon orange')};
		{['cropname'] = 'sand', ['brickcolor'] = BrickColor.new('Bright orange')};
		{['cropname'] = 'glass', ['brickcolor'] = BrickColor.new('Institutional white'), ['material'] = Enum.Material.Glass, ['rareity'] = 50, ['effects'] = rs.spawnables.nonerare};
		{'none'};
		['material'] = Enum.Material.Sand;
		['height'] = 2.2;
		['croplimit'] = 30_000;
	};

	['Neon'] = {
		{['cropname'] = 'neon wheat', ['brickcolor'] = BrickColor.new('Parsley green')};
		{['cropname'] = 'neon apple', ['brickcolor'] = BrickColor.new('Bright red')};
		{['cropname'] = 'neon carrot', ['brickcolor'] = BrickColor.new('Neon orange')};
		{['cropname'] = 'neon diamond', ['brickcolor'] = BrickColor.new('Steel blue')};
		{['cropname'] = 'pink crystal', ['brickcolor'] = BrickColor.new('Carnation pink'), ['material'] = Enum.Material.Neon, ['rareity'] = 50, ['effects'] = rs.spawnables.pinkrare};
		{['cropname'] = 'purple crystal', ['brickcolor'] = BrickColor.new('Royal purple'), ['material'] = Enum.Material.Neon, ['rareity'] = 200, ['effects'] = rs.spawnables.purplerare};
		['material'] = Enum.Material.Neon;
		['height'] = 247.697;
		['croplimit'] = 100_000;
	};
	
}
function module.grow(field, crop)

	if cropamtlist[field].Value < cropinfo[field].croplimit then
		local newx = math.random(fieldDimensions[field].minx, fieldDimensions[field].maxx)
		local newz = math.random(fieldDimensions[field].minz, fieldDimensions[field].maxz)
		crop.CanBeDestroyed.Value = true
		crop.Position = Vector3.new(newx, cropinfo[field].height, newz)
		crop.Material = cropinfo[field].material
		local rarity
		local raritynum = math.random(1, 100)
		crop.Feild.Value = field
		if raritynum <= 81 then
			crop.Type.Value = cropinfo[field][1].cropname
			crop.BrickColor = cropinfo[field][1].brickcolor
		elseif raritynum <= 95 then
			crop.Type.Value = cropinfo[field][2].cropname
			crop.BrickColor = cropinfo[field][2].brickcolor
		elseif raritynum <= 99 then
			crop.Type.Value = cropinfo[field][3].cropname
			crop.BrickColor = cropinfo[field][3].brickcolor
		elseif raritynum == 100 then
			local randomChance = math.random(1, rs.globalvalues.DiamondRarity.Value)
			if randomChance == 1 then
				crop.Type.Value = cropinfo[field][4].cropname
				crop.BrickColor = cropinfo[field][4].brickcolor
				if math.random(1,cropinfo[field][5].rareity) == 1 then
					if cropinfo[field][6][1] ~= 'none' then -- if 6th crop
						if math.random(1,cropinfo[field][6].rareity) == 1 then
							crop.Type.Value = cropinfo[field][6].cropname
							crop.BrickColor = cropinfo[field][6].brickcolor
							crop.Material = cropinfo[field][6].material
							for k,v in pairs(cropinfo[field][6].effects:GetChildren()) do
								v:Clone().Parent = crop 
							end
						end
					else --5th crop
						
						crop.Type.Value = cropinfo[field][5].cropname
						crop.BrickColor = cropinfo[field][5].brickcolor
						crop.Material = cropinfo[field][5].material

						for k,v in pairs(cropinfo[field][5].effects:GetChildren()) do
							v:Clone().Parent = crop 
						end
					end
				end
			else
				crop.Type.Value = cropinfo[field][3].cropname
				crop.BrickColor = cropinfo[field][3].brickcolor
			end




		end
		crop.Parent = workspace.crops

	else
		crop:Destroy() -- remove from memory, it wasnt used

	end







end


function module.harvest(player, part, multi)
	local field = player.Values.Feild.Value

	local insprinklerrange = false
	local sprinkmulti = 1
	for k,v in pairs(workspace.Sprinklers:GetChildren()) do
		local distance = (part.Position - v.Main.Position).Magnitude
		if distance <= v:GetAttribute('Range') then
			insprinklerrange = true
			sprinkmulti += v:GetAttribute('Multiplier')
		end
	end


	local amt = 1*multi
	local special = 'None'
	local special2 = ''
	local special3 = ''
	local totalspecial

	if Random.new():NextNumber(0.1,100) <= player.Values.CritChance.Value then
		amt = player.Values.CritPower.Value*multi
		special = 'Crit'

	end
	if Random.new():NextNumber(0.1,100) <= player.Values.GoldenChance.Value + values.WeatherGolden.Value then
		amt = amt*5
		special2 = 'Golden'

	end

	if Random.new():NextNumber(0.1,100) <= player.Values.RainbowChance.Value + values.WeatherRainbow.Value then
		amt = amt*10
		special3 = 'Rainbow'

	end
	-- feildmulti, final amt calculations

	amt = amt * player.Values.FeildMultis[field].Value * sprinkmulti * player.Values.FriendMulti.Value
	player.Values[part.Type.Value].Value +=amt
	events.CalculateQuestProgress:Fire(player, part, part.Type.Value, special, special2)
	events.CalculateQuestProgress:Fire(player, part, part.Type.Value, 'Collect', amt)	
	local charmlist = require(rs.Lists)['Charms']
	local itemlist = require(rs.Lists)['Items']
	local charmforagechance = rs.globalvalues.CharmRarity.Value - player.Values.CharmChance.Value

	-- max charmforage change (w/o gamepass)
	if charmforagechance < 500 then
		charmforagechance = 500
	end

	local foragechance = player.Values.ForageChance.Value

	if math.random(1,charmforagechance/foragechance) == 1 then
		local charm = charmlist[math.random(1,#charmlist)]
		game.ReplicatedStorage.events.Notification:FireClient(player, 'You found a '.. charm.. ' charm!')
		player.Values.Charms[charm..'1Charms'].Value += 1
		rs.events.InventoryNotif:FireClient(player, 'Charm')
		game.ReplicatedStorage.events.NewEnchant:FireClient(player)
		events.CalculateQuestProgress:Fire(player, nil, 'Forage', nil, nil)
	end
	local itemlist = require(rs.Lists)['Items']
	if math.random(1,50_000/foragechance) == 1 then
		local item = itemlist[math.random(1,#itemlist)]
		game.ReplicatedStorage.events.Notification:FireClient(player, 'You found a '..item.. '!')
		player.Values.Items[item].Value += 1
		game.ReplicatedStorage.events.NewItem:FireClient(player)
		events.CalculateQuestProgress:Fire(player, nil, 'Find', nil, nil)
	end
	totalspecial = special..special2..special3

	player.PlayerGui.HarvestMarker.Start:FireClient(player, amt, part.Type.Value, part.BrickColor, totalspecial)




	if (values.InstaGrowback.Value or insprinklerrange)  and part:FindFirstChild('effect') == nil then
		module.grow(field,part)
	else

		part:Destroy()
		cropamtlist[field].Value -= 1
	end





end




return module


I know this is a lot, but I can’t publish a laggy game and I have no solutions.
I am open to answering any questions, if you have any tell me!

1 Like

I haven’t thoroughly looked through the entire code so I can’t tell you what calculations are causing all the lag but I recommend you take a look at the micro profiler to see what exactly is taking the longest to compute yourself. A really good way to handle complex computations for large batches of data is to use parallel luau. Parallel luau lets you run code in parallel to reduce computation time. You can find the documentation for it here.

After you have found the problematic lines in your code you should then see if you can optimize said parts. After that you can work on implementing parallelisation. It is important that you try to optimize slow parts of your code first as parallelisation does not actually reduce load on the server, it just lets you utilize the servers CPU processing power more efficiently.

2 Likes

There are a few things you could do to help with performance:

  • Instead of firing an event every heartbeat to collect crops (heavy on performance), have it only fire when a crop has been collected instead of constantly firing the server
  • Instead of the crop teleporting out of view, you could unload/delete it, then create a new one when needed

Try something more like this:

function module.main(tool)
	tool.Equipped:Connect(function()
		game.ReplicatedStorage.events.CalculatePassives:FireServer()
	end)

	tool.hb.Touched:Connect(function(part) -- assuming this is where the detection for the touch happens
		if part.Parent == neededparent then

			part:Destroy()
			rs.events.cropdestoryed:FireServer(tool.hb.CFrame, tool.hb.Size, tool.hb.multiplier.Value)
		end
	end)
end
2 Likes

This was my previous method, but with so many crops being collected it goes over the maximum allowed remote events a second. I also cant delete the part because sometimes it is moved to a new place if it is meant to be instantly regrown to save performance.

1 Like

I know the crops have to be removed instantly, but do the results have to be instant to? You could batch the results of multiple crops, add up their scores or amount or similar, then send that instead? That way it looks fine, and the game runs better

currently im not processing the values of crops on the client because thats exploitable, but i do combine that frames crops into a list and then send that list to the server. but the server is doing so much work with all of those crops. its like having a while true loop without a wait, it just freezes the game until its done computing all the crops. the results dont need to be instant and i already have a solution for removing the crops from view instantly, i just need a solution to calculating crop related stuff on the server without lag.

1 Like

is the mico profiler available for serverside?

This post explains how to use it server side:

This post led me to experiment with the microprofiler, but also performance stats, which uncovered my issue: WAY too much data was being received.

I had already fixed the issue of sending too much data a month ago by combining all of the crops into one remote event send every 60 seconds, but I forgot that the server also sends back the computed crops back to the client to display as a textlabel listing what you did. Now 100k text labels would be incredibly laggy, so I combined them on the client side, but the server still sent every crop to the client.

So, when I looked at the received portion of performance stats, I realized it SPIKED up to 100kb/s when I collected, I instantly knew what to do.

I combined all of the players collection every 0.1 seconds into a table and sent it to the client. I also abbreviated the values when they got sent over (they were in the trillions) so they would take up less bytes. I even changed the way it tagged different collections to save space! Instead of “CritGoldenRainbow” it was simply “CGR”, and no specials was compressed from “NoneNoneNone” to “”.

for anyone else with this problem: minimize the amount of data you send between the client and server.

1 Like

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