Is there any other way to optimize this code? Been stuck on it for awhile

IM trying to achieve a better optimized replication of the code, currently it iterates through cached descendants before changing their colors through lerping constantly.

My main issues regarding how quickly it iterates is the performance drops constantly. My game is focused on visuals, mainly color swapping. Overall, I have not been able to find any resources on helping its performance and just need help or tips on how I can make the script less laggy. (144fps drops to around 60-50, this is horrible)

-- this is the cached function which saves objects into a table, then returns the objects used in color changing:
local function CacheDescendants(target, Name)

	local cachedTrails = {}
	local cachedParticles = {}
	local cachedBillboards = {}
	local cachedNeonParts = {}
	local cachedPointLights = {}

	for _, desc in pairs(target:GetDescendants()) do
		local isBlack = nil

		--Check if the descendant is black based on its type
		if desc:IsA("BasePart") then
			isBlack = desc.Name == "Ignore"
		elseif desc:IsA("ParticleEmitter") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		elseif desc:IsA("Trail") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		elseif desc:IsA("Beam") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		end

		-- Only add to tables if not black or named ignored?
		if not isBlack then
			if desc:IsA("Trail") then
				table.insert(cachedTrails, desc)
			elseif desc:IsA("ParticleEmitter") or desc:IsA("Beam") then
				table.insert(cachedParticles, desc)
			elseif desc:IsA("BillboardGui") then
				table.insert(cachedBillboards, desc)
			elseif (Name ~= "Desolation" and Name ~= "Nihilism") and (desc:IsA("BasePart") and desc.Material == Enum.Material.Neon or desc:IsA("PointLight")) then
				table.insert(cachedNeonParts, desc)
			elseif desc:IsA("PointLight") then
				table.insert(cachedPointLights, desc)
			end
		end
	end

	return cachedTrails, cachedParticles, cachedBillboards, cachedNeonParts, cachedPointLights
end

--the connection for color creation/handling:
Local updateConnection 
updateConnection = runService.Heartbeat:Connect(function(dt)

--target is what holds those color changing objects,particles, etc
	local cachedTrails, cachedParticles, cachedBillboards, cachedNeonParts,cachedPointLights = CacheDescendants(target, Name)	


--this funciton handles how the colors are arranged/changed constantly
local function UpdateColorsAndParticles(newSpeed, minSpeed, maxSpeed, sensitivity, Color31, Color32, overrideColor, FlashColor, flashCounter)
			-- Pre-calculate values
			local speedRatio = math.clamp((newSpeed - minSpeed) / (maxSpeed - minSpeed), 0, 1)
			local colorLerp = math.abs(math.sin(currentTick * sensitivity))
			local loudness = music.PlaybackLoudness

			-- Calculate interpolated color
			local color = Color3.new(
				(Color32.R - Color31.R) * colorLerp + Color31.R,
				(Color32.G - Color31.G) * colorLerp + Color31.G,
				(Color32.B - Color31.B) * colorLerp + Color31.B
			)

			-- Apply override or flash colors
			if overrideColor then
				color = overrideColor
			elseif FlashColor and flashCounter > 0 then
				color = FlashColor
				flashCounter = flashCounter - 1
			end

			--  ColorSequence to avoid redundant creation
			local colorSequence = ColorSequence.new(color)

			-- Update objects in ao loop
			local function updateObject(obj)
				if obj:IsA("ParticleEmitter") or obj:IsA("Trail") or obj:IsA("Beam") then
					if obj.Color ~= colorSequence then
						obj.Color = colorSequence
					end
					if obj:IsA("ParticleEmitter") and obj:FindFirstChild("PulseOnBeat") then
						local pulseValue = loudness / (obj:FindFirstChild("PulseOnBeat").Value or 100)
						obj.Size = NumberSequence.new(pulseValue)
					end
				elseif obj:IsA("BillboardGui") then
					local textLabel = obj:FindFirstChildWhichIsA("TextLabel")
					if textLabel and textLabel.TextColor3 ~= color then
						textLabel.TextColor3 = color
					end
				elseif obj:IsA("BasePart") then
					if obj.Color ~= color then
						obj.Color = color
					end
				elseif obj:IsA("PointLight") then
					if obj.Color ~= color then
						obj.Color = color
					end
				end
			end

			-- Loop through all objectss
			for _, obj in ipairs(cachedTrails) do updateObject(obj) end
			for _, obj in ipairs(cachedParticles) do updateObject(obj) end
			for _, obj in ipairs(cachedBillboards) do updateObject(obj) end
			for _, obj in ipairs(cachedNeonParts) do updateObject(obj) end
			for _, obj in ipairs(cachedPointLights) do updateObject(obj) end
		end

		UpdateColorsAndParticles(newSpeed, minSpeed, maxSpeed, sensitivity, Color31, Color32, overrideColor, FlashColor, flashCounter)
			
	

	end)

