Coroutine lag/low fps, coroutine alternative?

Hi, thanks for reading this topic, this one will be a long one. Let’s start, I’m making a game that has ALOT of coroutines. ALOT. and for every player, say there are 6 coroutines, but that’s not all. on average, there are about 3 players in my game, since it’s pretty small and is in beta, but there is no actual low fps, my system for the game is using loops, which wait a said amount of time based on a predefined number, and then increment a certain value based on that same number. Almost like manual tweening. all of those loops i have though, are in a coroutine. One of them looks like this:

There’s another one of these, which looks like this:

-- xp handling
task.spawn(function()
	while wait(xpcooldown) do
		if #xps:GetChildren() < xpcount then
			local newxp = createmodule.createXP(heightfactor, false)
		end
	end
end)

Here’s the script that it’s running:

function module.createXP(heightfactor, isFeed)
	local xp = game.ServerStorage.XP.XP:Clone()
	local value
	
	if isFeed then
		local feedtag = Instance.new("BoolValue", xp)
		feedtag.Name = "IsFeed"
		feedtag.Value = true
		value = 5
	else
		value = xpminsize + (math.random(spawnsmoothness * (xpmaxsize - xpminsize)) / spawnsmoothness)
	end
	
	xp.Position = Vector3.new(math.random(-border*spawnsmoothness, border*spawnsmoothness)/spawnsmoothness, 0, math.random(-border*spawnsmoothness, border*spawnsmoothness)/spawnsmoothness)
	xp.Color = Color3.new(math.random(0, 255), math.random(0, 255), math.random(0, 255))
	xp.Parent = game.Workspace.XP
	xp.Size = Vector3.new(value * heightfactor, math.sqrt(value/6.25), math.sqrt(value/6.25))
	xp.XP.Value = value
	
	return xp
end

Here’s the thing, will this create lag?

There’s also another coroutine running, it looks like this:

-- virus handling
task.spawn(function()
	while wait(1) do
		if #viruses:GetChildren() < viruscount then
			local virus = createmodule.createVirus(heightfactor)
			wait(viruscooldown - 1)
		end
	end
end)

And the module function it’s running

function module.createVirus(heightfactor)
	local virus = game.ServerStorage.Virus.Virus:Clone()
	
	virus.Parent = game.Workspace.Viruses
	virus.CFrame = CFrame.new(Vector3.new(math.random(-border*spawnsmoothness, border*spawnsmoothness)/spawnsmoothness, 0, math.random(-border*spawnsmoothness, border*spawnsmoothness)/spawnsmoothness)) * CFrame.Angles(0, 0, math.rad(-90))
	
	return virus
end

Also, in THE EXACT same script, i’m doing this:

