Introducing BasepartEmitter and ViewportEmitter

What Is PartEmitter?

PartEmitter is a simple easy to use and lightweight Class for emitting 3D particles. As this is the first version of the module there are not a lot of features. Documentation can be found down below.

Benefits?

Initially I created this system for use in my own game. However, since I came to realize that this module is actually quite useful for other interesting effects too.

For example you can create colliding particles, 3D particles, particles that emit light and particles that each have differing acceleration and (my favourite) particles that have other effects like particles or trails.

Module

Here is the current Class: Version 1.0.0
local CFramenew = CFrame.new
local RunService = game:GetService("RunService")


local BasepartEmitter = {}

local EmitterList = {}

local function Stepped(runTime,deltaTime)
	for _, Emitter in ipairs(EmitterList) do
		Emitter.TimePosition += deltaTime
		if Emitter.TimePosition > Emitter.Info.Lifetime then
			Emitter.TimePosition -= Emitter.Info.Lifetime	
			local CurrentParticle = Emitter.Clones[Emitter.Current]	
			if not CurrentParticle then
				warn("Particles are being deleted. Make sure particles are not deleted to avoid lag")
				local tempClone = Emitter.Info.Basepart:Clone()
				tempClone.CFrame = Emitter.Info.Origin.CFrame
				tempClone.AssemblyLinearVelocity = Emitter.Velocity
				Emitter.Clones[Emitter.Current] = tempClone
				tempClone.Parent = Emitter.Info.Parent
			elseif CurrentParticle.Parent == nil then
				CurrentParticle.Parent = workspace
			else
				Emitter.ParticleReset:Fire(CurrentParticle)
			end
			CurrentParticle.CFrame = Emitter.Info.Origin.CFrame
			CurrentParticle.AssemblyLinearVelocity = Emitter.Info.Velocity
			Emitter.Current += 1
			if Emitter.Current > Emitter.Info.ParticleAmount then
				Emitter.Current = 1
			end
		end
	end
end

function BasepartEmitter.new(Basepart: BasePart?, Origin: BasePart? | Attachment?, ParticleAmount: number?, Lifetime: number?)
	local Emitter = {
		Basepart				= Basepart or Instance.new("Part");
		ParticleAmount			= ParticleAmount or 10;
		Lifetime				= Lifetime or 2;		-- Lifetime controls rate
		Enabled 				= true;
		Origin					= Origin or workspace:FindFirstChildWhichIsA("Terrain");
		Parent 					= workspace;
		Velocity				= Vector3.new()
	}
	local EmitterMetatable = {
		_newindex = function()
			warn("Tried to add property to object.")
		end;
		_index = function(Table, index)
			print("Indexed")
		end
	}
	local Clones = {}
	
	local ParticleResetEvent = Instance.new("BindableEvent")
	Emitter.ParticleReset = ParticleResetEvent.Event
	
	
	for i = 1, Emitter.ParticleAmount do
		local BasepartClone = Emitter.Basepart:Clone()
		
		BasepartClone.CFrame = Emitter.Origin.CFrame
		BasepartClone.AssemblyLinearVelocity = Vector3.new()
		table.insert(Clones,BasepartClone)
	end
	setmetatable(Emitter,EmitterMetatable)
	
	table.insert(EmitterList,{
		Info			= Emitter;
		Metatable		= EmitterMetatable;
		TimePosition	= 0;
		Current			= 1;
		Clones 			= Clones;
		ParticleReset	= ParticleResetEvent;
		}
	)
	return Emitter
end


RunService.Stepped:Connect(Stepped)

return BasepartEmitter

There are notable a couple of things that probably will be removed or added in the future. This the very first version. Please lmk if you have any things that you would like to have added

How to use

The particle emited has several properties and 1 event and 1 constructor function

Basepart Basepart

Template of the basepart used. Can be anywhere. Currently does not update when this property is changed

number ParticleAmount

Number of particles which will be in existence

number Lifetime

Rate of which the particles will update at. 1 means particles will reset every second

Instance Parent

Controls the parent of the particles

Instance Origin

The attachment or part from where the BasepartEmitter will spawn particles from

