How to optimize 5000+ parts spinning?

So I use some code a friend gave me to spin EVERY coin in my game (theres more than 5000) at the same time on the client. When a certain distance away, they should stop spinning in hopes to optimize them but it doesnt seem to work… Any help is appriciated!

	local c = coinclone:GetPivot() -- The original position + orientation of the coin
	local start = tick() + math.random() * 10
	game:GetService("RunService").Heartbeat:Connect(function()
		repeat task.wait(); character = game:GetService('Players').LocalPlayer.Character until character
		local dist = (coin:GetPivot().Position - game.Players.LocalPlayer.Character:GetPivot().Position).Magnitude
		if dist > 200 then return end
		local time = tick() - start -- The amount of seconds since the start time
		local rotation = math.rad(time * 180) -- Rotation (180 degrees per second)
		local upoffset = Vector3.new(0, math.cos(time * math.pi / 2) * 0.5 + 1) -- The hover variable
		coinclone:PivotTo(c * CFrame.Angles(0, 0, rotation) + upoffset, 0) -- Set new position + orientation
	end)
2 Likes

You don’t?

I can ask why your game has 5000 parts spinning, but putting that aside, I recommend using a zone-load system so coins will load for each zone/area individually. A good example of this being implemented is Pet Simulator 99, where coins will tween to their maximum size (from being invisible) when a player enters a new zone to reduce lag.

5 Likes

why doesnt the thing that checks the distance of each coin then if its too far then it stops spinning work?

1 Like

because your constantly checking the distance of 5000+ parts in a heartbeat loop

4 Likes

is there a script in each coin, if there is there shouldn’t be.

It should be one script handling ALL the coins. It should only touch the coins within a certain range like idk 100 studs

1 Like

yeah just one local script, i believe its lagging because its checking 5000+ parts at a time so i need a way for it to not check at all in certain areas

3 Likes

how is it even 5k parts?

you should spawn the coin when they are in range, and despawn when they are not in range.

3 Likes

Is there a coin in each script? because what it looks like to me is your creating a bunch of heartbeat events. More RunTime events = more lag

2 Likes

This post is what I was thinking with the no script approach… I guess it comes down to what you actually mean by 5000 parts … That could be 1 object or 5000. If it is something or a few things made of many parts this is pretty lag free … even a example download.

Best way to make parts rotate constantly without lag?

2 Likes

Oh dear. Please try out GetPartBoundsInBox if you’re going to be working with that many items. You could poll it when the person moves with something like:

local players = game:GetService("Players")
local character = players.LocalPlayer

local root = character.HumanoidRootPart
local coinHolder = workspace.Coins --> change this 

local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Include
params:AddToFilter({coinHolder})

local getParts = function()
    local coins = workspace:GetPartBoundsInBox(root.CFrame,Vector3.new(200,10,200),params)
    print(coins)
end

root.Changed:Connect(getParts)

This should check the world for any coins ONLY around your character and you can implement logic from there.

2 Likes

Use fastDistance checking to compare distance instead of .Magnitude. Especially with this many part. Its very easy

3 Likes

I think this is a better approach, give the exact same result but better, allow checking distance with only some given part.

3 Likes

@KindaBadToast you can add a tag using CollectionService to each coin and call it “Coin” and loop through all the coins using CollectionService:GetTagged("Coin") then check if its within range using blog provided by @vipkute0057 to do distance checking.

3 Likes

consider checking the distance between each coin and the player

(humanoidrootpart.Position - coin.Position).Magnitude = distance between humanoidrootpart and coin in studs

also this script hurts my head, why not use a tween or an animation?
i think binding this script to heartbeat is the real reason youre experiencing lag.
just make a tween and set the loop count to -1 for the tween to never end

for instance

for i,e in workspace.CoinsFolder:GetChildren() do 
--in this context "e" is the coin
--this block of code will run once for every coin insdie of workspace.CoinsFolder
--create the tween in here and play it on e
end

edit: lol i just tried this in a studio with 5k parts and its still pretty laggy, U gotta do some sort of distance thing

1 Like

Hey, toast! Avocado here.

There are a few ways I can think of. First, you shouldn’t wait in a RenderStepped or Heartbeat connection. Instead, return if conditions aren’t right. Waiting in this way could cause the thread count to climb incredibly quickly while the character doesn’t exist. This may crash players with a poor connection, because they are still waiting for their character.

local c = coinclone:GetPivot()
local start = tick() + math.random() * 10
game:GetService("RunService").Heartbeat:Connect(function()
    if not game.Players.LocalPlayer.Character then return end
    local dist = (coin:GetPivot().Position - game.Players.LocalPlayer.Character:GetPivot().Position).Magnitude
    if dist > 200 then return end
    local time = tick() - start -- The amount of seconds since the start time
    local rotation = math.rad(time * 180) -- Rotation (180 degrees per second)
    local upoffset = Vector3.new(0, math.cos(time * math.pi / 2) * 0.5 + 1) -- The hover variable
    coinclone:PivotTo(c * CFrame.Angles(0, 0, rotation) + upoffset, 0) -- Set new position + orientation
end)

Then, we should optimize by making variables to reduce the amount of indexing done every frame.

local player = game.Players.LocalPlayer

local RunService = game:GetService("RunService")

local c = coinclone:GetPivot()
local origin = c.Position
local start = tick() + math.random() * 10
local halfpi = math.pi / 2

