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.
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
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
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
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