├── README.md ├── UIParticle.lua └── UIParticleMethodChaining.lua /README.md: -------------------------------------------------------------------------------- 1 | # UIParticle 2 | An attempt at replicating the ROBLOX ParticleEmitters as closely as possible in 2D UIs 3 | 4 | ### Installation 5 | Create a new ModuleScript anywhere (preferably ReplicatedStorage) and paste the source code into it. 6 | 7 | ### Usage 8 | 9 | ```lua 10 | --Require the module 11 | local UIParticleEmitter = require("path.to.ModuleScript") 12 | 13 | --Create a new particle emitter, passing a hook (the UI equivalent of a ParticleEmitter's parent) and an element (the element that will represent a particle) 14 | local emitter = UIParticleEmitter.new(hook, element) 15 | 16 | --Assign some properties 17 | emitter.Rate = 20 18 | emitter.Acceleration = Vector2.new(15,15) 19 | emitter.Color = Color3.new(0.5,0.5,1) 20 | 21 | --Emit 50 particles 22 | emitter:Emit(50) 23 | 24 | --Enable the emitter 25 | emitter.Enabled = true 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /UIParticle.lua: -------------------------------------------------------------------------------- 1 | --custom type for the 2d number sequences 2 | export type NumberSequence2D = { 3 | X: NumberSequence, 4 | Y: NumberSequence 5 | } 6 | 7 | export type Particle = { 8 | Element: GuiObject, 9 | Speed: number, 10 | Position: Vector2, 11 | SpreadAngle: number, 12 | RotSpeed: number, 13 | Acceleration: Vector2, 14 | Size: NumberSequence2D | Vector2, 15 | Transparency: NumberSequence | number, 16 | Color: ColorSequence | Color3, 17 | Age: number, 18 | Ticks: number, 19 | maxAge: number, 20 | isDead: boolean, 21 | Canvas: CanvasGroup, 22 | new: (Emitter: ParticleEmitter2D) -> (Particle), 23 | Update: (self: Particle, delta: number) -> (), 24 | Destroy: (self: Particle) -> () 25 | } 26 | 27 | local ParticleClass: Particle = {} 28 | ParticleClass.__index = ParticleClass 29 | local function Rotate(v: Vector2, degrees: number) 30 | local sin = math.sin(math.rad(degrees)); 31 | local cos = math.cos(math.rad(degrees)); 32 | 33 | local tx = v.x; 34 | local ty = v.y; 35 | return Vector2.new((cos * tx) - (sin * ty),(sin * tx) + (cos * ty)) 36 | end 37 | 38 | local function Normalize(min, max, alpha) 39 | return (alpha - min)/(max-min) 40 | end 41 | 42 | -- sequence evaluation functions taken from developer hub 43 | 44 | function evalCS(cs, t) 45 | if typeof(cs) ~= "ColorSequence" then return cs end 46 | -- If we are at 0 or 1, return the first or last value respectively 47 | if t == 0 then return cs.Keypoints[1].Value end 48 | if t >= 1 then return cs.Keypoints[#cs.Keypoints].Value end 49 | -- Step through each sequential pair of keypoints and see if alpha 50 | -- lies between the points' time values. 51 | for i = 1, #cs.Keypoints - 1 do 52 | local this = cs.Keypoints[i] 53 | local next = cs.Keypoints[i + 1] 54 | if t >= this.Time and t < next.Time then 55 | -- Calculate how far alpha lies between the points 56 | local alpha = (t - this.Time) / (next.Time - this.Time) 57 | -- Evaluate the real value between the points using alpha 58 | return Color3.new( 59 | (next.Value.R - this.Value.R) * alpha + this.Value.R, 60 | (next.Value.G - this.Value.G) * alpha + this.Value.G, 61 | (next.Value.B - this.Value.B) * alpha + this.Value.B 62 | ) 63 | end 64 | end 65 | end 66 | 67 | local function evalNS(ns, t) 68 | if typeof(ns) ~= "NumberSequence" then return ns end 69 | -- If we are at 0 or 1, return the first or last value respectively 70 | if t == 0 then return ns.Keypoints[1].Value end 71 | if t >= 1 then return ns.Keypoints[#ns.Keypoints].Value end 72 | -- Step through each sequential pair of keypoints and see if alpha 73 | -- lies between the points' time values. 74 | for i = 1, #ns.Keypoints - 1 do 75 | local this = ns.Keypoints[i] 76 | local next = ns.Keypoints[i + 1] 77 | if t >= this.Time and t < next.Time then 78 | -- Calculate how far alpha lies between the points 79 | local alpha = (t - this.Time) / (next.Time - this.Time) 80 | -- Evaluate the real value between the points using alpha 81 | return (next.Value - this.Value) * alpha + this.Value 82 | end 83 | end 84 | end 85 | 86 | function evalNR(range) 87 | if typeof(range) ~= "NumberRange" then return range end 88 | return math.random(range.Min, range.Max) 89 | end 90 | function ParticleClass.new(emitter) 91 | local self = {} 92 | self.Element = emitter.Element:Clone() 93 | self.Color = emitter.Color 94 | self.StartSize = self.Element.AbsoluteSize 95 | self.Transparency = emitter.Transparency 96 | self.Canvas = Instance.new("CanvasGroup") 97 | self.Canvas.Parent = emitter.Hook:FindFirstAncestorWhichIsA("LayerCollector") 98 | self.Canvas.Size = UDim2.fromOffset(self.Element.AbsoluteSize.X, self.Element.AbsoluteSize.Y) 99 | self.Canvas.AnchorPoint = Vector2.new(0.5,0.5) 100 | self.Canvas.BackgroundTransparency = 1 101 | self.Canvas.ZIndex = emitter.Hook.ZIndex + emitter.ZOffset 102 | self.Canvas.GroupColor3 = evalCS(self.Color, 0) 103 | self.Canvas.GroupTransparency = evalNS(self.Transparency, 0) 104 | self.Element.Size = UDim2.fromScale(1,1) 105 | local spawnPosition 106 | if emitter.EmitterMode == "Point" then 107 | spawnPosition = emitter.Hook.AbsolutePosition 108 | local size = emitter.Hook.AbsoluteSize 109 | spawnPosition = Vector2.new(spawnPosition.X + size.X/2, spawnPosition.Y + size.Y/2) 110 | else 111 | spawnPosition = emitter.Hook.AbsolutePosition 112 | local size = emitter.Hook.AbsoluteSize 113 | spawnPosition = Vector2.new(spawnPosition.X + math.random(0, size.X), spawnPosition.Y + math.random(0, size.Y)) 114 | end 115 | 116 | self.Position = spawnPosition 117 | self.Canvas.Position = UDim2.new( 118 | UDim.new(0, self.Position.X), 119 | UDim.new(0, self.Position.Y) 120 | ) 121 | self.Element.AnchorPoint = Vector2.new(0.5,0.5) 122 | self.Element.Position = UDim2.fromScale(0.5,0.5) 123 | self.Element.Parent = self.Canvas 124 | self.Size = emitter.Size 125 | emitter.preSpawn(self.Element) 126 | self.Speed = Vector2.new( 127 | evalNR(emitter.xSpeed), 128 | evalNR(emitter.ySpeed) 129 | ) 130 | 131 | 132 | self.SpreadAngle = evalNR(emitter.SpreadAngle) 133 | self.RotSpeed = evalNR(emitter.RotSpeed) 134 | 135 | --unrotate the acceleration since the speed will be rotated 136 | self.Acceleration = Rotate(emitter.Acceleration, -self.SpreadAngle) 137 | 138 | self.Transparency = emitter.Transparency 139 | self.Age = 0 140 | self.Ticks = 0 141 | self.maxAge = evalNR(emitter.Lifetime) 142 | self.isDead = false 143 | 144 | return setmetatable(self, ParticleClass) 145 | end 146 | 147 | 148 | 149 | function ParticleClass:Update(delta) 150 | 151 | if self.Age >= self.maxAge and self.maxAge > 0 then 152 | self:Destroy() 153 | return 154 | end 155 | 156 | 157 | self.Ticks = self.Ticks + 1 158 | self.Age = self.Age + delta 159 | local xSize, ySize = evalNS(self.Size.X, Normalize(0, self.maxAge, self.Age)), evalNS(self.Size.Y, Normalize(0, self.maxAge, self.Age)) 160 | if xSize and ySize then 161 | self.Canvas.Size = UDim2.new( 162 | UDim.new(0, self.StartSize.X * xSize), 163 | UDim.new(0, self.StartSize.Y *ySize) 164 | ) 165 | end 166 | local nextColor = evalCS(self.Color, Normalize(0, self.maxAge, self.Age)) 167 | if nextColor and nextColor ~= self.Canvas.GroupColor3 then 168 | self.Canvas.GroupColor3 = nextColor 169 | end 170 | self.Canvas.GroupTransparency = evalNS(self.Transparency, Normalize(0, self.maxAge, self.Age)) 171 | 172 | 173 | local dir = Rotate(self.Speed, self.SpreadAngle) * Vector2.new(1,-1) 174 | self.Speed += (self.Acceleration * delta) 175 | 176 | self.Position += (dir * delta) 177 | self.Canvas.Position = UDim2.new( 178 | UDim.new(0, self.Position.X), 179 | UDim.new(0, self.Position.Y) 180 | ) 181 | 182 | 183 | 184 | self.Canvas.Rotation += self.RotSpeed * delta 185 | 186 | end 187 | 188 | function ParticleClass:Destroy() 189 | self.isDead = true 190 | self.Canvas:Destroy() 191 | end 192 | 193 | 194 | export type ParticleEmitter2D = { 195 | particles: {Particle}, 196 | Enabled: boolean, 197 | Element: GuiObject, 198 | Hook: GuiObject, 199 | preSpawn: (Particle) -> (), 200 | Rate: number, 201 | Color: ColorSequence | Color3, 202 | Size: NumberSequence2D | Vector2, 203 | Transparency: NumberSequence | number, 204 | ZOffset: number, 205 | xSpeed: NumberRange | number, 206 | ySpeed: NumberRange | number, 207 | SpreadAngle: NumberRange | number, 208 | RotSpeed: NumberRange | number, 209 | Lifetime: NumberRange | number, 210 | Acceleration: Vector2, 211 | EmitterMode: ("Point") | ("Fill"), 212 | __dead: boolean, 213 | __elapsedTime: number, 214 | __runServiceConnection: RBXScriptConnection, 215 | new: (Hook: GuiObject, Element: GuiObject) -> (ParticleEmitter2D), 216 | fromEmitter3D: (Hook: GuiObject, Emitter: ParticleEmitter, unitMultiplier: number?) -> (ParticleEmitter2D), 217 | Emit: (self: ParticleEmitter2D, count: number) -> (), 218 | Destroy: (self: ParticleEmitter2D) -> () 219 | } 220 | 221 | local ParticleEmitterClass: ParticleEmitter2D = {} 222 | ParticleEmitterClass.__index = ParticleEmitterClass 223 | function ParticleEmitterClass.fromEmitter3D(hook: GuiObject, emitter: ParticleEmitter, unitMultiplier: number?) 224 | local self = {} 225 | unitMultiplier = unitMultiplier or 1 226 | self.particles = {} 227 | self.Enabled = false 228 | self.Element = Instance.new("ImageLabel") 229 | self.Element.Size = UDim2.new(0,unitMultiplier,0,unitMultiplier) 230 | self.Element.Image = emitter.Texture 231 | self.Element.BackgroundTransparency = 1 232 | self.Element.Parent = game.ReplicatedStorage 233 | self.Hook = hook 234 | 235 | self.preSpawn = function(p) end 236 | 237 | --properties 238 | self.Rate = emitter.Rate 239 | self.Color = emitter.Color 240 | self.Size = {X = emitter.Size, Y = emitter.Size} 241 | self.Transparency = emitter.Transparency 242 | self.ZOffset = emitter.ZOffset 243 | self.xSpeed = NumberRange.new(0,0) 244 | self.ySpeed = NumberRange.new(emitter.Speed.Min * unitMultiplier, emitter.Speed.Max * unitMultiplier) 245 | self.SpreadAngle = NumberRange.new(emitter.SpreadAngle.X, emitter.SpreadAngle.Y) 246 | self.RotSpeed = emitter.RotSpeed 247 | self.Lifetime = emitter.Lifetime 248 | self.Acceleration = Vector2.new(emitter.Acceleration.X * unitMultiplier, emitter.Acceleration.Y * unitMultiplier) 249 | 250 | self.EmitterMode = emitter.ShapeStyle == Enum.ParticleEmitterShapeStyle.Volume and "Fill" or "Point" 251 | 252 | 253 | 254 | self.__dead = false 255 | self.__elapsedTime = 0 256 | 257 | self.__runServiceConnection = game:GetService("RunService").Heartbeat:Connect(function(delta) 258 | 259 | 260 | self.__elapsedTime = self.__elapsedTime + delta 261 | for index, particle in ipairs(self.particles) do 262 | if particle.isDead then 263 | table.remove(self.particles, index) 264 | else 265 | particle:Update(delta) 266 | end 267 | end 268 | 269 | 270 | if self.Rate > 0 and (self.__dead == false) and self.Enabled then 271 | while self.__elapsedTime >= (1/self.Rate) do 272 | table.insert(self.particles, ParticleClass.new(self)) 273 | self.__elapsedTime = self.__elapsedTime - (1/self.Rate) 274 | end 275 | end 276 | end) 277 | 278 | return setmetatable(self, ParticleEmitterClass) 279 | end 280 | 281 | function ParticleEmitterClass.new(hook: GuiObject, particleElement: GuiObject) 282 | local self = {} 283 | 284 | self.particles = {} 285 | self.Enabled = false 286 | self.Element = particleElement 287 | self.Hook = hook 288 | 289 | self.preSpawn = function(p) end 290 | 291 | --properties 292 | self.Rate = 20 293 | self.Color = ColorSequence.new(Color3.new(1,1,1)) 294 | self.Size = {X = NumberSequence.new(1), Y = NumberSequence.new(1)} 295 | self.Transparency = NumberSequence.new(0) 296 | self.ZOffset = 0 297 | self.xSpeed = NumberRange.new(0,0) 298 | self.ySpeed = NumberRange.new(150,500) 299 | self.SpreadAngle = NumberRange.new(-15,15) 300 | self.RotSpeed = NumberRange.new(0) 301 | self.Lifetime = NumberRange.new(5,10) 302 | self.Acceleration = Vector2.new(0,-500) 303 | 304 | -- "Fill": spawn randomly within the hook 305 | -- "Point": spawn at the center of the hook 306 | self.EmitterMode = "Point" 307 | 308 | 309 | 310 | self.__dead = false 311 | self.__elapsedTime = 0 312 | 313 | self.__runServiceConnection = game:GetService("RunService").Heartbeat:Connect(function(delta) 314 | 315 | 316 | self.__elapsedTime = self.__elapsedTime + delta 317 | for index, particle in ipairs(self.particles) do 318 | if particle.isDead then 319 | table.remove(self.particles, index) 320 | else 321 | particle:Update(delta) 322 | end 323 | end 324 | 325 | 326 | if self.Rate > 0 and (self.__dead == false) and self.Enabled then 327 | while self.__elapsedTime >= (1/self.Rate) do 328 | table.insert(self.particles, ParticleClass.new(self)) 329 | self.__elapsedTime = self.__elapsedTime - (1/self.Rate) 330 | end 331 | end 332 | end) 333 | 334 | return setmetatable(self, {__index = ParticleEmitterClass}) 335 | end 336 | 337 | 338 | function ParticleEmitterClass:Emit(count: number) 339 | local counter = 0 340 | while counter < count do 341 | counter += 1 342 | table.insert(self.particles, ParticleClass.new(self)) 343 | end 344 | 345 | end 346 | 347 | function ParticleEmitterClass:Destroy() 348 | 349 | if self.__dead then 350 | error('Cannot destroy dead particle emitter.') 351 | return 352 | end 353 | 354 | self.__dead = true 355 | for _,particle in ipairs(self.particles) do 356 | if particle then 357 | particle:Destroy() 358 | end 359 | end 360 | 361 | if self.__runServiceConnection then 362 | self.__runServiceConnection:Disconnect() 363 | end 364 | end 365 | 366 | return ParticleEmitterClass 367 | -------------------------------------------------------------------------------- /UIParticleMethodChaining.lua: -------------------------------------------------------------------------------- 1 | --!nocheck 2 | 3 | -- fork of nuttolum's 2D particle emitter but allows for method chaining. 4 | -- works by calling :Set(value) 5 | -- for example, this will set its transparency property and speed property: 6 | -- ParticleEmitterClass.new():SetTransparency(NumberRange.new(0,1)):SetSpeed(5) 7 | -- note this works only for ParticleEmitterClass, not ParticleClass because ParticleClass is usually not being accessed by the dev 8 | 9 | --custom type for the 2d number sequences 10 | export type NumberSequence2D = { 11 | X: NumberSequence, 12 | Y: NumberSequence 13 | } 14 | 15 | export type Particle = { 16 | Element: GuiObject, 17 | Speed: number, 18 | Position: Vector2, 19 | SpreadAngle: number, 20 | RotSpeed: number, 21 | Acceleration: Vector2, 22 | Size: NumberSequence2D, 23 | Transparency: NumberSequence, 24 | Color: ColorSequence, 25 | Age: number, 26 | Ticks: number, 27 | maxAge: number, 28 | isDead: boolean, 29 | Canvas: CanvasGroup, 30 | new: (Emitter: ParticleEmitter2D) -> (Particle), 31 | Update: (self: Particle, delta: number) -> (), 32 | Destroy: (self: Particle) -> () 33 | } 34 | 35 | local ParticleClass: Particle = {} 36 | ParticleClass.__index = ParticleClass 37 | 38 | local function Rotate(v: Vector2, degrees: number) 39 | local sin = math.sin(math.rad(degrees)); 40 | local cos = math.cos(math.rad(degrees)); 41 | 42 | local tx = v.x; 43 | local ty = v.y; 44 | return Vector2.new((cos * tx) - (sin * ty),(sin * tx) + (cos * ty)) 45 | end 46 | 47 | local function Normalize(min, max, alpha) 48 | return (alpha - min)/(max-min) 49 | end 50 | 51 | -- sequence evaluation functions taken from developer hub 52 | 53 | function evalCS(cs, t) 54 | -- If we are at 0 or 1, return the first or last value respectively 55 | if t == 0 then return cs.Keypoints[1].Value end 56 | if t == 1 then return cs.Keypoints[#cs.Keypoints].Value end 57 | -- Step through each sequential pair of keypoints and see if alpha 58 | -- lies between the points' time values. 59 | for i = 1, #cs.Keypoints - 1 do 60 | local this = cs.Keypoints[i] 61 | local next = cs.Keypoints[i + 1] 62 | if t >= this.Time and t < next.Time then 63 | -- Calculate how far alpha lies between the points 64 | local alpha = (t - this.Time) / (next.Time - this.Time) 65 | -- Evaluate the real value between the points using alpha 66 | return Color3.new( 67 | (next.Value.R - this.Value.R) * alpha + this.Value.R, 68 | (next.Value.G - this.Value.G) * alpha + this.Value.G, 69 | (next.Value.B - this.Value.B) * alpha + this.Value.B 70 | ) 71 | end 72 | end 73 | end 74 | 75 | local function evalNS(ns, t) 76 | -- If we are at 0 or 1, return the first or last value respectively 77 | if t == 0 then return ns.Keypoints[1].Value end 78 | if t == 1 then return ns.Keypoints[#ns.Keypoints].Value end 79 | -- Step through each sequential pair of keypoints and see if alpha 80 | -- lies between the points' time values. 81 | for i = 1, #ns.Keypoints - 1 do 82 | local this = ns.Keypoints[i] 83 | local next = ns.Keypoints[i + 1] 84 | if t >= this.Time and t < next.Time then 85 | -- Calculate how far alpha lies between the points 86 | local alpha = (t - this.Time) / (next.Time - this.Time) 87 | -- Evaluate the real value between the points using alpha 88 | return (next.Value - this.Value) * alpha + this.Value 89 | end 90 | end 91 | end 92 | 93 | function ParticleClass.new(emitter) 94 | local self = {} 95 | self.Element = emitter.Element:Clone() 96 | self.Color = emitter.Color 97 | self.StartSize = self.Element.AbsoluteSize 98 | self.Transparency = emitter.Transparency 99 | self.Canvas = Instance.new("CanvasGroup") 100 | self.Canvas.Parent = emitter.Hook:FindFirstAncestorWhichIsA("LayerCollector") 101 | self.Canvas.Size = UDim2.fromOffset(self.Element.AbsoluteSize.X, self.Element.AbsoluteSize.Y) 102 | self.Canvas.AnchorPoint = Vector2.new(0.5,0.5) 103 | self.Canvas.BackgroundTransparency = 1 104 | self.Canvas.ZIndex = emitter.Hook.ZIndex + emitter.ZOffset 105 | self.Canvas.GroupColor3 = evalCS(self.Color, 0) 106 | self.Canvas.GroupTransparency = evalNS(self.Transparency, 0) 107 | self.Element.Size = UDim2.fromScale(1,1) 108 | local spawnPosition 109 | if emitter.EmitterMode == "Point" then 110 | spawnPosition = emitter.Hook.AbsolutePosition 111 | local size = emitter.Hook.AbsoluteSize 112 | spawnPosition = Vector2.new(spawnPosition.X + size.X/2, spawnPosition.Y + size.Y/2) 113 | else 114 | spawnPosition = emitter.Hook.AbsolutePosition 115 | local size = emitter.Hook.AbsoluteSize 116 | spawnPosition = Vector2.new(spawnPosition.X + math.random(0, size.X), spawnPosition.Y + math.random(0, size.Y)) 117 | end 118 | 119 | self.Position = spawnPosition 120 | self.Canvas.Position = UDim2.new( 121 | UDim.new(0, self.Position.X), 122 | UDim.new(0, self.Position.Y) 123 | ) 124 | self.Element.AnchorPoint = Vector2.new(0.5,0.5) 125 | self.Element.Position = UDim2.fromScale(0.5,0.5) 126 | self.Element.Parent = self.Canvas 127 | self.Size = emitter.Size 128 | emitter.preSpawn(self.Element) 129 | self.Speed = Vector2.new( 130 | math.random(emitter.xSpeed.Min, emitter.xSpeed.Max), 131 | math.random(emitter.ySpeed.Min, emitter.ySpeed.Max) 132 | ) 133 | 134 | 135 | self.SpreadAngle = math.random(emitter.SpreadAngle.Min, emitter.SpreadAngle.Max) 136 | self.RotSpeed = math.random(emitter.RotSpeed.Min, emitter.RotSpeed.Max) 137 | 138 | --unrotate the acceleration since the speed will be rotated 139 | self.Acceleration = Rotate(emitter.Acceleration, -self.SpreadAngle) 140 | 141 | self.Transparency = emitter.Transparency 142 | self.Age = 0 143 | self.Ticks = 0 144 | self.maxAge = math.random(emitter.Lifetime.Min, emitter.Lifetime.Max) 145 | self.isDead = false 146 | 147 | return setmetatable(self, ParticleClass) 148 | end 149 | 150 | 151 | 152 | function ParticleClass:Update(delta) 153 | 154 | if self.Age >= self.maxAge and self.maxAge > 0 then 155 | self:Destroy() 156 | return 157 | end 158 | 159 | 160 | self.Ticks = self.Ticks + 1 161 | self.Age = self.Age + delta 162 | local xSize, ySize = evalNS(self.Size.X, Normalize(0, self.maxAge, self.Age)), evalNS(self.Size.Y, Normalize(0, self.maxAge, self.Age)) 163 | if xSize and ySize then 164 | self.Canvas.Size = UDim2.new( 165 | UDim.new(0, self.StartSize.X * xSize), 166 | UDim.new(0, self.StartSize.Y *ySize) 167 | ) 168 | end 169 | local nextColor = evalCS(self.Color, Normalize(0, self.maxAge, self.Age)) 170 | if nextColor then 171 | self.Canvas.GroupColor3 = nextColor 172 | end 173 | self.Canvas.GroupTransparency = evalNS(self.Transparency, Normalize(0, self.maxAge, self.Age)) 174 | 175 | 176 | local dir = Rotate(self.Speed, self.SpreadAngle) * Vector2.new(1,-1) 177 | self.Speed += (self.Acceleration * delta) 178 | 179 | self.Position += (dir * delta) 180 | self.Canvas.Position = UDim2.new( 181 | UDim.new(0, self.Position.X), 182 | UDim.new(0, self.Position.Y) 183 | ) 184 | 185 | 186 | 187 | self.Canvas.Rotation += self.RotSpeed * delta 188 | 189 | end 190 | 191 | function ParticleClass:Destroy() 192 | self.isDead = true 193 | self.Canvas:Destroy() 194 | end 195 | 196 | 197 | export type ParticleEmitter2D = { 198 | particles: {Particle}, 199 | Enabled: boolean, 200 | Element: GuiObject, 201 | Hook: GuiObject, 202 | preSpawn: any, 203 | Rate: number, 204 | Color: ColorSequence, 205 | Size: NumberSequence2D, 206 | Transparency: NumberSequence, 207 | ZOffset: number, 208 | xSpeed: NumberRange, 209 | ySpeed: NumberRange, 210 | SpreadAngle: NumberRange, 211 | RotSpeed: NumberRange, 212 | Lifetime: NumberRange, 213 | Acceleration: Vector2, 214 | EmitterMode: (string: "Point") | (string: "Fill"), 215 | __dead: boolean, 216 | __elapsedTime: number, 217 | __runServiceConnection: RBXScriptConnection, 218 | new: (Hook: GuiObject, Element: GuiObject) -> (ParticleEmitter2D), 219 | fromEmitter3D: (Hook: GuiObject, Emitter: ParticleEmitter, unitMultiplier: number?) -> (ParticleEmitter2D), 220 | Emit: (self: ParticleEmitter2D, count: number) -> (), 221 | Destroy: (self: ParticleEmitter2D) -> () 222 | } 223 | 224 | local ParticleEmitterClass: ParticleEmitter2D = {} 225 | function ParticleEmitterClass:__index(key: string) 226 | return ParticleEmitterClass[key] or function(_, value) 227 | self[key:gsub('Set', '')] = value 228 | return self 229 | end 230 | end 231 | 232 | function ParticleEmitterClass.fromEmitter3D(hook: GuiObject, emitter: ParticleEmitter, unitMultiplier: number?) 233 | local self = {} 234 | unitMultiplier = unitMultiplier or 1 235 | self.particles = {} 236 | self.Enabled = false 237 | self.Element = Instance.new("ImageLabel") 238 | self.Element.Size = UDim2.new(0,unitMultiplier,0,unitMultiplier) 239 | self.Element.Image = emitter.Texture 240 | self.Element.BackgroundTransparency = 1 241 | self.Element.Parent = game.ReplicatedStorage 242 | self.Hook = hook 243 | 244 | self.preSpawn = function(p) end 245 | 246 | --properties 247 | self.Rate = emitter.Rate 248 | self.Color = emitter.Color 249 | self.Size = {X = emitter.Size, Y = emitter.Size} 250 | self.Transparency = emitter.Transparency 251 | self.ZOffset = emitter.ZOffset 252 | self.xSpeed = NumberRange.new(0,0) 253 | self.ySpeed = NumberRange.new(emitter.Speed.Min * unitMultiplier, emitter.Speed.Max * unitMultiplier) 254 | self.SpreadAngle = NumberRange.new(emitter.SpreadAngle.X, emitter.SpreadAngle.Y) 255 | self.RotSpeed = emitter.RotSpeed 256 | self.Lifetime = emitter.Lifetime 257 | self.Acceleration = Vector2.new(emitter.Acceleration.X * unitMultiplier, emitter.Acceleration.Y * unitMultiplier) 258 | 259 | self.EmitterMode = emitter.ShapeStyle == Enum.ParticleEmitterShapeStyle.Volume and "Fill" or "Point" 260 | 261 | 262 | 263 | self.__dead = false 264 | self.__elapsedTime = 0 265 | 266 | self.__runServiceConnection = game:GetService("RunService").Heartbeat:Connect(function(delta) 267 | 268 | 269 | self.__elapsedTime = self.__elapsedTime + delta 270 | for index, particle in ipairs(self.particles) do 271 | if particle.isDead then 272 | table.remove(self.particles, index) 273 | else 274 | particle:Update(delta) 275 | end 276 | end 277 | 278 | 279 | if self.Rate > 0 and (self.__dead == false) and self.Enabled then 280 | while self.__elapsedTime >= (1/self.Rate) do 281 | table.insert(self.particles, ParticleClass.new(self)) 282 | self.__elapsedTime = self.__elapsedTime - (1/self.Rate) 283 | end 284 | end 285 | end) 286 | 287 | return setmetatable(self, ParticleEmitterClass) 288 | end 289 | 290 | function ParticleEmitterClass.new(hook: GuiObject, particleElement: GuiObject) 291 | local self = {} 292 | 293 | self.particles = {} 294 | self.Enabled = false 295 | self.Element = particleElement 296 | self.Hook = hook 297 | 298 | self.preSpawn = function(p) end 299 | 300 | --properties 301 | self.Rate = 20 302 | self.Color = ColorSequence.new(Color3.new(1,1,1)) 303 | self.Size = {X = NumberSequence.new(1), Y = NumberSequence.new(1)} 304 | self.Transparency = NumberSequence.new(0) 305 | self.ZOffset = 0 306 | self.xSpeed = NumberRange.new(0,0) 307 | self.ySpeed = NumberRange.new(150,500) 308 | self.SpreadAngle = NumberRange.new(-15,15) 309 | self.RotSpeed = NumberRange.new(0) 310 | self.Lifetime = NumberRange.new(5,10) 311 | self.Acceleration = Vector2.new(0,-500) 312 | 313 | -- "Fill": spawn randomly within the hook 314 | -- "Point": spawn at the center of the hook 315 | self.EmitterMode = "Point" 316 | 317 | 318 | 319 | self.__dead = false 320 | self.__elapsedTime = 0 321 | 322 | self.__runServiceConnection = game:GetService("RunService").Heartbeat:Connect(function(delta) 323 | 324 | 325 | self.__elapsedTime = self.__elapsedTime + delta 326 | for index, particle in ipairs(self.particles) do 327 | if particle.isDead then 328 | table.remove(self.particles, index) 329 | else 330 | particle:Update(delta) 331 | end 332 | end 333 | 334 | 335 | if self.Rate > 0 and (self.__dead == false) and self.Enabled then 336 | while self.__elapsedTime >= (1/self.Rate) do 337 | table.insert(self.particles, ParticleClass.new(self)) 338 | self.__elapsedTime = self.__elapsedTime - (1/self.Rate) 339 | end 340 | end 341 | end) 342 | 343 | return setmetatable(self, ParticleEmitterClass) 344 | end 345 | 346 | 347 | function ParticleEmitterClass:Emit(count: number) 348 | local counter = 0 349 | while counter < count do 350 | counter += 1 351 | table.insert(self.particles, ParticleClass.new(self)) 352 | end 353 | 354 | end 355 | 356 | function ParticleEmitterClass:Destroy() 357 | 358 | if self.__dead then 359 | error('Cannot destroy dead particle emitter.') 360 | return 361 | end 362 | 363 | self.__dead = true 364 | for _,particle in ipairs(self.particles) do 365 | if particle then 366 | particle:Destroy() 367 | end 368 | end 369 | 370 | if self.__runServiceConnection then 371 | self.__runServiceConnection:Disconnect() 372 | end 373 | end 374 | 375 | return ParticleEmitterClass 376 | --------------------------------------------------------------------------------