RunService.Heartbeat:Connect(function()
    local char = player.Character
    if not char then return end

    local dist = (origin - char:GetPivot().Position).Magnitude
    if dist > 200 then return end

    local time = tick() - start

    local rotation = math.rad(time * 180)
    local upoffset = math.cos(time * halfpi) * 0.5 + 1

    coinclone:PivotTo(c * CFrame.Angles(0, 0, rotation) + Vector3.yAxis * upoffset, 0)
end)

Finally, we can use octrees to massively improve the distance checks.

Octrees are a recursive chunking system. They group items up based on which chunks they are in. Searching this way only searches the nearest chunks, and then the items inside of the chunks. Big improvement! It’s easy to set up.

Octree Documentation Octree.rbxm (10.4 KB)

This video helped me learn how to use it.

Here’s a benchmark I made that compares this and checking with magnitude!

image

Octrees are 10-40 times faster than using Magnitude. The more objects and the further apart objects are, the more efficient these are. Even DOORS uses them!

Try implementing Octrees yourself using the tutorial!

4 Likes

Ah thanks! Ill look into Octrees, you always seem to show me a ton of really useful modules!

4 Likes

Finally, we can use octrees to massively improve the distance checks.

this is the solution

what people here fail to notice is that distance checking is prettty expensive…

below are the screenshots of a localscript i made that tweens 5000 coin, which has been created on the server:

image
(this is without StreamingEnabled)

image
(with StreamingEnabled with default settings)

of course, the next step is to do less of those operations, and that’s exactly what your solution does :wink:

1 Like

You’re all trying to solve the problem of how to not optimize spinning 5000+ parts (coins). That is the right approach. Anyway,

Spinning 5000 parts (as in actual Parts) is slow even with BulkMoveTo, because parts can do a lot of things: They can show up on the screen, they can have collision, etc. etc. We only care about spinning the coin on the screen. We do not care about the rest.

Long story short: When you move a MeshPart by changing the Transform of a Bone, you only change how it looks on the screen. That’s exactly what we need I think. Let’s find out if it’s faster.

  1. Transfer your coin model from studio into blender or make a new one. Have fun! First you add a cylinder, then you tab into edit mode, press s then z to scale, do this, and that… I used the default cube for this but you should make a nice coin.

  2. Here’s the wiki article about how to skin your coin so it moves with the bone:
    Skinning a Simple Mesh | Documentation - Roblox Creator Hub
    Add → Armature → Single Bone, select your coin and then the armature and hit Ctrl+P to parent your coin to the armature with automatic weights. It should look like this in the outliner and be all red in weight paint mode with the bone weights selected.
    image

  3. Import your coin into roblox. Good luck! Don’t forget to set the ModelStreamingMode to “Atomic” or “Persistent” when you’re done.
    image
    Yay! What are InitialPoses and AnimationController? Anyway,

  4. Now let’s write some benchmarking code to see if it’s actually faster. Note that we transform the bone. We do not change any CFrames or move any parts. This must run on the player side because bone transforms do not replicate (also please do not animate stuff like this on the server side in general unless you know how to do it good).

--!strict
local COIN_COUNT = 5000
local bones = table.create(COIN_COUNT)

local bonetestclone = workspace:WaitForChild("bone test")
for ii = 1,COIN_COUNT do
	local bonetest = bonetestclone:Clone()
	table.insert(bones, bonetest.RiggedCoin.Bone)
	bonetest:PivotTo(CFrame.new(math.random(-500,500), 5, math.random(-500,500)))
	bonetest.Parent = workspace
end

game:GetService("RunService").Stepped:Connect(function(tt, dt)
	debug.profilebegin("Bones")
	for _,bone in bones do
		bone.Transform = CFrame.Angles(0,0,math.rad(tt*360))
	end
	debug.profileend()
end)
  1. Profile the benchmark with the microprofiler. Set your computer’s power plan thingy to “powersave ECO” or equivalent to get a somewhat useful reading. Whatever you set the cpu frequency to, make it so it can’t clock up or down (min is the same as max). Save this custom plan as a separate thing for convenience maybe.
    image
    This number depends on your device but look we did it! It’s fast!!
    AND NOW we can do the funny octrees and whatever. Maybe it’s okay to only spin 1000 coins instead of 5000. Maybe distant coins can spin every other frame or every 3 frames. How many roblox games have 1000 spinning coins on screen anyway.

  2. AFTERTHOUGHTS:
    There’s an AnimationPlayer and it can play Animations or I mean AnimationTracks. So! Shouldn’t that be even faster?
    image
    As you can see it’s definitely parallelized but also catastrophically worse. I guess you’re not supposed to use animations for this then. Anyway,

  • If your coin model had shiny sparkle particles in it, you can parent those to the bone with an attachment to point it in the right direction. You will want to disable the distant particles of course.
  • We can’t parallelize this with Actors. But we can parallelize everything else in our game to save frametime for spinning coins. As a joke. That would be very funny I think.
  • Motor6D also exists, it’s faster than CFrame, and it works perfectly for spinning things in particular! But bones are the hot new thing we’re supposed to use, and they work with anchored parts.
  • SpecialMesh Offset is also faster than CFrame, but no spinning. Oh well…
  • Profiling on a computer is convenient but you should also measure everything on a phone and tablet and laptop. At least get a feel for the “exchange rates” so you can look at 4ms on your computer in eco mode and feel like that would probably be too slow for phones.
  • Change Transform to CFrame to make it slow again for fun.
    image
    I don’t know or care why this is, it’s still just a bone right? Special CFrame legacy behavior? Whatever!! We already won!!
  • 16ms is always too slow. I like that making it slow made it the exact bad number that you never want to see. On newer displays with higher refresh rates you have even less than 16ms.
5 Likes

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