Should this be causing this much lag?

function Service.LuckyBlockEffect(self: Service, Id: string)
	local Data = self.Drops[Id]
	self.Drops[Id] = nil
	local Origin = Data.DropModel:GetPivot()
	local Root = Data.DropModel.PrimaryPart
	if not Root then
		Data.DropModel:Destroy()
		return
	end
	
	local function LuckyBlock()
		TweenService:Create(Root, TweenInfo.new(0.25), { CFrame = Root.CFrame * CFrame.new(0, 0.5, 0) }):Play()
		
		task.wait(0.25)
		local Start = tick()
		local Original = Root.CFrame
		
		local ShakeConnection
		ShakeConnection = RunService.PreRender:Connect(function()
			local Elapsed = tick() - Start
			
			if Elapsed >= 0.5 then
				ShakeConnection:Disconnect()
				Root.CFrame = Original
				return
			end
			
			-- Fixed: denominator now matches the full 0.5s shake
			-- duration, so Strength eases to 0 instead of
			-- overshooting negative and ramping back up.
			local Strength = 0.18 * (1 - Elapsed / 0.5)
			
			Root.CFrame = Original
				* CFrame.new(
					(math.random() - 0.5) * Strength,
					(math.random() - 0.5) * Strength,
					(math.random() - 0.5) * Strength
				)
				* CFrame.Angles(
					math.rad((math.random() - 0.5) * 12),
					math.rad((math.random() - 0.5) * 12),
					math.rad((math.random() - 0.5) * 12)
				)
		end)
	end
	LuckyBlock()
	task.wait(0.5)
	Data.DropModel:Destroy()
	
	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.Brainrots[randomBrainrot] = false
				randomBrainrot.Parent = workspace.Ignore.BrainrotDrops
				BrainrotObject = randomBrainrot
			end
		end
		--
		if BrainrotObject then
			ObjectPool.Brainrots[BrainrotObject] = true -- Mark as busy
			BrainrotObject.Parent = workspace.Ignore.BrainrotDrops
			table.insert(PooledObjects,BrainrotObject)
			
			local Root = BrainrotObject.PrimaryPart
			if Root then
				Root.CFrame = Origin
			else
				BrainrotObject:PivotTo(Origin)
			end
			
			task.wait(0.15)
			BrainrotObject.Parent = nil
		end
	end
	
	for _,Object in PooledObjects do
		if ObjectPool.Brainrots[Object] then
			ObjectPool.Brainrots[Object] = false -- Mark as not busy
			if Object.Parent then
				Object.Parent = nil
			end
		end
	end
end

Fully client sided code

Right now when I play this (3-4 drops are playing) it causes my frames to jitter (go down and dramatically change) then and my network received spikes. This especially happens if I’m also moving my character and my camera.

Why would this be happening?

frame jitter is likely the shake connections stacking, PreRender runs right before camera renders so 3-4 of these running while your character/camera also moves is a lot of work landing in the same frame window

for the network spikes, if Root isn’t anchored and the model also exists on the server, the client ends up fighting for network ownership every time it sets CFrame in PreRender, that’s probably what’s spiking your received bandwidth. if the model’s truly 100% client-only with nothing server side tracking it, anchoring wouldn’t be about network at all, worth checking which case you’re actually in

also the pooling logic clones new models when the pool’s empty and reparents them with task.wait between each, not free if the models are complex. side note, your second loop’s if Object.Parent then Object.Parent = nil end check never actually runs since the first loop already set Parent to nil before it gets there, the busy flag reset is still needed but that inner check is dead code

