How could I optimize this system? Crazy high CPU system

Code:

function Service.LuckyBlockEffect(self: Service, Id: string)
	if not self.Drops[Id] or not self.Drops[Id].DropModel then warn("SOMETHING WENT WRONG DISPLAYING THE LUCKYBLOCK EFFECT: ", self.Drops[Id]) return end
	local Data = self.Drops[Id]
	self.Drops[Id] = nil
	local Origin: CFrame = Data.DropModel:GetPivot()
	local Root: Part = Data.DropModel.PrimaryPart
	local BrainrotName: string = Data.Value
	if not Root or not BrainrotName or not ReplicatedStorage.Assets.Brainrots:FindFirstChild(BrainrotName) then
		Data.DropModel:Destroy()
		return
	end
	
	local LookAt = Origin.Position
	local Character = Player.Character
	local HumanoidRootPart = Character and Character:FindFirstChild("HumanoidRootPart")
	if HumanoidRootPart then LookAt = HumanoidRootPart.Position end
	
	--Helper
	local function ScaleEffect(Model: Model, TargetScale: number, Duration: number, isFadeOut: boolean?)
		local CurrentScale = Model:GetScale()
		local StartScale = isFadeOut and CurrentScale or 0.05 -- avoid 0, some parts/joints don't like true zero scale

		-- If a VfxInstance exists, scale into its center instead of the
		-- model's pivot. Captured before the initial ScaleTo so this is
		-- the VfxInstance's true resting position, not its shrunk one.
		local VfxInstance = Model:FindFirstChild("VfxInstance") :: BasePart?
		local Origin, AnchorPosition, LocalOffset
		if VfxInstance then
			Origin = Model:GetPivot()
			AnchorPosition = VfxInstance.Position
			LocalOffset = Origin:PointToObjectSpace(AnchorPosition) / CurrentScale
		end

		local function ApplyScale(Scale: number)
			Model:ScaleTo(Scale)
			if VfxInstance then
				local NewPosition = AnchorPosition - Origin:VectorToWorldSpace(LocalOffset * Scale)
				Model:PivotTo(CFrame.new(NewPosition) * Origin.Rotation)
			end
		end

		ApplyScale(StartScale)
		local ScaleValue = Instance.new("NumberValue")
		ScaleValue.Value = StartScale
		local Connection
		Connection = ScaleValue.Changed:Connect(ApplyScale)

		local Tween = TweenService:Create(ScaleValue, TweenInfo.new(Duration, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Value = TargetScale })
		Tween:Play()

		for _, v in Model:GetDescendants() do
			if v:IsA("BasePart") or v:IsA("Decal") or v:IsA("Texture") then
				TweenService:Create(v, TweenInfo.new(1,Enum.EasingStyle.Quart,Enum.EasingDirection.Out,0,false,0.1), { Transparency = 1 }):Play()
			end
		end

		task.wait(Duration) -- yields until the tween finishes, no Completed connection needed

		Connection:Disconnect()
		ScaleValue:Destroy()
	end
	
	local function PlayDetachedSound(SourceSound: Sound, Parent: Instance)
		local Sound = SourceSound:Clone()
		Sound.Parent = Parent
		Sound:Play()

		if Sound.IsLoaded then
			Debris:AddItem(Sound, Sound.TimeLength + 0.1)
		else
			local Conn
			Conn = Sound.Loaded:Connect(function()
				Conn:Disconnect()
				Debris:AddItem(Sound, Sound.TimeLength + 0.1)
			end)
			Debris:AddItem(Sound, 3)
		end
	end
	--
	
	--LuckyBLock
	local function LuckyBlock()
		local vfx = Data.DropModel:FindFirstChild("BrainrotSpin")
		if vfx then
			shared.vfx.emit(vfx)
		end
		TweenService:Create(Root, TweenInfo.new(0.25), { CFrame = Root.CFrame * CFrame.new(0, 1, 0) }):Play()
		task.wait(0.25)
		LuckyBlocksEntries[HttpService:GenerateGUID()] = {
			Part = Root,
			Start = os.clock(),
			Original = Root.CFrame
		}
		task.wait(ShakeDuration)
		ScaleEffect(Data.DropModel,1.2,0.25)
	end
	LuckyBlock()
	Debris:AddItem(Data.DropModel,2)
	--
	
	--Brainrot intermission
	local PooledObjects = {} :: {Model}
	local randomNum = math.random(6,8)
	
	for i = 1,randomNum do
		local BrainrotObject = getPooledObject("Brainrots")
				
		--Create new brainrot if every objecy is busy
		if not BrainrotObject then
			--All busy
			local randomBrainrot = BrainrotDrops:GetChildren()[math.random(1,#BrainrotDrops:GetChildren())]
			if randomBrainrot then randomBrainrot = randomBrainrot:Clone()
				ObjectPool.Brainrot[randomBrainrot] = false
				randomBrainrot.Parent = workspace.Ignore.BrainrotDrops
				BrainrotObject = randomBrainrot
			end
		end
		--
		if BrainrotObject then
			ObjectPool.Brainrot[BrainrotObject] = true -- Mark as busy
			BrainrotObject.Parent = workspace.Ignore.BrainrotDrops
			table.insert(PooledObjects,BrainrotObject)
			
			for _,v in pairs(BrainrotObject:GetDescendants()) do
				if v:IsA("TextLabel") then
					v.TextTransparency = 0
				end
				if v:IsA("UIStroke") then
					v.Transparency = 0
				end
				if v:IsA("BasePart") then
					v.LocalTransparencyModifier = 0
				end
			end
			BrainrotObject:PivotTo(CFrame.lookAt(Origin.Position,Vector3.new(LookAt.X,Origin.Position.Y,LookAt.Z)))
			
			task.wait()
			PlayDetachedSound(ReplicatedStorage.Assets.SFX.ClickSFX, BrainrotObject)
			
			task.wait(0.1)
			
			for _,v in pairs(BrainrotObject:GetDescendants()) do
				if v:IsA("TextLabel") then
					v.TextTransparency = 1
				end
				if v:IsA("UIStroke") then
					v.Transparency = 1
				end
				if v:IsA("BasePart") then
					v.LocalTransparencyModifier = 1
				end
			end
		end
	end
	
	for _,Object in PooledObjects do
		ObjectPool.Brainrot[Object] = false -- Mark as not busy
	end
	--
	
	--Display brainrot
	local Brainrot: Model = ReplicatedStorage.Assets.Brainrots:FindFirstChild(BrainrotName)
	if Brainrot then
		local BrainrotClone = Brainrot:Clone()
		BrainrotClone.Parent = workspace.Ignore.BrainrotDrops
		
		local Overhead = ReplicatedStorage.Assets.BrainrotOverhead_Drop:Clone()
		Overhead.Parent = BrainrotClone
		
		Overhead.BrainrotName.Text = BrainrotName
		--Size and position based off VfxInstance in BrainrotClone
		
		local VfxInstance = BrainrotClone:FindFirstChild("VfxInstance") :: Part?
		if VfxInstance then
			Overhead.Adornee = VfxInstance

			local VfxSize = VfxInstance.Size
			local maxDimension = math.max(VfxSize.X, VfxSize.Y, VfxSize.Z)

			-- Position: since VfxInstance's CFrame is the model's center, go up
			-- to the top of it (half height) plus some padding to clear the head
			local padding = 2
			Overhead.StudsOffset = Vector3.new(0, VfxSize.Y*1.2 + padding, 0)

			-- Scale: grow/shrink the billboard itself relative to model size
			local baseSize = Overhead.Size -- e.g. UDim2.new(4, 0, 1, 0) from the template
			local scaleFactor = math.clamp(maxDimension / 4, 0.5, 3) -- tune divisor/clamp to taste
			Overhead.Size = UDim2.new(
				baseSize.X.Scale * scaleFactor, baseSize.X.Offset,
				baseSize.Y.Scale * scaleFactor, baseSize.Y.Offset
			)
		end
		
		local Primary = BrainrotClone.PrimaryPart
		if Primary then
			BrainrotClone:ScaleTo(BrainrotClone:GetScale()/2)
			BrainrotClone:PivotTo(CFrame.lookAt(Origin.Position, Vector3.new(LookAt.X, Origin.Position.Y, LookAt.Z)))
			
			--Clear all VFX
			for _,v in pairs(BrainrotClone:GetDescendants()) do
				if v:IsA("ParticleEmitter") or v:IsA("Beam") or v:IsA("Trail") then
					v:Destroy()
				end
			end
			
			--Clear Brainrot
			task.wait(0.4)
			Debris:AddItem(BrainrotClone,0.05)
			ScaleEffect(BrainrotClone,0.3,0.05,true)
			--
		end
	end
	
	--
	
end

Problem:

This is causing really high CPU usage in my game.

Video of problem:

2 Likes

I will point out performance eating parts, and I hope you can figure out rest.

Performance issues happen in Braintrot intermission section of the code, the for loop as I observed that you lag when black Brainrots appear.

  • GetChildren() shouldn’t be used sparingly, it does cost a lot cpu cycle and you’re using it twice to get randomBrainrot, often times you don’t need them inside the for loop because they’re static which contents of BrainrotDrops don’t change. Also if this part of the code often gets triggered you might need to improve your getPooledObject function.
  • After the acquisation of brainrotObject, you do set its parent and setting parent is actually expensive and its one of the reasons why most effecient instance pooling functions does store instance in workspace and just set their positions to really far away.
  • As for the code block that sets transparency to 0, I recommend you to always leave those set to 0 by default. As for if statements, this might sound like nitpicking because its not that important but if you want to save some cpu cycles its better to use elseif instead of 3 indepented if statements as there only one instance being checked in the moment and that instance can be one class at time, so its unnessecary work for cpu to check its class 2 more if its already found out its TextLabel.
  • GetDescendants also can be very expensive based on how many descendants brainrot, if one has a lot of parts you should merge the parts into one mesh if you can.
    doesn’t check if its same or not before doing all that ground work.

That’s all from me for now, let me know if its still causing lag.

4 Likes

I decided to change the code so that it only spawns 1 object, which is the actual object.

function Service.LuckyBlockEffect(self: Service, Id: string)
	if not self.Drops[Id] or not self.Drops[Id].DropModel then warn("SOMETHING WENT WRONG DISPLAYING THE LUCKYBLOCK EFFECT: ", self.Drops[Id]) return end
	local Data = self.Drops[Id]
	self.Drops[Id] = nil
	local Origin: CFrame = Data.DropModel:GetPivot()
	local Root: Part = Data.DropModel.PrimaryPart
	local BrainrotName: string = Data.Value
	if not Root or not BrainrotName or not ReplicatedStorage.Assets.Brainrots:FindFirstChild(BrainrotName) then
		Data.DropModel:Destroy()
		return
	end
	
	local LookAt = Origin.Position
	local Character = Player.Character
	local HumanoidRootPart = Character and Character:FindFirstChild("HumanoidRootPart")
	if HumanoidRootPart then LookAt = HumanoidRootPart.Position end
	
	--Helper
	local function ScaleEffect(Model: Model, TargetScale: number, Duration: number, isFadeOut: boolean?)
		local CurrentScale = Model:GetScale()
		local StartScale = isFadeOut and CurrentScale or 0.05
		
		local VfxInstance = Model:FindFirstChild("VfxInstance") :: BasePart?
		local Origin, AnchorPosition, LocalOffset
		if VfxInstance then
			Origin = Model:GetPivot()
			AnchorPosition = VfxInstance.Position
			LocalOffset = Origin:PointToObjectSpace(AnchorPosition) / CurrentScale
		end
		
		local function ApplyScale(Scale: number)
			Model:ScaleTo(Scale)
			if VfxInstance then
				local NewPosition = AnchorPosition - Origin:VectorToWorldSpace(LocalOffset * Scale)
				Model:PivotTo(CFrame.new(NewPosition) * Origin.Rotation)
			end
		end
		
		ApplyScale(StartScale)
		local ScaleValue = Instance.new("NumberValue")
		ScaleValue.Value = StartScale
		local Connection
		Connection = ScaleValue.Changed:Connect(ApplyScale)
		
		local Tween = TweenService:Create(ScaleValue, TweenInfo.new(Duration, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Value = TargetScale })
		Tween:Play()
		
		for _, v in Model:GetDescendants() do
			if v:IsA("BasePart") or v:IsA("Decal") or v:IsA("Texture") then
				TweenService:Create(v, TweenInfo.new(1,Enum.EasingStyle.Quart,Enum.EasingDirection.Out,0,false,0.1), { Transparency = 1 }):Play()
			end
		end
		
		task.wait(Duration) -- yields until the tween finishes, no Completed connection needed
		
		Connection:Disconnect()
		ScaleValue:Destroy()
	end
	
	--LuckyBLock
	local function LuckyBlock()
		local vfx = Data.DropModel:FindFirstChild("BrainrotSpin")
		if vfx then
			shared.vfx.emit(vfx)
		end
		TweenService:Create(Root, TweenInfo.new(0.25), { CFrame = Root.CFrame * CFrame.new(0, 1, 0) }):Play()
		task.wait(0.25)
		LuckyBlocksEntries[HttpService:GenerateGUID()] = {
			Part = Root,
			Start = os.clock(),
			Original = Root.CFrame
		}
		task.wait(ShakeDuration)
		ScaleEffect(Data.DropModel,1.2,0.25)
	end
	LuckyBlock()
	Debris:AddItem(Data.DropModel,2)
	--
	
	--Display brainrot
	local Brainrot: Model = BrainrotDrops:FindFirstChild(BrainrotName)
	if Brainrot then
		local BrainrotClone = Brainrot:Clone()
		BrainrotClone.Parent = workspace.Ignore.BrainrotDrops
		
		local Overhead = ReplicatedStorage.Assets.BrainrotOverhead_Drop:Clone()
		Overhead.BrainrotName.Text = BrainrotName
		Overhead.Parent = BrainrotClone
		
		local VfxInstance = BrainrotClone:FindFirstChild("VfxInstance") :: Part?
		if VfxInstance then
			Overhead.Adornee = VfxInstance
			
			local VfxSize = VfxInstance.Size
			local maxDimension = math.max(VfxSize.X, VfxSize.Y, VfxSize.Z)
			
			local padding = 2
			Overhead.StudsOffset = Vector3.new(0, VfxSize.Y*1.2 + padding, 0)
			
			local baseSize = Overhead.Size
			local scaleFactor = math.clamp(maxDimension / 4, 0.5, 3)
			Overhead.Size = UDim2.new(
				baseSize.X.Scale * scaleFactor, baseSize.X.Offset,
				baseSize.Y.Scale * scaleFactor, baseSize.Y.Offset
			)
		end
		
		local Primary = BrainrotClone.PrimaryPart
		if Primary then
			BrainrotClone:ScaleTo(BrainrotClone:GetScale()/2)
			BrainrotClone:PivotTo(CFrame.lookAt(Origin.Position, Vector3.new(LookAt.X, Origin.Position.Y, LookAt.Z)))
			
			--Clear Brainrot
			local succ = self:ClearBrainrot(BrainrotClone)
			if not succ then
				BrainrotClone:Destroy()
			end
			--
		end
	end
	
end

Although this works 100x better (I also added the optimization you suggested).
The CPU still does go up seemingly randomly when the brainrot spawns, it’s not everytime now and not even close to as bad, but it’s still present, is there more optimization I could add?

how many parts do your models have? You are creating a lot of tweens if the part count is relatively high. When you tween the models to be transparency one if a model has say 50 parts and you spawn say 10 that is 500 tweens running at once. Tweens aren’t terribly unperformant but still this could be a source.

The main issue I think is the apply scale function. Both resizing and repositioning parts is pretty costly. Again say 50 parts in a model, you have a function bound every frame that scales and positions all of those parts. You are basically fully recalculating the geometry, position, and hitbox for every part in both the lucky blocks and the brainrots every frame. Disabling cancollide, CanTouch, shadows, setting RenderFidelity to performance, and setting CollisionFidelity to box all will help but you inherently cannot get around rerendering the parts every frame so depending on how much meshes/parts you use it’ll be a problem.

Either try simplifying the models or try updating less frequently. Perhaps instead of tweening the scale number values you can lerp them every other renderstepped. Going from 60 FPS to 30 FPS realistically isn’t noticeable and already is 50% save on usage.

Use ScaleTo() instead of tweening each part.

Wait for models to load, to prevent bug that occurs with ScaleTo(). (this bug might be patched by roblox so idk)

The models are not very complex


Although some can be. Problem is that I didn’t make the models, my partner bought them so redoing each one isn’t an option, moreover, you said the stuff about tweening, collisionfidelity and properities like CanCollide and castShadow, I’ve made a newer version with all that off

function Service.LuckyBlockEffect(self: Service, Id: string)
	if not self.Drops[Id] or not self.Drops[Id].DropModel then warn("SOMETHING WENT WRONG DISPLAYING THE LUCKYBLOCK EFFECT: ", self.Drops[Id]) return end
	local Data = self.Drops[Id]
	self.Drops[Id] = nil
	local Origin: CFrame = Data.DropModel:GetPivot()
	local Root: Part = Data.DropModel.PrimaryPart
	local BrainrotName: string = Data.Value
	if not Root or not BrainrotName or not ReplicatedStorage.Assets.Brainrots:FindFirstChild(BrainrotName) then
		Data.DropModel:Destroy()
		return
	end
	
	local LookAt = Origin.Position
	local Character = Player.Character
	local HumanoidRootPart = Character and Character:FindFirstChild("HumanoidRootPart")
	if HumanoidRootPart then LookAt = HumanoidRootPart.Position end
	
	--Helper
	local function ScaleEffect(Model: Model, TargetScale: number, Duration: number, isFadeOut: boolean?)
		local CurrentScale = Model:GetScale()
		local StartScale = isFadeOut and CurrentScale or 0.05
		
		local VfxInstance = Model:FindFirstChild("VfxInstance") :: BasePart?
		local Origin, AnchorPosition, LocalOffset
		if VfxInstance then
			Origin = Model:GetPivot()
			AnchorPosition = VfxInstance.Position
			LocalOffset = Origin:PointToObjectSpace(AnchorPosition) / CurrentScale
		end
		
		local function ApplyScale(Scale: number)
			Model:ScaleTo(Scale)
			if VfxInstance then
				local NewPosition = AnchorPosition - Origin:VectorToWorldSpace(LocalOffset * Scale)
				Model:PivotTo(CFrame.new(NewPosition) * Origin.Rotation)
			end
		end
		
		ApplyScale(StartScale)
		local ScaleValue = Instance.new("NumberValue")
		ScaleValue.Value = StartScale
		local Connection
		Connection = ScaleValue.Changed:Connect(ApplyScale)
		
		local Tween = TweenService:Create(ScaleValue, TweenInfo.new(Duration, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Value = TargetScale })
		Tween:Play()
		
		for _, v in Model:GetDescendants() do
			if v:IsA("BasePart") or v:IsA("Decal") or v:IsA("Texture") then
				TweenService:Create(v, TweenInfo.new(1,Enum.EasingStyle.Quart,Enum.EasingDirection.Out,0,false,0.1), { Transparency = 1 }):Play()
			end
		end
		
		task.wait(Duration) -- yields until the tween finishes, no Completed connection needed
		
		Connection:Disconnect()
		ScaleValue:Destroy()
	end
	
	--LuckyBLock
	local function LuckyBlock()
		local vfx = Data.DropModel:FindFirstChild("BrainrotSpin")
		if vfx then
			shared.vfx.emit(vfx)
		end
		TweenService:Create(Root, TweenInfo.new(0.25), { CFrame = Root.CFrame * CFrame.new(0, 1, 0) }):Play()
		task.wait(0.25)
		LuckyBlocksEntries[HttpService:GenerateGUID()] = {
			Part = Root,
			Start = os.clock(),
			Original = Root.CFrame
		}
		task.wait(ShakeDuration)
		--ScaleEffect(Data.DropModel,1.2,0.25)
	end
	--LuckyBlock()
	Debris:AddItem(Data.DropModel,2)
	--
	
	--Display brainrot
	local Brainrot: Model = BrainrotDrops:FindFirstChild(BrainrotName)
	if Brainrot then
		local BrainrotClone = Brainrot:Clone()
		BrainrotClone.Parent = workspace.Ignore.BrainrotDrops
		
		local Primary = BrainrotClone.PrimaryPart
		if Primary then
			BrainrotClone:PivotTo(CFrame.lookAt(Origin.Position, Vector3.new(LookAt.X, Origin.Position.Y, LookAt.Z)))
			
			--Clear Brainrot
			local succ = self:ClearBrainrot(BrainrotClone)
			if not succ then
				BrainrotClone:Destroy()
			end
			--
		end
	end
	
end
function Service.ClearBrainrot(self: Service, Brainrot: Model): boolean?
	local Primary = Brainrot.PrimaryPart
	if not Primary then return end
	
	local Character = Player.Character
	local HumanoidRootPart = Character and Character:FindFirstChild("HumanoidRootPart") :: Part?
	if HumanoidRootPart then
		local StartPosition = Primary.Position
		local MidPosition = (StartPosition + HumanoidRootPart.Position) / 2 + Vector3.new(0, 5, 0)
		
		self.ClearingBrainrots[Brainrot] = {
			StartPosition = StartPosition,
			MidPosition = MidPosition,
			StartTime = os.clock(),
			Elapsed = 0
		} -- This is constantly being iterated in a Heartbeat connection for positioning only
		return true
	end
	return
end

With this I have basically removed all :Clone() besides when creating the object.
The way I did that is by setting up the objects before hand, by adding all the stuff like trails and overheads to the objects in studio so that they are there when the game starts, if that makes sense. Lmk if this is a bad approach or if you don’t understand what I’m saying.

With this the lag seems to have become much more manageable, so thank you!

The LuckyBlock sequence isn’t the problem I believe, I’m pretty sure it’s the Brainrots coming out. The LuckyBlock function doesn’t seem to be causing any lag at least

Try out switching welds to motor3d or bones, I remember switching to motor3d when I was making tower defense mechanic for performance reasons.

It’s already mostly connected by bones

Not sure how impactful this can be but you should give disabling highlights a try.

no problem, glad I could help! From what I gather you are object caching (cloning/generating a lot of instances at once then using those instead of making new ones each time one is needed) which yeah is good practice. Make sure to test it on lower end devices as well

edit: upon second read I don’t think you are object caching. If it works as is there is no need, otherwise a strategy some games use is creating a set of objects on server start (say 3 models of each brainrot) then when you need to use said model instead of cloning it you use one of the three models that already exists. Also optimally put these objects in workspace just really far away so you don’t have to deal with performance hits from reparenting

Basically I have one folder which contains the normal objects then another one which contains same objects but setup to be drops, for example the second folder’s objects don’t have VFX and cast shadow or collision and have it’s collision group already set.

Wait so is it better or worse to do this:

Create a table of each brainrot when initalising the service. Then when a brainrot is needed I just take it from the table and if it’s busy/being used elsewhere I just created another one and put it in the table. And instead of parenting it I simply put it really far away.

Should I do this for EVERY brainrot?
Or is it better to clone a brainrot at the first use then cache it from the table after?

I feel like doing this on the client is also better?

do it on the client yeah. If you know a set amount of instances that is the maximum you will ever need generate them on join, I assume that isn’t the case though given the nature of your system. Cloning and then storing the models to reuse will work. It’s worth noting this will probably not be a major performance improvement. Looking at the composition of your models I still think the fact you are resizing/positioning them every frame is the biggest issue. In the 1x1x1x1 model I count 17 parts, when you call :ScaleTo or :PivotTo it is just recalculating all the parts of the model individually so all together they form the new size/position of the model. For that single model you are doing 17 sets of CFrame math, vector math, then additional compute to actually render it a frame which obviously adds up over 10-20 models.

Again I think the biggest improvement you’ll see is using RenderStepped instead of tweening. If you update the scale every other frame or every third frame you are doing respectively 50% and 66% less calculations. In my experience a system updating on 30 Hz really isn’t noticeable on 60 fps, especially for quick visual stuff like what you are doing.

Also worth noting the performance costs will only scale with FPS. If someone is playing on 240 FPS (the max the Roblox client allows by default) they are experiencing 4x the load a 60 fps player would. If you aren’t already try testing the system on higher FPS because Id bet money even if it’s smooth on 60 it’ll lag on 240.

I tested it on 240 frames and it was the same as 60 frames, I think maybe you are getting confused with my code, if you look at it again, I’m not actually scaling the brainrots and even with PivotTo(), I’m only doing it once per brainrot the ScaleTo() is only being used for the LuckyBlocks which come out at the beginning, but the LuckyBlocks aren’t causing lag

How exactly are the highlights handled, i dont see them apart of the script you supplied above, are they being instanced multiple times, or just reparented?

ah my bad, seems pretty optimized at this point (at least as far as I can tell). Test on lower end devices and if it’s fine congrats

Would you consider an iphone 11 low end now? I tested it on my iphone and it works very smoothly but on my PC it still lags sometimes lol

They aren’t any highlights now I deleted them all except the ones in the LuckyBlocks but they are never parented via code instead I just have them exist in the LuckyBlock already or in other words I have them already in the LuckyBlock in studio

Sorry for not responding I missed this message, but disabling the highlights didn’t have any bit of impact, at least none that I noticed tbh