-- Place this script in ServerScriptService
local CollectionService = game:GetService("CollectionService")
local parts = {}
-- Bright color palette
local brightColors = {
Color3.fromRGB(255, 0, 0), -- Bright Red
Color3.fromRGB(255, 128, 0), -- Bright Orange
Color3.fromRGB(255, 255, 0), -- Bright Yellow
Color3.fromRGB(0, 255, 0), -- Bright Green
Color3.fromRGB(0, 255, 255), -- Bright Cyan
Color3.fromRGB(0, 128, 255), -- Bright Blue
Color3.fromRGB(128, 0, 255), -- Bright Purple
Color3.fromRGB(255, 0, 255), -- Bright Magenta
Color3.fromRGB(255, 192, 203), -- Bright Pink
Color3.fromRGB(255, 255, 255) -- White
}
-- Function to get random bright color
local function getRandomColor()
return brightColors[math.random(1, #brightColors)]
end
-- Function to setup a model
local function setupModel(model)
for _, descendant in pairs(model:GetChildren()) do
if descendant.Name == "Ball" and descendant:IsA("BasePart") then
table.insert(parts, descendant)
descendant.Massless = true
descendant.CanCollide = false
descendant.Anchored = model.Parent.Name == "Lights"
-- Check if PointLight already exists, if not create one
local pointLight = descendant:FindFirstChildOfClass("PointLight")
if not pointLight then
pointLight = Instance.new("PointLight")
pointLight.Parent = descendant
pointLight.Brightness = 1.2
pointLight.Range = 4
end
-- Set initial random color
local color = getRandomColor()
descendant.Color = color
pointLight.Color = color
elseif descendant.Name == "Part" and descendant:IsA("BasePart") then
model.PrimaryPart = descendant
end
end
end
-- Setup all existing tagged models
for _, model in pairs(CollectionService:GetTagged("LightModel")) do
setupModel(model)
end
-- Setup any models tagged in the future
CollectionService:GetInstanceAddedSignal("LightModel"):Connect(function(model)
setupModel(model)
end)
-- Change colors every 1 second
task.spawn(function()
while task.wait(1) do
print("Changed")
for _, part in pairs(parts) do
if part then -- Check if part still exists
local color = getRandomColor()
part.Color = color
local pointLight = part:FindFirstChildOfClass("PointLight")
if pointLight then
pointLight.Color = color
end
end
end
end
end)
this code changes the color of around 2000 parts. soo doing this on the server makes the recieve die. but when transferring this to a local script in start player. the code just decides to break.
I think it might just be with collection service, try just setting up the part without using collection service. If you need to reference new parts just use .DecendentAdded:Connect(function(decendent)
I can’t use collection service in the client? May I say collection service does work in client or does it. But it should atleast. And collection service is upper necessary cat remove it
I tried it and it worked fine, maybe your not tagging the Models Right, I just had this serverscript that tagged it and it worked.
local folder = game.Workspace:WaitForChild("Folder")
for _, part in ipairs(folder:GetChildren()) do
if part:IsA("Model") then
game:GetService("CollectionService"):AddTag(part, "LightModel")
end
end
Yes you can use collection service on the client. I don’t really know what could be going wrong in your script, could you describe what happens? What do you mean by the code deciding to break, does it just do absolutely nothing, or does it throw an error in the output?
A good way to figure out issues like these is to put print()s in different places in the code, to see what happens, or why something doesn’t happen. As a first step, you can put a print inside of the collection service :GetTagged("LightModel") for loop, and the :GetInstanceAddedSignal("LightModel") connected function, check if you get the prints in the output, and then go up from there into the setupModel(model) function, to see where things don’t do what is expected
Well visually wise sometimes the loop runs, But effects a few tagged parts. This outcome doesn’t occur when used by the server. Or it doesn’t rn the loop at all. I have no clue why
I would suggest, as tests, to disable workspace.StreamingEnabled, put some prints in the loop (perhaps printing the parts table, and also, ensuring that you don’t have a server script still running, that updates the properties of those same parts, as that could overwrite what your local script does
I would suggests you keep on with the goal of doing it on the client, because the client can perform those actions itself without any real downsides. Things that are for animation purposes can usually all be done from the client
While maybe that amount of network usage might be fine (don’t really know, it is a decent amount of data), I would say it is needless, because it can be done on the client, without much more effort (unless you get a nasty bug lol. Then you play detective)
It works fine with only local scripts, all you need to do is tag correctly. Your script works perfectly fine, just how you are tagging the parts does not
This is just an example local script with your script also as a local script
local folder = game.Workspace:WaitForChild("Folder")
for _, model in ipairs(folder:GetChildren()) do
if model:IsA("Model") then
game:GetService("CollectionService"):AddTag(model, "LightModel")
end
end
-- StarterPlayerScripts (LocalScript)
local CollectionService = game:GetService("CollectionService")
local parts = {}
local brightColors = {
Color3.fromRGB(255, 0, 0),
Color3.fromRGB(255, 128, 0),
Color3.fromRGB(255, 255, 0),
Color3.fromRGB(0, 255, 0),
Color3.fromRGB(0, 255, 255),
Color3.fromRGB(0, 128, 255),
Color3.fromRGB(128, 0, 255),
Color3.fromRGB(255, 0, 255),
Color3.fromRGB(255, 192, 203),
Color3.fromRGB(255, 255, 255)
}
local function getRandomColor()
return brightColors[math.random(1, #brightColors)]
end
local function setupModel(model)
for _, descendant in pairs(model:GetChildren()) do
if descendant:IsA("BasePart") and descendant.Name == "Ball" then
table.insert(parts, descendant)
descendant.Massless = true
descendant.CanCollide = false
descendant.Anchored = false
local pointLight = descendant:FindFirstChildOfClass("PointLight") or Instance.new("PointLight", descendant)
pointLight.Brightness = 1.2
pointLight.Range = 4
local color = getRandomColor()
descendant.Color = color
pointLight.Color = color
elseif descendant:IsA("BasePart") and descendant.Name == "Part" then
model.PrimaryPart = descendant
end
end
end
for _, model in pairs(CollectionService:GetTagged("LightModel")) do
setupModel(model)
end
CollectionService:GetInstanceAddedSignal("LightModel"):Connect(setupModel)
task.spawn(function()
local index = 1
local batchSize = 50
while true do
task.wait(0.03)
for i = 1, batchSize do
local part = parts[index]
if part then
local color = getRandomColor()
part.Color = color
local pointLight = part:FindFirstChildOfClass("PointLight")
if pointLight then
pointLight.Color = color
end
end
index = index + 1
if index > #parts then
index = 1
end
end
end
end)
Why do you need another thread?
Also its a very tight loop, you should optimize it well and no have any checks inside.
Also im pretty sure that the reason is your script simply does not run at all since you parented it inside unreachable area/area where engine does not initiate local scripts.
Ok sounds like you understand all that.. it’s just the 2,000 parts thing. Are you sure you’re sure…
Pulling 2,000 parts, replication delays, PrimaryPart access, and Light behavior.. are going to hit hard.
This is why I went all commando on it with my first post.
Need your test results not speculation now. You got a few ways to do it.. start testing..
The code in question doesn’t benefit from being server sided on a security point of view. The script only changes colors and lights, if it isn’t done by the client, it is still being done by the client when the client receives the replication data from the server (or from a remote event).
A hacker also can’t take advantage of visuals to do anything interesting
From optimizing code using the micro profiler, the thread and whatnot doesn’t matter. The bottleneck will very likely be property changes to PointLight.Color. Using the rawRBXmemberSet() function from the post you linked might be a good idea, but I would strongly discourage a beginner look into that
Excellent — this is one of those Roblox optimization traps that a lot of devs hit the first time they do something large-scale on the client vs server.
Let’s break down why your local version “breaks”, and how to fix it cleanly
What’s happening
1. On the server
Your script runs fine (because it’s in ServerScriptService), but:
The server replicates every .Color and .PointLight.Color change for ~2000 parts to every client.
That’s thousands of property updates per second.
When you move it to StarterPlayerScripts, it “breaks” because:
LocalScripts don’t automatically see server parts
LocalScripts only run on the client and can’t see server-only objects unless they’re replicated to the client (in Workspace, ReplicatedStorage, etc.).
If your “LightModel” objects live in ServerStorage or are spawned by the server, the LocalScript can’t find them.
So your line:
for _, model in pairs(CollectionService:GetTagged("LightModel")) do
returns nothing (empty table), because on the client, those tagged objects don’t exist or aren’t replicated yet.
Keep it client-side (best performance)**
If these lights are visual only and don’t need to be networked:
Steps
Move all your “LightModel” instances into ReplicatedStorage or Workspace so the client can see them.
Keep your script in StarterPlayerScripts or StarterCharacterScripts.
Make sure the tag LightModel is applied before the player joins, or add a small delay before scanning tags.
Example fix:
task.wait(2) -- give workspace time to load
for _, model in pairs(CollectionService:GetTagged("LightModel")) do
setupModel(model)
end
Also, ensure the client can see CollectionService tags — they replicate automatically for instances that exist on the client.
This will avoid server replication entirely — each player will locally color the lights independently.