Overall, I’m starting to think it might be impossible to optimize or do…

1 Like

It might be the formatting but it looks like the updateColorsAndParticles function calls itself recursively inside a heartbeat connection…

The heartbeat would call it each frame anyway so the recursion is unnecessary. Try removing this and storing the parameters as variables outside the connection.

2 Likes

Move the variables outside it, still no performance increase that’s major. Did not know why I never saw that though. As for everything else, its still heavy

thing I’ve tried so far was reworking the recursion with delays but it only resulted in a slower color changing effect with not much increase in performance.

2 Likes

I had a play around with the script a little, this works for me, but I only added a few things to test it.

--Place script in part, add music, particles etc
--!strict
local runService = game:GetService("RunService")

local target = script.Parent
local cache = {} ::{any}

local music :Sound = target.Energy

local colorSequence:ColorSequence, speedRatio:number, colorLerp:number, loudness:number, color:Color3, currentTick:number
local newSpeed:number, minSpeed:number, maxSpeed:number, sensitivity:number, Color31:Color3, Color32:Color3, overrideColor:Color3, FlashColor:Color3, flashCounter:number

local function ShouldCache(desc:Instance, Name:string)
	local isBlack = nil

	--Check if the descendant is black based on its type
	if desc:IsA("BasePart") then
		isBlack = desc.Name == "Ignore"
	elseif desc:IsA("ParticleEmitter") then
		isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
	elseif desc:IsA("Trail") then
		isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
	elseif desc:IsA("Beam") then
		isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
	end

	-- Only add to tables if not black or named ignored?
	if not isBlack then
		if desc:IsA("Trail") then
			table.insert(cache, desc)
		elseif desc:IsA("ParticleEmitter") or desc:IsA("Beam") then
			table.insert(cache, desc)
		elseif desc:IsA("BillboardGui") then
			table.insert(cache, desc)
		elseif (Name ~= "Desolation" and Name ~= "Nihilism") and (desc:IsA("BasePart") and desc.Material == Enum.Material.Neon or desc:IsA("PointLight")) then
			table.insert(cache, desc)
		elseif desc:IsA("PointLight") then
			table.insert(cache, desc)
		end
	end
end


local function CacheDescendants(target:Instance, Name:string)
	--empty the cache if target changes
	table.clear(cache)
	--create cache
	for _, desc in pairs(target:GetDescendants()) do
		ShouldCache(desc, Name)
	end
end

-- Update objects in ao loop
local function updateObject(obj)
	if obj:IsA("ParticleEmitter") or obj:IsA("Trail") or obj:IsA("Beam") then
		if obj.Color ~= colorSequence then
			obj.Color = colorSequence
		end
		if obj:IsA("ParticleEmitter") and obj:FindFirstChild("PulseOnBeat") then
			local pulseValue = loudness / (obj:FindFirstChild("PulseOnBeat").Value or 100)
			obj.Size = NumberSequence.new(pulseValue)
		end
	elseif obj:IsA("BillboardGui") then
		local textLabel = obj:FindFirstChildWhichIsA("TextLabel")
		if textLabel and textLabel.TextColor3 ~= color then
			textLabel.TextColor3 = color
		end
	elseif obj:IsA("BasePart") then
		if obj.Color3 ~= color then
			obj.Color3 = color
		end
	elseif obj:IsA("PointLight") then
		if obj.Color ~= color then
			obj.Color = color
		end
	end
