Custom Particle Emitter Module lags REALLY badly after a few seconds

Hello!

I have been working on an extension for the Roblox Studio Engine for just over 2 weeks, and I’ve found something weird to say the least with my particle system. Honestly, the video will explain it much better than I can, but basically the system lags after a bit of use (not client, only the particles).

Here is when I got into the game:

And here is after 30 seconds:

Here is the file (ParticleEmitter):

--[[
	Blade Engine / Physically Based Particle Emitter
	(BladeParticleEmitter)
	made by melee
	
	DO NOT EDIT ANYTHING BELOW HERE UNLESS YOU KNOW WHAT YOU ARE DOING!
	
	-- EXAMPLE CODE, SERVER SCRIPT
	local module = require(path.to.this.module) -- change this value
	
	local particleEmitter = module.New()
	particleEmitter.PositionOffset = Vector3.new(0, 5, 0)
	particleEmitter:Emit(particleEmitter, "Continuous")
]]--

local ParticleEmitter = {}
ParticleEmitter.__index = ParticleEmitter

type self = {
	-- // DEFAULT PARTICLE EMITTER PROPERTIES
	-- // DO NOT EDIT
	-- Appearance
	Color : Color3 | BrickColor | ColorSequence,
	LightEmission : number,
	LightInfluence : number,
	Orientation : Enum.ParticleOrientation,
	Size : number | NumberSequence,
	Squash : number | NumberSequence,
	Texture : Texture,
	Transparency : number | NumberSequence,
	ZOffset : number,
	
	-- Data
	Archivable : boolean,
	Name : string,
	Parent : Instance,
	
	-- Emission
	EmissionDirection : Enum.NormalId,
	Enabled : boolean,
	LifetimeMin : number,
	LifetimeMax : number,
	Rate : number,
	RotationMin : number,
	RotationMax : number,
	RotSpeedMin : number,
	RotSpeedMax : number,
	SpeedMin : number,
	SpeedMax : number,
	SpreadAngle : Vector2,
	
	-- EmitterShape
	Shape : Enum.ParticleEmitterShape,
	EmitterSize : Vector3,
	ShapeInOut : Enum.ParticleEmitterShapeInOut,
	ShapeStyle : Enum.ParticleEmitterShapeStyle,
	
	-- Flipbook
	-- none
	
	-- Motion
	Acceleration : Vector3,
	
	-- Particles
	Drag : number,
	LockedToPart : boolean,
	TimeScale : boolean,
	VelocityInheritance : number,
	WindAffectsDrag : boolean,
	
	-- // CUSTOM PROPERTIES
	-- Data
	PositionOffset : Vector3,
	Visualize : boolean,
	-- Particles
	HasEnvironmentPhysics : boolean,
	Velocity : Vector3,
}

export type BladeParticleEmitter = typeof(setmetatable({} :: self, ParticleEmitter))

function ParticleEmitter.New() : BladeParticleEmitter
	local self = setmetatable({} :: self, ParticleEmitter)
	-- set all values to default
	-- Appearance
	self.Color = Color3.new(255, 255, 255)
	self.LightEmission = 0
	self.LightInfluence = 1
	self.Orientation = Enum.ParticleOrientation.FacingCamera
	self.Size = 1
	self.Squash = 0
	self.Texture = "rbxasset://textures/particles/sparkles_main.dds"
	self.Transparency = 0
	self.ZOffset = 0
	
	-- Data
	self.Archivable = true
	self.Name = "BladeDefaultParticleEmitter"
	self.Parent = workspace
	self.PositionOffset = Vector3.new(0, 0, 0)
	self.Visualize = false
	
	-- Emission
	self.EmissionDirection = Enum.NormalId.Top
	self.Enabled = true
	self.LifetimeMin = 5
	self.LifetimeMax = 10
	self.Rate = 20
	self.RotationMin = 0
	self.RotSpeedMax = 0
	self.RotSpeedMin = 0
	self.RotSpeedMax = 0
	self.SpeedMin = 5
	self.SpeedMax = 5
	self.SpreadAngle = Vector2.new(0, 0)
	
	-- EmitterShape
	self.Shape = Enum.ParticleEmitterShape.Box
	self.EmitterSize = Vector3.new(2, 2, 2)
	self.ShapeInOut = Enum.ParticleEmitterShapeInOut.Outward
	self.ShapeStyle = Enum.ParticleEmitterShapeStyle.Volume
	
	-- Flipbook
	-- none
	
	-- Motion
	self.Acceleration = Vector3.new(0, 0, 0)
	
	-- Particles
	self.Drag = 0
	self.LockedToPart = false
	self.TimeScale = 1
	self.VelocityInheritance = 0
	self.WindAffectsDrag = false
	self.HasEnvironmentPhysics = false
	
	return self
end