Vector3 Velocity

The property controls what the initial velocity of the particles will be

NumberRange Angle

Unimplemented


Events

RBXScriptSignal BasepartEmitted.ResetParticle(Basepart particle)

Fired whenever a particle is reset


Functions

BasepartEmitter BasepartEmitter.new( BasePart BasePart, BasePart Origin, number ParticleAmount, number Lifetime)

Creates a new basepart emitter

Demo

https://gyazo.com/823818ad876b5ade7a36b42d6bddccc2

Code (Not factorized)
local EmitterClass = require(script.Class)

local Particle = workspace.Particle -- I already created a particle with effects and stuff

local Run =game:GetService("RunService")

local Emitter = EmitterClass.new(Particle,workspace.Origin)
Particle.Parent = nil

Emitter.Lifetime = .1
Emitter.ParticleAmount = 10

Run.Heartbeat:Connect(function()
	Emitter.Velocity = workspace.Origin.CFrame.LookVector * 100
end)

Emitter.ParticleReset:Connect(function(particle)
	particle.Trail.Enabled = false
	task.wait()
	task.wait()
	particle.Trail.Enabled = true
end)

Endnotes

I have a few other ideas for a module. But first I have to finish adding stuff to this one first. If you have any complaints or any ideas for improvements let me know. Have a wonderful day :smiley:

44 Likes

Absolutely love this and I will definitely use this! Can’t wait for more features :slight_smile:

1 Like

I think this will help me with my bullet hit effects, awesome!!!

1 Like

Version 2.0.0 - Module Revamp

Just want to improve on the module. It isn’t fully ready yet however I fixed a bunch of things and also added some new APIs that you can use.

I am thinking of doing a Github repository but I am not sure if I have the time to organize something like that and I don’t have the know-how either.

Changes:

* Changed
+ Added
- Removed
* Lifetime > Rate
* Stepped > Heartbeat (Visual Trails can now be changed by simple calling task.wait() once instead of twice)
* Origin: If Origin is a BasePart the Particles will spawn from within the dimensions of the Part. Not just from the center.Also, takes Rotation into account.

+ AngularVelocity
+ TweenInformation  (See Docs)
+ TweenGoal ^ 
+ Functionality for changing BasePart and ParticleAmount and Angle
+ Function GetParticles
+ Function Stop
+ Function Start
+ Function Emit 

- BasePart.new() Parameters for simplicity 

Docs

TweenInfo TweenInfo

TweenInfo for the tween that will be created. (Similar to how you can change the size of a particle and transparency over time. you can now do that with PartEmitter)

Dictionary TweenGoal

This is the how you are able to tween the size, transparency or colour of a particle. NOTE: Only these values are supported by the module to be tweened. Putting a value in here will update the particles that are going to be spawned in the moment.

Functions

Array GetParticles

Returns the array of all the Particles

void Stop

Stops ParticleEmitter

void Start

Starts ParticleEmitter

void Emit

Resets all Particles: NOTE Particle Emitter has to be stoped for this to work. Also, After PartEmitter.Lifetime is reeach the Particles will be Parented to nil.
The number of Particles emitted will be the PartEmitter.ParticleAmount

To be implemented

  • I am thinking of adding a value to toggle self collision easily.

I was thinking that I would only release 1 version but I think since a lot of people like the module I might as well fix some of my code and add a few more things to it.

Code

New Source Code
local CFramenew = CFrame.new
local Vector3new = Vector3.new
local RunService = game:GetService("RunService")
local abs, pi =math.abs, math.pi

local BasePartEmitter = {}

local EmitterList = {}

local Randomizer = Random.new()

local TweenService = game:GetService("TweenService")
local PhysicsService = game:GetService("PhysicsService")

local function GetRandomOrientation()
	return CFrame.Angles(
		Randomizer:NextNumber(0,pi * 2),
		Randomizer:NextNumber(0,pi * 2),
		Randomizer:NextNumber(0,pi * 2)
	)
end

local CurrentParticle = nil
local TweenInformation = nil
local TweenGoal = nil

local TargetCFrame = CFramenew()