end

--this funciton handles how the colors are arranged/changed constantly
local function UpdateColorsAndParticles()
	if not newSpeed or not minSpeed or not maxSpeed or not currentTick or not sensitivity or not music or not Color31 or not Color32 then return end
	
	-- Pre-calculate values
	speedRatio = math.clamp((newSpeed - minSpeed) / (maxSpeed - minSpeed), 0, 1)
	colorLerp = math.abs(math.sin(currentTick * sensitivity))
	loudness = music.PlaybackLoudness

	-- Calculate interpolated color
	color = Color3.new(
		(Color32.R - Color31.R) * colorLerp + Color31.R,
		(Color32.G - Color31.G) * colorLerp + Color31.G,
		(Color32.B - Color31.B) * colorLerp + Color31.B
	)

	-- Apply override or flash colors
	if overrideColor then
		color = overrideColor
	elseif FlashColor and flashCounter > 0 then
		color = FlashColor
		flashCounter = flashCounter - 1
	end

	--  Set ColorSequence
	colorSequence = ColorSequence.new(color)

	-- Loop through all objects
	for _, obj in cache do updateObject(obj) end
end

--New function added to change values
local function assignValues(NewSpeed:number, MinSpeed:number, MaxSpeed:number, Sensitivity:number, color31:Color3, color32:Color3, OverrideColor:Color3, flashColor:Color3, FlashCounter:number)
	newSpeed = NewSpeed
	minSpeed = MinSpeed
	maxSpeed = MaxSpeed
	sensitivity = Sensitivity
	Color31 = color31
	Color32 = color32
	overrideColor = OverrideColor
	FlashColor = flashColor
	flashCounter = FlashCounter
end


--the connection for color creation/handling:
local updateConnection 
updateConnection = runService.Heartbeat:Connect(function(dt)
	currentTick = tick()
	UpdateColorsAndParticles()
end)
--Function calls to run
CacheDescendants(target, target.Name)
music:Play()
assignValues(3,1,5,2,Color3.new(0.160784, 0.101961, 1), Color3.new(1, 0, 0), nil, Color3.new(1, 1, 0), 120)
1 Like

ive added those edits, and some of my own. Of course, will testing and some other attempts at performance increases. So far, the only real difference was made when I turned the base part coloration into a comment, which did increase performance by a lot. So this leads me to believe that changing any base parts color somehow with another method could increase the FPS,

local runService = game:GetService("RunService")