event.OnServerEvent:Connect(function(plr, circle, mousepos, campos, smoothness, _split, _feed)
	if not circle then return end
	if not circle:FindFirstChild("IsAlive") then return end
	
	local speedradius = 1
	local stopradius = .1
	local function getRayPlaneIntercept(rayPoint, planePoint, rayDir, planeNormal)
		return rayPoint + (rayDir * -(rayPoint - planePoint):Dot(planeNormal)/rayDir:Dot(planeNormal))
	end
	local function handleOverlap(unit1, unit2) 
		if (unit1.Position - unit2.Position).Magnitude + overlapnum < unit1.Size.Z/2 + unit2.Size.Z/2 then
			local bgr = (unit1.Size.Z >= unit2.Size.Z) and unit1 or unit2
			local smlr = (unit1.Size.Z >= unit2.Size.Z) and unit2 or unit1
			local overlap = ((unit1.Position - unit2.Position).Magnitude) - ((unit1.Size.Z/2) + (unit2.Size.Z/2))
			
			local unitvector = (smlr.Position - bgr.Position).Unit
			local mid_pos = (overlap > overlapmaxnum) and bgr.Position:Lerp(smlr.Position, .5) or bgr.Position + (unitvector * (bgr.Size.Z/2))
			local bgrpos = mid_pos - ((bgr.Size.Z/2 * unitvector)) -- - (unitvector * (overlapnum * spawnoverlapnum))
			local smlrpos = mid_pos + ((smlr.Size.Z/2 * unitvector)) --  + (unitvector * (overlapnum * spawnoverlapnum))
			
			bgrpos = Vector3.new(math.clamp(bgrpos.X, -border, border), 0, math.clamp(bgrpos.Z, -border, border))
			smlrpos = Vector3.new(math.clamp(smlrpos.X, -border, border), 0, math.clamp(smlrpos.Z, -border, border))
			
			bgr.Position = bgrpos
			smlr.Position = smlrpos
		end
	end
	
	
	local intPosition = getRayPlaneIntercept(campos, Vector3.new(0, height, 0), (mousepos - campos).Unit, Vector3.new(0, 1, 0))
	
	-- move each unit with vel
	for _, v in pairs(circle.Units:GetChildren()) do
		local diff = (intPosition - v.Position)
		local distance = diff.Magnitude
		local clampedunitvector = diff.Unit
		local clampeddistance = math.min(distance / (v.Size.Z/2), speedradius)
		if distance / (v.Size.Z/2) <= stopradius then clampeddistance = 0 end
		local endpos = (clampedunitvector + v._Velocity.Value) * clampeddistance
		local tr_endpos = (endpos * v.Speed.Value) / smoothness
		local x_pol = (v.Position.x >= 0) and 1 or -1 
		local z_pol = (v.Position.Z >= 0) and 1 or -1 
		local x = ((v.Position.X + tr_endpos.X) > border or (v.Position.X + tr_endpos.X) < -border) and ((border * x_pol) - v.Position.X) or tr_endpos.X
		local z = ((v.Position.Z + tr_endpos.Z) > border or (v.Position.Z + tr_endpos.Z) < -border) and ((border * z_pol) - v.Position.Z) or tr_endpos.Z
		local clampedtr_endpos = Vector3.new(x, 0, z) -- clamp y also
		
		v.Position += clampedtr_endpos
		
		-- split and feed
		if _split then
			if v.XP.Value >= minsplitxp and #circle.Units:GetChildren() < maxunitcount then
				split(circle, v, clampedunitvector, false)
			end
		end

		if _feed then
			if v.XP.Value >= minfeedxp then
				feed(v, clampedunitvector)
				v.XP.Value -= feedxp
			end
		end
		
		v._Velocity.Value -= (v._Velocity.Value * slowdownfactor) / smoothness
	end	
	
	-- average out position and set root
	local unitcount = #circle.Units:GetChildren()
	local sumposition = Vector3.new()
	for _, v in pairs(circle.Units:GetChildren()) do
		sumposition += v.Position
	end
	local circlepos = sumposition/unitcount
	circle.RootPosition.Value = circlepos
	
	-- overlap
	local all_table = {}
	for _, v in pairs(circle.Units:GetChildren()) do
		table.insert(all_table, v)
	end
	for _, v in pairs(circle.Units:GetChildren()) do
		table.remove(all_table, table.find(all_table, v))
		
		for _, w in pairs(all_table) do
			if v:FindFirstChild("CanMerge") and w:FindFirstChild("CanMerge") then
				if (not v.CanMerge.Value) or (not w.CanMerge.Value) then
					handleOverlap(w, v)
				else
					if isColliding(w, v) or isColliding(v, w) then
						if w.Size.Z >= v.Size.Z then
							w.XP.Value += v.XP.Value
							v:Destroy()
						else
							v.XP.Value += w.XP.Value
							w:Destroy()
						end
					end
				end
			end
		end
	end
end)

And that’s running SIXTY TIMES PER SECOND for EVERY SINGLE PLAYER IN THE GAME. But there’s no lag. Now, I’m pretty sure there IS lag, but it’s not local, it’s serversided. I recorded the script activity for this script, and it’s 13%. That doesn’t sound good.

Maybe the solution is to separate them into different scripts?
Or maybe just not use coroutines?
Maybe what i’m doing in the first place is just extremely taxxing in general, and there’s no way around it?

Someone, please help me, I’m having trouble with this.

Look into using parallel luau

BUT, try optimizing your code first. For example:

There are redundancies here. Do you see them? You’re making the code do complex math more than one time when it can be stored and reused. I see this quite often in your script, especially with roblox functions. You’re doing circle.Units:GetChildren() way too many times when you should’ve only done it once. It can be a very expensive operation if there’s a lot of children to find.
image

local circleUnits = circle.Units:GetChildren() --reuse this!!!!!

for _, v in circleUnits do ... --using generalized iteration here, does not need pairs()

This kind of optimization of storing and reusing data is called common subexpression elimination, and you should look into it.

Another nitpick:

Consider using a hashmap for this purpose. It is way more efficient at recording
and managing specific data. It would look something like this:

local all_table = {}
for _, v in circle.Units:GetChildren() do
    all_table[v] = true
end
for _, v in circle.Units:GetChildren() do
    all_table[v] = nil

    for w in all_table do...

Second nitpick:

Consider using attributes or tags (CollectionService) instead to mark instances with a unique property. They should be faster because they aren’t additional child instances.

2 Likes

You’re right, I should use that instead of actual values, also, for the pairs thing, I’ve never really known what pairs does, I’ve always just used it for looping through tables. Thanks.

Could you reiterate on the ‘hashmap’?

I believe pairs essentially just ‘pairs’ up the key and the value to a dictionary or array. I don’t think you don’t need to do this for normal tables though.

Remember: use pairs for dictionaries, and ipairs for arrays

Could you explain what a hashmap is? I’ve heard it in C#, but I don’t know what the equivalent is in lua.

Let’s go over the basics first. Here’s how tables in Lua are structured:

table = {key = value} --if key is a string such that the same string can be used as a variable name
table = {[key] = value} --the key can be any datatype, even functions or other tables
table.key = value --if key is a string
table[key] = value --if key is anything else
for key, value in table do... --key comes first, then the value

An array is formed when there’s a sequence of values such that the numerical keys can be organized into a continuous list.

--this entire thing is an array
1 = 1,
2 = 2,
3 = 3

--this entire thing is also an array
2 = 2,
1 = 1,
3 = 3,

--the only array is {1 = 1} because there's a gap between it and the next value
1 = 1,
3 = 3,
4 = 4,

Tables in Lua are both an array and a dictionary. You can mix them together, which is why there are pairs and ipairs; the latter will only iterate the array part of the table while pairs will iterate through everything. Note that pairs is not guaranteed to iterate through arrays in chronological order. Generalized iteration, however, will iterate the array portion first and in the correct order while still iterating through the entire table.

mixedTable = {1, 2, 3, nil, 5, apple = true, banana = false}
for _, v in pairs(mixedTable) do print(v) end --> true 1 false 5 2 3 (can be in any order)
for _, v in ipairs(mixedTable) do print(v) end --> 1 2 3 (only the array is iterated)
for _, v in mixedTable do print(v) end --> 1 2 3 5 true false (array is in order and everything else is still iterated)

Now, a hashmap (aka a hash table or a lookup table) is just a dictionary used in a way such that the key is the actual value, and the value is just a filler to occupy the key. The filler is usually the boolean true.

hash = {
	['hello world'] = true,
	[Vector3.zero] = true,
	[workspace.Part] = true,
	[function() print('hi') end] = true,
}

for v in hash do print(v) end

--[[
	Output:

	0, 0, 0
	hello world
	function: 0x5a6559b013741b3b 
	Part
]]

A common use case of hashmaps would be to record a list of whitelisted players. Example:

--the key is the UserId of admins
local admin: {[number]: true} = {
	[1] = true, --builderman is admin
	[732785] = true,
	[462420] = true,
	[339310190] = true,
}

game:GetService('Players').PlayerAdded:Connect(function(p)
	if admin[p.UserId] then --check to see if the player's UserId is in the table
		print(p.Name..' is an admin')
		giveAdminPowers(p) --if it is, give them administrative powers
	end
end)

And, for comparison, here’s the same code but with regular arrays:

--the value is the UserId of admins
local admin: {number} = {
	1,
	732785,
	462420,
	339310190,
}

game:GetService('Players').PlayerAdded:Connect(function(p)
	if table.find(admin, p.UserId) then --check to see if the player's UserId is in the table
		print(p.Name..' is an admin')
		giveAdminPowers(p) --if it is, give them administrative powers
	end
end)

The one that uses array might look simpler, but it is in reality much slower than using hashmaps. Why? Because it has to look through the entire array and check each value to see if it matches the UserId. Hashmaps simply just encode the value into a hash (that’s where the name came from) and see if it has an occupant in the table.

From Wikipedia:

In many situations, hash tables turn out to be on average more efficient than search trees or any other table lookup structure. For this reason, they are widely used in many kinds of computer software, particularly for associative arrays, database indexing, caches, and sets.

If you want to learn more: Hash table - Wikipedia

Fun fact, sets in Python were originally just dictionaries using hashmaps!

3 Likes

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