-- Helper Function from Roblox Documentation
local function EvaluateColorSequence(sequence: ColorSequence, time: number)
	-- If time is 0 or 1, return the first or last value respectively
	if time == 0 then
		return sequence.Keypoints[1].Value
	elseif time == 1 then
		return sequence.Keypoints[#sequence.Keypoints].Value
	end

	-- Otherwise, step through each sequential pair of keypoints
	for i = 1, #sequence.Keypoints - 1 do
		local thisKeypoint = sequence.Keypoints[i]
		local nextKeypoint = sequence.Keypoints[i + 1]
		if time >= thisKeypoint.Time and time < nextKeypoint.Time then
			-- Calculate how far alpha lies between the points
			local alpha = (time - thisKeypoint.Time) / (nextKeypoint.Time - thisKeypoint.Time)
			-- Evaluate the real value between the points using alpha
			return Color3.new(
				(nextKeypoint.Value.R - thisKeypoint.Value.R) * alpha + thisKeypoint.Value.R,
				(nextKeypoint.Value.G - thisKeypoint.Value.G) * alpha + thisKeypoint.Value.G,
				(nextKeypoint.Value.B - thisKeypoint.Value.B) * alpha + thisKeypoint.Value.B
			)
		end
	end
end

function ParticleEmitter:Emit(self : BladeParticleEmitter, emissionType : "Continuous" | "Burst")
	-- roblox doesn't support 'self' type-checking by default
	-- orange squiggle is normal
	local edir = Vector3.FromNormalId(self.EmissionDirection) -- convert emission direction to usable normal
	
	if emissionType == "Continuous" then
		local spawnRate = 1 / self.Rate -- default value is 1 / 20 seconds, 50 ms (doesn't matter, calculated through RunService.Heartbeat on server)
		
		while self.Enabled and task.wait(spawnRate) do
			--print("Spawning Particle!")
			-- set values
			local part = Instance.new("Part")
			part.Parent = self.Parent
			part.Name = self.Name
			if self.Parent == workspace then
				part.Position = Vector3.new(0, 0, 0) + self.PositionOffset + Vector3.new(
					math.random(-(self.EmitterSize.X), (self.EmitterSize.X)),
					math.random(-(self.EmitterSize.Y), (self.EmitterSize.Y)),
					math.random(-(self.EmitterSize.Z), (self.EmitterSize.Z))
				)
			else
				part.Position = self.Parent.Position + self.PositionOffset + Vector3.new(
					math.random(-(self.EmitterSize.X), (self.EmitterSize.X)),
					math.random(-(self.EmitterSize.Y), (self.EmitterSize.Y)),
					math.random(-(self.EmitterSize.Z), (self.EmitterSize.Z))
				)
			end
			part.Size = Vector3.new(1, 1, 1)
			part.Anchored = true
			part.CanCollide = false
			part.CanTouch = false
			part.CastShadow = false
			part.Transparency = 1
			part.CanQuery = true
			part.TopSurface, part.BottomSurface = Enum.SurfaceType.Smooth, Enum.SurfaceType.Smooth
			local billboard = Instance.new("BillboardGui")
			billboard.Parent = part
			billboard.Adornee = part
			billboard.Size = UDim2.fromScale(1, 1)
			billboard.ClipsDescendants = false
			local particle = Instance.new("ImageLabel")
			particle.Parent = billboard
			particle.Image = self.Texture
			particle.Size = UDim2.fromScale(1, 1)
			particle.BackgroundTransparency = 1
			local timeSinceSpawn = 0
			local particleLifeTime = math.random(self.LifetimeMin, self.LifetimeMax)
			
			self.Velocity = math.random(self.SpeedMin, self.SpeedMax) * edir
			
			if self.HasEnvironmentPhysics then
				
			else
				
			end
			
			local connnection = game:GetService("RunService").Stepped
			
			connnection:Connect(function(step, dt)
				part.Position += self.Velocity * dt
				if typeof(self.Color) == "ColorSequence" then
					timeSinceSpawn += dt
					local timeLeft = particleLifeTime - timeSinceSpawn
					local colorTime = timeLeft / particleLifeTime
					local particleColorAtCurrentTime = EvaluateColorSequence(self.Color, colorTime)
					particle.ImageColor3 = particleColorAtCurrentTime
				end
			end)
			
			task.delay(particleLifeTime, function()
				part:Destroy()
				billboard:Destroy()
				particle:Destroy()
				connnection:Disconnect()
			end)
		end
	elseif emissionType == "Burst" then
		
	else
		warn("'" .. emissionType .. "' is not a valid emission type!")
	end
end

return ParticleEmitter

I don’t know where to go from here, so any help is greatly appreciated.

1 Like

turns out setting a connection and then connecting it doesn’t allow disconnections
for anyone wondering here is the fix:

local connection = game:GetService("RunService").Stepped

connection:Connect(function())

becomes

local connection = game:GetService("RunService").Stepped:Connect(function())
2 Likes