function module:DualFlash(target, music, Name, Color31, Color32, FlashColor)
	local loudness = music.PlaybackLoudness
	local sensitivity = 3
	local minSpeed = 0.1
	local maxSpeed = 1
	local newSpeed = loudness / sensitivity
	local flashCounter = 0
	local currentTick = tick()
	local overrideColor = nil
	local colorSequence = nil
	local color = nil

	local cache = {}

	local function ShouldCache(desc)
		local isBlack = nil

		if desc:IsA("BasePart") then
			isBlack = desc.Name == "Ignore"
		elseif desc:IsA("ParticleEmitter") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		elseif desc:IsA("Trail") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		elseif desc:IsA("Beam") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		end

		if not isBlack then
			table.insert(cache, desc)
		end
	end

	local function CacheDescendants()
		table.clear(cache)
		for _, desc in pairs(target:GetDescendants()) do
			ShouldCache(desc)
		end
	end

	local function updateObject(obj)
		if obj:IsA("ParticleEmitter") or obj:IsA("Trail") or obj:IsA("Beam") then
			if obj.Color ~= colorSequence then
				obj.Color = colorSequence
			end
			if obj:IsA("ParticleEmitter") and obj:FindFirstChild("PulseOnBeat") then
				local pulseValue = loudness / (obj:FindFirstChild("PulseOnBeat").Value or 100)
				obj.Size = NumberSequence.new(pulseValue) --this also impacts performance from what ive seen 
			end
		elseif obj:IsA("BillboardGui") then
			local textLabel = obj:FindFirstChildWhichIsA("TextLabel")
			if textLabel and textLabel.TextColor3 ~= color then
				textLabel.TextColor3 = color
			end
		elseif obj:IsA("BasePart") then
			if obj.Color ~= color then
				--obj.Color = color THE COMMENT HERE MADE THE DIFFERENCE!
			end
		elseif obj:IsA("PointLight") then
			if obj.Color ~= color then
				obj.Color = color
			end
		end
	end
	local updateConnection 
	local function UpdateColorsAndParticles()
		
		if not  music.IsPlaying then
			updateConnection:Disconnect()
			warn("DISCONNECTING THE MUSIC")
			return
		end
		
		local speedRatio = math.clamp((newSpeed - minSpeed) / (maxSpeed - minSpeed), 0, 1)
		local colorLerp = math.abs(math.sin(currentTick * sensitivity))
		loudness = music.PlaybackLoudness

		color = Color3.new(
			(Color32.R - Color31.R) * colorLerp + Color31.R,
			(Color32.G - Color31.G) * colorLerp + Color31.G,
			(Color32.B - Color31.B) * colorLerp + Color31.B
		)

		if overrideColor then
			color = overrideColor
		elseif FlashColor and flashCounter > 0 then
			color = FlashColor
			flashCounter = flashCounter - 1
		end

		colorSequence = ColorSequence.new(color)

		for _, obj in cache do
			updateObject(obj)
		end
	end

	CacheDescendants()

	 updateConnection = runService.Heartbeat:Connect(function()
		currentTick = tick()
		UpdateColorsAndParticles()
		
	end)
	

end

This seems to be about as optimized as is reasonably feasible. There are very minor things maybe, but no huge time saves. Looping through a ton of descendants every heartbeat is just going to be costly. The only real solution is to perform less work. So first, this is of course the minimum amount of objects you need to apply this on right?

Secondly does this fade? As it looks now this runs for everything ever added forever. Even when those items get destroyed since there is a reference to them this code won’t stop. I recommend using collection service and just tagging everything instead of putting them in a table so that you don’t have to worry about them leaving.

Thirdly you may want to simply impose a hard limit to how many things you can process at once. Or at least make further away items get processed less frequently.

The best optimization without changing what you are processing I could give is to basically replace those if statements with tables of functions and directly handle the code that way to skip if statements. But calling that an optimization is a bit incorrect. It would run faster at scale, but with the handful of different types you have, it’s probably negligible or detrimental to performance to do that.

It fades with some lerps, essentially it creates a sort of flashing in-out with the colors assigned,

local runService = game:GetService("RunService")