2 Likes
function Service.LuckyBlockEffect(self: Service, Id: string)
	local Data = self.Drops[Id]
	self.Drops[Id] = nil
	local Origin: CFrame = Data.DropModel:GetPivot()
	local Root = Data.DropModel.PrimaryPart
	if not Root then
		Data.DropModel:Destroy()
		return
	end
	
	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, 0.5, 0) }):Play()
		
		task.wait(0.25)
		local Start = tick()
		local Original = Root.CFrame
		
		local ShakeConnection
		ShakeConnection = RunService.Heartbeat:Connect(function()
			local Elapsed = tick() - Start
			
			if Elapsed >= 0.5 then
				ShakeConnection:Disconnect()
				Root.CFrame = Original
				return
			end
			
			-- Fixed: denominator now matches the full 0.5s shake
			-- duration, so Strength eases to 0 instead of
			-- overshooting negative and ramping back up.
			local Strength = 0.18 * (1 - Elapsed / 0.5)
			
			Root.CFrame = Original
				* CFrame.new(
					(math.random() - 0.5) * Strength,
					(math.random() - 0.5) * Strength,
					(math.random() - 0.5) * Strength
				)
				* CFrame.Angles(
					math.rad((math.random() - 0.5) * 12),
					math.rad((math.random() - 0.5) * 12),
					math.rad((math.random() - 0.5) * 12)
				)
		end)
	end
	LuckyBlock()
	task.wait(0.5)
	Data.DropModel:Destroy()
	
	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.Brainrots[randomBrainrot] = false
				randomBrainrot.Parent = workspace.Ignore.BrainrotDrops
				BrainrotObject = randomBrainrot
			end
		end
		--
		if BrainrotObject then
			ObjectPool.Brainrots[BrainrotObject] = true -- Mark as busy
			BrainrotObject.Parent = workspace.Ignore.BrainrotDrops
			table.insert(PooledObjects,BrainrotObject)
			
			for _,v in pairs(BrainrotObject:GetDescendants()) do
				if v:IsA("BasePart") then
					v.LocalTransparencyModifier = 0
				end
			end
			
			local Character = Player.Character
			local Root = BrainrotObject.PrimaryPart
			if Character then
				local RootPart: Part = Character:FindFirstChild("HumanoidRootPart")
				if RootPart then
					if Root then
						Root.CFrame = CFrame.new(Origin.Position,Vector3.new(Origin.Y,RootPart.Position.Y,Origin.Y))
					else
						BrainrotObject:PivotTo(CFrame.new(Origin.Position,Vector3.new(Origin.Y,RootPart.Position.Y,Origin.Y)))
					end
				end
			end
			
			task.wait(0.15)
			for _,v in pairs(BrainrotObject:GetDescendants()) do
				if v:IsA("BasePart") then
					ObjectPool.Brainrots[BrainrotObject] = false -- Mark as not busy
					v.LocalTransparencyModifier = 1
				end
			end
		end
	end
end

Does this solve all the problems?

nice cleanup on the pooling side, that transparency approach is way cheaper than reparenting. still doesn’t fully answer the two main things though: switching PreRender to Heartbeat doesn’t change the jitter, you’ve still got 3-4 shake connections doing math.random + CFrame every frame regardless of which event runs it. and did you actually try anchoring Root? that’s the part that’d confirm whether the network spikes are ownership related or not

The parts/models are not being created on the server at all and our fully anchored/anchored where they need to be, since PivotTo() is pretty clostly or so I’ve been told. What that means is some parts aren’t anchored but they are connecting by weldconstraints.

That also means the root is anchored too.
That being said is there a way to do the loop without a connection? Or a different connection?

yeah, instead of connecting a new Heartbeat every time an effect plays, keep one single Heartbeat connection running for the whole game and just add/remove entries from a shared table of active shakes. each frame you loop through that table and update all of them, then remove ones that finished. that way you’re not stacking 3-4 separate connections doing the same kind of work, it’s one connection doing a loop over N items instead

Sorry for the late response

Something like this?

	RunService.Heartbeat:Connect(function()
		--LuckyblockEntries
		for Id,Data in LuckyBlocksEntries do
			local Elapsed = os.clock() - Data.Start

			if Elapsed >= 0.5 then
				Data.Part.CFrame = Data.Original
				LuckyBlocksEntries[Id] = nil
				return
			end

			local Strength = 0.18 * (1 - Elapsed / 0.5)

			Data.Part.CFrame = Data.Original
				* CFrame.new(
					(math.random() - 0.5) * Strength,
					(math.random() - 0.5) * Strength,
					(math.random() - 0.5) * Strength
				)
				* CFrame.Angles(
					math.rad((math.random() - 0.5) * 12),
					math.rad((math.random() - 0.5) * 12),
					math.rad((math.random() - 0.5) * 12)
				)
		end
	end)
	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, 0.5, 0) }):Play()
		task.wait(0.25)
		LuckyBlocksEntries[HttpService:GenerateGUID()] = {
			Part = Root,
			Start = os.clock(),
			Original = Root.CFrame
		}
	end
LuckyBlock()
task.wait(0.5)
Data.DropModel:Destroy()

Also how bad is it if I use PivotTo() for the BrainrotObjects? Since I have their PivotOffsets already set, it is much easier to correctly position them that way

good direction with the single connection, but there’s a bug in the loop: that return inside the if Elapsed >= 0.5 branch exits the whole Heartbeat callback, not just that entry. so if one lucky block finishes while others are still shaking, everything after it in the table just gets skipped that frame. swap that return for continue so it only skips the current entry and keeps processing the rest

for a one-time positioning like that (not per-frame), PivotTo is totally fine to use. it’s more expensive than setting CFrame directly since it recalculates the offset for the whole model and moves everything relative to the pivot, but that cost only really matters if you’re calling it every frame, like in the shake loop. spawning 6-8 objects once per drop with PivotTo isn’t gonna be a measurable difference here, use whichever’s easier to position correctly, in this case that’s PivotTo

oml I don’t know how I missed this :sob: ty

Alright perfect, Thanks! I’ll mark your original post as the solution too.

1 Like