Custom particles emitting system

Some of you may know that particle emitters emit less particles for players with lower graphic levels (from settings) than you may have set the rate property to.
In some cases, you may want the particles to be emitted the same amount for everyone, so how do you do that? It’s actually pretty simple, you can have a custom particles system!

Here’s the code:

Code
--- A table (array) to keep the info for each particle
local particleData = {}

--- Assuming you have all the particles you're looking to change in workspace, with the name "CustomParticle"
--- Initializing by adding the particles to the table
for _, obj in pairs(workspace:GetDescendants()) do
	--- checking the requirements (classtype and name)
	if obj:IsA("ParticleEmitter") and obj.Name == "CustomParticle" then

		particleData[#particleData+1] = {
			particle = obj,
			lastEmit = tick(), -- you can use other time alternatives like os.click() as well
			emitTime = 1 / obj.Rate -- somewhat like a cooldown, so that the object emits obj.Rate particles every second
		}

        obj.Rate = 0
	end
end

-- instead of true, you can have the waiting method or a variable in case you'd like to stop all particles at some point
while true do
	for i = 1, #particleData do
		-- checking if the particle object should emit a particle or not (somewhat like a debounce/cooldown system)
		if tick() - particleData[i].lastEmit > particleData[i].emitTime then
			particleData[i].lastEmit = tick()
			particleData[i].obj:Emit(1)
		end
	end
	
	wait() --- you can also have RunService's (.event) :Wait() for more accuracy
end

How does it work?

  1. Initialize all the particles by placing them inside an array, saving the particle object, the rate and an emission time, which you’ll change later.

  2. Change the particle rate to 0, so it does not emit by itself.

  3. Have a loop that keeps checking if the particles should or not emit, depending on when it last emitted a particle and how many particles it should emit every second. If you don’t want the loop to yield, just wrap it into a new coroutine thread.

By using a table, you can continue adding particles even after the initialization :smiley:

EDIT: I’ll finish the module and release it publicly if I see that people want it.

Hopefully you found this useful and even if you may not use this method for this purpose, you can adapt it for all kinds of features

Have a nice day :wink:

8 Likes

Ok, I’ve re-read your system and it seems like you are already aware that :Emit ignores client quality settings. However, it’d still be better to use an alternative listed below on the client rather than storing data and doing checks.


Something like this would work fine for a puffy emitter:

local SpecialParticles = {};
while true do
    for Particle, _ in next, SpecialParticles do
        Particle:Emit(Particle.Rate)
    end
    wait(1)
end

Something like this would work fine for the constant rate:

local RunService = game:GetService("RunService")
local SpecialParticles = {};
while true do
    local _, delta = RunService.Stepped:Wait();
    for Particle, _ in next, SpecialParticles do
        Particle:Emit(Particle.Rate * delta)
    end
end

PS: haven’t tried any of these, but it’d be more beneficial if this were done on the client. This is an improvement suggestion.

2 Likes

Hello there!
I’ve stated the reason for using the custom particles system above:

In some cases, you may want the particles to be emitted the same amount for everyone

So yes, your “ignores Roblox Graphic Levels” is the exact reason someone would use it.

As for your method, unless you make the rate of the particles equal to 0 (or disabling it), roblox will keep emitting them as well.
Now to the delta time from RunService, not only does it vary depending on the FPS, as stated here:

but if the rate is too low, the Particle.Rate * delta value will be lower than 1. Roblox rounds the number if it’s 0.5 or greater, meaning that if you have 0.5 it’ll be taken as 1, and if you have lower than 0.5, it’ll be 0. No matter how you take it, this will make your solution extremely inaccurate.

Best regards,
Mike

While step may vary on the client, this is no different from the inaccuracy from the server replicating to the client (network delay). If I’m lagging with frame drops or high ping, I’m not expecting accuracy with the server, I’m expecting lag. I doubt there is a need for particles to be perfectly in sync. It’d still be much more beneficial to do this on the client.

I’ve tested in studio and you’re right about the Roblox keep emitting the particles (which is odd), based on the tests I’d have to agree you’re going to have to log data.


My other suggestion is to modulize your code and allow particles to be added (when cloned, instanced, etc) to the custom emitting system. And clean up for particle emitters that are removed from the game from the table. It’d be much more flexible and useful if this were done.


PS: It’s rare that people want to bypass the Roblox Graphics. These are just suggestions.

2 Likes

Alright, I’ll do it, though the reason I posted this is not for people just copy and paste code, but rather to understand how this works.

I appreciate that we could reach a common conclusion, as well as that you helped me find a way to optimize it even more, by disabling the particles and not logging the rate anymore.
As far as I can think, roblox uses some kind of method of firstly checking if a particle emitter is Enabled, then emitting the particles depending on the rate. Even micro-optimizations matter when it comes to big games.

Best regards,
Mike

2 Likes

Just a small optimization for the last code sample:

local specialParticles = {}

game:GetService("RunService").RenderStepped:Connect(function(delta)
    for _, emitter in ipairs(SpecialParticles) do
        emitter:Emit(emitter.Rate * delta)
    end
end)

Hey, Using RunService RenderStepped is a good method; But it can be improved. Each Connection you create creates a new thread when tat connection is fired; Hence creating a new thread every render stepped. This is bad but we can sort this out!

while true do
     game.RunService.RenderStepped:Wait()
end

Using the “Wait” Method on a Connection such as RenderStepped helps us wait for the event to fire; Hence causing no new thread as its all single threaded.

2 Likes

Each Connection you create creates a new thread when tat connection is fired;

Nope. A new coroutine is only created on every fire if the connection callback ever yields, otherwise; a thread is only created when the connect method is called.

However, compared to way you just mentioned, this is more ideal. you’re having a constantly running while loop that has a stack index (runService.blah) and then yields via function call. Which is far more work than we need to be doing. (They both end up having events be fired, but one yields until coroutine.resume is called, the other has a function called that does everything)