local function Heartbeat(deltaTime)
	
	--print("Untracked memory:",gcinfo().."kB")	-- To Remove
	for _, Emitter in ipairs(EmitterList) do
		if not Emitter.ProxyTable.Origin then
			warn("Origin not set. PartEmitter is stopping.")
			table.remove(EmitterList,Emitter.Table.EmitterID)
			continue
		elseif not Emitter.Table.Particle then
			warn("Particle not set. PartEmitter is stopping.")
			table.remove(EmitterList,Emitter.Table.EmitterID)
			continue
		end
		Emitter.TimePosition += deltaTime
		if Emitter.TimePosition > Emitter.ProxyTable.Rate then
			Emitter.TimePosition -= Emitter.ProxyTable.Rate	
			CurrentParticle = Emitter.Clones[Emitter.Current]
			if not CurrentParticle then
				warn("Particles are being deleted. Make sure particles are not deleted to avoid lag")
				local tempClone = Emitter.Table.Particle:Clone()
				tempClone.CFrame = Emitter.ProxyTable.Origin.CFrame
				tempClone.AssemblyLinearVelocity = Emitter.Velocity
				Emitter.Clones[Emitter.Current] = tempClone
				tempClone.Parent = Emitter.Table.Parent
			elseif CurrentParticle.Parent ~= Emitter.ProxyTable.Parent then
				CurrentParticle.Parent = Emitter.ProxyTable.Parent
			end
			Emitter.ParticleReset:Fire(CurrentParticle)
			if Emitter.Table.TweenInformation and Emitter.Table.TweenGoal then
				
				CurrentParticle.Tween:Play()
			end
			if Emitter.ProxyTable.Origin:IsA("BasePart") then
				CurrentParticle.CFrame = Emitter.ProxyTable.Origin.CFrame:ToWorldSpace(CFramenew(
					Emitter.ProxyTable.Origin.Size.X * abs(Randomizer:NextNumber()),
					Emitter.ProxyTable.Origin.Size.Y * abs(Randomizer:NextNumber()),
					Emitter.ProxyTable.Origin.Size.Z * abs(Randomizer:NextNumber())
				) - (Emitter.ProxyTable.Origin.Size / 2))
			else
				CurrentParticle.CFrame = Emitter.ProxyTable.Origin.CFrame --* GetRandomOrientation()
			end
			
			CurrentParticle.AssemblyLinearVelocity = Emitter.ProxyTable.Velocity
			CurrentParticle.AssemblyAngularVelocity = Emitter.ProxyTable.AngularVelocity
			Emitter.Current += 1
			if Emitter.Current > Emitter.Table.ParticleAmount then
				Emitter.Current = 1
			end
		end
	end
end