function module:DualFlash(target, music, Name, Color31, Color32, FlashColor)
	local loudness = music.PlaybackLoudness
	local sensitivity = 3
	local minSpeed = 0.1
	local maxSpeed = 1
	local newSpeed = loudness / sensitivity
	local flashCounter = 0
	local currentTick = tick()
	local overrideColor = nil
	local colorSequence = nil
	local color = nil

	local cache = {}
	local pulseCache = {} -- Cache for PulseOnBeat values

	local function ShouldCache(desc)
		local isBlack = nil

		if desc:IsA("BasePart") then
			isBlack = desc.Name == "Ignore"
		elseif desc:IsA("ParticleEmitter") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		elseif desc:IsA("Trail") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		elseif desc:IsA("Beam") then
			isBlack = desc.Color.Keypoints[1].Value == Color3.new(0, 0, 0)
		end

		if not isBlack then
			table.insert(cache, desc)
			if desc:IsA("ParticleEmitter") and desc:FindFirstChild("PulseOnBeat") then
				pulseCache[desc] = desc:FindFirstChild("PulseOnBeat").Value or 100 -- Cache PulseOnBeat value
			end
		end
	end

	local function CacheDescendants()
		table.clear(cache)
		table.clear(pulseCache)
		for _, desc in pairs(target:GetDescendants()) do
			ShouldCache(desc)
		end
	end

	local function updateObject(obj)
		if obj:IsA("ParticleEmitter") or obj:IsA("Trail") or obj:IsA("Beam") then
			if obj.Color ~= colorSequence then
				obj.Color = colorSequence
			end
			if obj:IsA("ParticleEmitter") and pulseCache[obj] then
				local pulseValue = loudness / pulseCache[obj]
				obj.Size = NumberSequence.new(pulseValue) -- Use cached PulseOnBeat value
			end
		elseif obj:IsA("BillboardGui") then
			local textLabel = obj:FindFirstChildWhichIsA("TextLabel")
			if textLabel and textLabel.TextColor3 ~= color then
				textLabel.TextColor3 = color
			end
		elseif obj:IsA("BasePart") then
			if obj.Color ~= color then
				-- Smoothly transition color using TweenService
				--local tweenService = game:GetService("TweenService")
				--local tweenInfo = TweenInfo.new(0.1, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut)
				---local goal = {Color = color}
				--tweenService:Create(obj, tweenInfo, goal):Play()
			end
		elseif obj:IsA("PointLight") then
			if obj.Color ~= color then
				obj.Color = color
			end
		end
	end
	local updateConnection
	local function UpdateColorsAndParticles()
		if  not music.IsPlaying then
			warn("disconnectin")
			updateConnection:Disconnect()
			return
		end

		local speedRatio = math.clamp((newSpeed - minSpeed) / (maxSpeed - minSpeed), 0, 1)
		local colorLerp = math.abs(math.sin(currentTick * sensitivity))
		loudness = music.PlaybackLoudness

		color = Color3.new(
			(Color32.R - Color31.R) * colorLerp + Color31.R,
			(Color32.G - Color31.G) * colorLerp + Color31.G,
			(Color32.B - Color31.B) * colorLerp + Color31.B
		)

		if overrideColor then
			color = overrideColor
		elseif FlashColor and flashCounter > 0 then
			color = FlashColor
			flashCounter = flashCounter - 1
		end

		colorSequence = ColorSequence.new(color)

		for _, obj in cache do
			updateObject(obj)
		end
	end

	CacheDescendants()

	 updateConnection = runService.Heartbeat:Connect(function()
		currentTick = tick()
		UpdateColorsAndParticles()
	end)

	
end

i added some other things to my script (and help) that ignores the parts in terms of recoloring them frequently. I understand that there could be alternatives, yet just haven’t found them yet in terms of workload regarding the effects.

Here is a video of this all in action, the games inspired by star glitcher FE scripts, just made it my own way.

This can absolutely be optimized. Not at pc so won’t write out code but generally this would be what you can do…

For the first part of the code and some of the second function, make a table that has the descriptions as keys then just set the value to the value element of the key of the table. This reduces time complexity. Hope you see what I mean.

Generally speaking, not really? By descriptions just the color3 value? then as for every update, set the color3 value for the assigned object to change to the color?

I get some understanding from it, but I think I know what you mean by that.

i think he means that for each part of the script where you have the if else if walls

you can instead store the object type and then create a dictionary where the keys (object types) are associated with functions that handle those object types, like emulating a switch statement

so you could do something like

objectHandlerFunctions[cachedObject.objectType]()

instead of a wall of

if cachedObject:IsA()

edit1: you could store color and loudness/pulseCache[obj] in more caches

edit 2: the sin function is also pretty slow so maybe you can make a lookup table for that too and then use modulo on the argument since sin is a periodical function