function BasePartEmitter.new(Origin: Instance?, Particle: BasePart?)
	-- Proxy EmitterTable
	local _Emitter = {
		Origin					= Origin or workspace;		-- Where particles originate from
		Velocity				= Vector3new();				-- Starting Velocity of Particles
		AngularVelocity			= Vector3new();
		Parent 					= workspace;				-- Parent of the particles
		Rate					= 1;						-- Rate which particles update
		Angle					= 0;						-- Angle of deviation from Velocity Vector, 180 is max.
	}
	-- Original EmitterTable
	local Emitter = {
		Particle				= Particle or Instance.new("Part");
		ParticleAmount			= 10;
		TweenInformation		= nil;
		TweenGoal				= nil;
		EmitterID 				= #EmitterList + 1;
	}
	-- Particle EmitterTable
	local Clones = {}
	
	-- Meta EmitterTable
	local EmitterMetatable = {
		__newindex = function(self, index, value)
			if typeof(index) == "string" then
				if index == "BasePart" then					
					table.foreach(Clones,function(index, BasePart)
						BasePart:Destroy()
					end)					
					table.clear(Clones)
					Emitter.Particle = value
					for i = 1, Emitter.ParticleAmount do
						local BasePartClone: BasePart = Emitter.Particle:Clone()
						
						BasePartClone.CFrame = Emitter.Origin.CFrame
						BasePartClone.AssemblyLinearVelocity = Vector3.new()
						
						--local Tween:Tween = TweenService:Create(BasePartClone,Emitter.Tween.TweenTable,Emitter.TweenGoal)
						local BrickColor3Value = Instance.new("BrickColorValue",BasePartClone)
						BrickColor3Value.Value = BasePartClone.BrickColor
						local Color3Value = Instance.new("Color3Value",BasePartClone)
						Color3Value.Value = BasePartClone.Color
						local TransparencyValue = Instance.new("NumberValue",BasePartClone)
						TransparencyValue.Value = BasePartClone.Transparency
						local SizeValue = Instance.new("Vector3Value",BasePartClone)
						SizeValue.Value = BasePartClone.Size
						
						table.insert(Clones,BasePartClone)
					end
					EmitterList[Emitter.EmitterID].Clones = Clones
				elseif index == "TweenInformation" or index == "TweenGoal" then
					-- Differentiate between info and goal
					if index == "TweenInformation" then
						if typeof(value) ~= "TweenInfo" then
							return
						end
						TweenInformation = value
						TweenGoal = Emitter.TweenGoal
					else
						if type(value) ~= "table" then
							return
						end
						TweenGoal = value
						TweenInformation = Emitter.TweenInformation
					end
					if TweenInformation and TweenGoal then
						-- Recreate Tweens is they exist
						for _, BasePart: BasePart in ipairs(Clones) do
							local Tween: Tween = BasePart.Tween
							if Tween then
								Tween:Destroy()
							end
							Tween =	TweenService:Create(BasePart,TweenInformation,TweenGoal)
							Tween.Parent = BasePart
						end
					end
				elseif index == "ParticleAmount" then
					local Difference = value - Emitter.ParticleAmount
					Emitter.ParticleAmount = value
					if value > 0 then 
						for i = 1, Difference do
							local BasePartClone: BasePart = Emitter.Particle:Clone()
								
							BasePartClone.CFrame = _Emitter.Origin.CFrame
							BasePartClone.AssemblyLinearVelocity = Vector3.new()
							
							local BrickColor3Value = Instance.new("BrickColorValue",BasePartClone)
							BrickColor3Value.Value = BasePartClone.BrickColor
							local Color3Value = Instance.new("Color3Value",BasePartClone)
							Color3Value.Value = BasePartClone.Color
							local TransparencyValue = Instance.new("NumberValue",BasePartClone)
							TransparencyValue.Value = BasePartClone.Transparency
							local SizeValue = Instance.new("Vector3Value",BasePartClone)
							SizeValue.Value = BasePartClone.Size
							
							local Tween: Tween = BasePartClone.Tween
							if Tween then
								Tween:Destroy()
							end
							Tween =	TweenService:Create(BasePartClone,Emitter.TweenGoal,Emitter.TweenGoal)
							Tween.Parent = BasePartClone
							
							table.insert(Clones,BasePartClone)
						end
					else
						for i = 1, -value do
							table.remove(Clones,#Clones)
						end						
					end
				end
			end
		end
	}
	
	local ParticleResetEvent = Instance.new("BindableEvent")
	_Emitter.ParticleReset = ParticleResetEvent.Event
	-- ReadOnly
	function Emitter:GetParticles()
		return Clones
	end
	-- Stop Particle Emitter
	function _Emitter:Stop()
		table.remove(EmitterList,Emitter.EmitterID)
	end
	-- Start Particle Emitter
	function _Emitter:Start()
		table.insert(
			EmitterList,
			{
				ProxyTable		= _Emitter;
				Table			= Emitter;
				Metatable		= EmitterMetatable;
				TimePosition	= 0;
				Current			= 1;
				Clones 			= Clones;
				ParticleReset	= ParticleResetEvent;
			}
		)
	end
	
	function _Emitter:Emit()
		if not table.find(EmitterList,Emitter.EmitterID) then
			for _, Particle: BasePart in ipairs(Clones) do
				if not Emitter.ProxyTable.Origin then
					warn("Origin not set. PartEmitter is stopping.")
					return
				elseif not Emitter.Table.Particle then
					warn("Particle not set. PartEmitter is stopping.")
					return
				end
					
				if Emitter.Table.TweenInformation and Emitter.Table.TweenGoal then
					Particle.Tween:Play()
				end
				if Emitter.ProxyTable.Origin:IsA("BasePart") then
					CurrentParticle.CFrame = Emitter.ProxyTable.Origin.CFrame:ToWorldSpace(CFramenew(
						Emitter.ProxyTable.Origin.Size.X * abs(Randomizer:NextNumber()),
						Emitter.ProxyTable.Origin.Size.Y * abs(Randomizer:NextNumber()),
						Emitter.ProxyTable.Origin.Size.Z * abs(Randomizer:NextNumber())
						) - (Emitter.ProxyTable.Origin.Size / 2))
				else
					CurrentParticle.CFrame = Emitter.ProxyTable.Origin.CFrame --* GetRandomOrientation()
				end
				
				CurrentParticle.AssemblyLinearVelocity = Emitter.ProxyTable.Velocity
				CurrentParticle.AssemblyAngularVelocity = Emitter.ProxyTable.AngularVelocity
			end
		end
	end
	-- Instantiate particles
	for i = 1, Emitter.ParticleAmount do
		local BasePartClone: BasePart = Emitter.Particle:Clone()
		if not _Emitter.Origin then 
			warn("Origin not set")
		else
			BasePartClone.CFrame = _Emitter.Origin.CFrame
		end
		BasePartClone.AssemblyLinearVelocity = Vector3.new()
		--local Tween:Tween = TweenService:Create(BasePartClone,Emitter.TweenInfo,Emitter.TweenGoal)
		--Tween.Parent = BasePartClone
		
		local BrickColor3Value = Instance.new("BrickColorValue",BasePartClone)
		BrickColor3Value.Value = BasePartClone.BrickColor
		local Color3Value = Instance.new("Color3Value",BasePartClone)
		Color3Value.Value = BasePartClone.Color
		local TransparencyValue = Instance.new("NumberValue",BasePartClone)
		TransparencyValue.Value = BasePartClone.Transparency
		local SizeValue = Instance.new("Vector3Value",BasePartClone)
		SizeValue.Value = BasePartClone.Size
		
		
		
		table.insert(Clones,BasePartClone)
	end
	
	
	-- Detect changes to proxy table
	setmetatable(_Emitter,EmitterMetatable)
	
	-- Add to EmitterList in Class for updating 
	table.insert(EmitterList,{
		ProxyTable		= _Emitter;
		Table			= Emitter;
		Metatable		= EmitterMetatable;
		TimePosition	= 0;
		Current			= 1;
		Clones 			= Clones;
		ParticleReset	= ParticleResetEvent;	
		}
	)
	return _Emitter
end


RunService.Heartbeat:Connect(Heartbeat)

return BasePartEmitter

5 Likes

Been trying to figure out a way to make a refraction effect on fire particle emitters, and I believe this code can help me out. I can just make that emitter part glass since glass adds that light bend effect, with a high transparent number. Will keep you updated.

1 Like

Yeah, I have actually just realized that there are still bugs in the code that I sent. Lmk if you have any problems and I’ll update it again.

1 Like

Please publish this module because if someone wants automatic updates he just requires id.

1 Like

Hello there. I recently got back into business with this project. I was thinking on trying to make an improved version and release the module in the market place.

Is this something you want to see?

Also

Viewport Frame Particle Emitters!

If you have wanted to do some particle emitters inside viewport frames then now you can!

Wow look at that!

There are two versions. Both use the same concept but in different ways. One uses an active updatin system. The other uses tweens. I haven’t completed the active one yet. ViewportParticle Tween is much much faster than the active updating but it also looks a litte bit worse.

Here is the source code:

local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")

local Randomness = Random.new()

local cos, rad = math.cos, math.rad

type SimpleParticle = {
	Drag: boolean,
	Lifetime: number,
	SpreadAngle: number,
	Rate: number,
ImageId: string,
Origin: BasePart | Attachment,
Speed: NumberRange,
Transparency: NumberRange,
Size: NumberRange,
EmissionDirection: Enum.NormalId,
Orientation: Enum.ParticleOrientation,
Colour: {
	Start: Color3,
	End: Color3
},
Step: (SimpleParticle,dt: number) -> ()
}

local function GetValidDirection(direction, spread)
	local proposed
	repeat
		proposed = Randomness:NextUnitVector()
	until proposed:Dot(direction) > cos(rad(spread))
	return proposed
end

local SimpleParticle = function(
	imageId,origin: BasePart | Attachment,viewport: ViewportFrame
)
	
	local Particles: {BasePart} = {}
	local TimePosition = 0
	local CurrentParticle = 0
	local function Step(self: SimpleParticle, dt: number)
		local phase = 1 / self.Rate
		TimePosition += dt
		if TimePosition >= phase and viewport.CurrentCamera then
			TimePosition -= phase
			CurrentParticle += 1
			if CurrentParticle > #Particles then
				CurrentParticle = 1
			end
			local cframeOrigin
			if origin:IsA("BasePart") then
				cframeOrigin = origin.CFrame
			else
				cframeOrigin = origin.WorldCFrame
			end
			
			local Direction = GetValidDirection(
				cframeOrigin:VectorToWorldSpace(Vector3.fromNormalId(self.EmissionDirection)),
				self.SpreadAngle
			)
			Particles[CurrentParticle].CFrame = CFrame.lookAt(origin.Position,workspace.CurrentCamera.CFrame.Position)
			local tween = TweenService:Create(
				Particles[CurrentParticle],
				TweenInfo.new(self.Lifetime,Enum.EasingStyle.Linear,Enum.EasingDirection.Out),
				{
					Size = (Vector3.one - Vector3.zAxis) * (self.Size.Max),
					CFrame = CFrame.lookAt(
						origin.Position + (Direction * self.Lifetime * Randomness:NextNumber(self.Speed.Min,self.Speed.Max)),
						viewport.CurrentCamera.CFrame.Position
					)
				}
			)
			tween:Play()
			Debris:AddItem(tween,self.Lifetime)
			
		end
	end
	
	
	local self: SimpleParticle = {
		ImageId = imageId,
		Origin = origin,
		Lifetime = 10,
		Speed = NumberRange.new(10),
		SpreadAngle = 30,
		Rate = 10,
		Drag = false,
		Transparency = NumberRange.new(0),
		EmissionDirection = Enum.NormalId.Top,
		Colour = {
			Start = Color3.new(),
			End = Color3.new()
		},
		Step = Step,
		Size = NumberRange.new(1),
		Orientation = Enum.ParticleOrientation.FacingCamera
	}
	local particleAmount = self.Lifetime * self.Rate
	for i = 1, particleAmount do
		Particles[i] = Instance.new("Part")
		Particles[i].Transparency = 1
		Particles[i].Size = (Vector3.one - Vector3.zAxis) * (self.Size.Min)
		Particles[i].Parent = viewport:FindFirstChildWhichIsA("WorldModel")
		local decal: Decal = Instance.new("Decal")
		decal.Texture = imageId
		decal.Color3 = self.Colour.Start
		decal.Transparency = self.Transparency.Min
		decal.Parent = Particles[i]
	end
	return self
end

return SimpleParticle

The code is unfactoriezed I’m not sure I want to extend the module to include this but we will see.

In the future I want to fix up this module and make it production ready.

I won’t create the docs for now. This is just if you are interested in making something like this yourself.

13 Likes

By any chance, are you able to make thsse particles compatible with the current Roblox particle system by imitating the original particle-emitters movement?

My sincerest apologies for the bump…

Don’t be ashamed to ask an honest question.

It is possible.

The particles in the ViewportEmitter here are made using tweens for simplicity but it should be able to be adapted. (Much easier to code)
The BaspartEmitter should already be setup to do that. If you want to have images instead of basparts you can use a sphere and a BillboardGui.

I did make another one which updated every frame. But, never got around to releasing it as I found it a bit unnecessary to have collisions in Viewport Frame.

The code is slightly unwieldy as it uses a proxy table. (this is initially because I am a purist and wanted proper intellisense) lmk if you need a quick explainer.

1 Like

sorry for the bump, but how do you use the source code?