Usage:
Code: Select all
function love.load()
ParticleSystem = require('ParticleSystem')
ps = ParticleSystem.newParticleSystem(texture)
ps:setEmissionRate(10)
ps:setParticleLifetime(1)
end
function love.update(dt)
ps:update(dt)
end
function love.draw()
ps:draw()
end
Permission is given for doing anything you want with this file.
Code: Select all
local MAX_PARTICLES = math.huge
local rng = love.math.newRandomGenerator(os.time())
local P = {}
P.__index = P
function P.newParticleSystem(texture, size)
local ps = {}
setmetatable(ps, P)
ps.texture = texture
ps.active = true
ps.insertMode = 'top'
ps.maxParticles = size
ps.activeParticles = 0
ps.emissionRate = 1
ps.emitCounter = 0
ps.areaSpreadDistribution = 'none'
ps.lifetime = -1
ps.life = 0
ps.particleLifeMin = 0
ps.particleLifeMax = 0
ps.direction = 0
ps.spread = 0
ps.speedMin = 0
ps.speedMax = 0
ps.linearAccelerationMinX = 0
ps.linearAccelerationMinY = 0
ps.linearAccelerationMaxX = 0
ps.linearAccelerationMaxY = 0
ps.radialAccelerationMin = 0
ps.radialAccelerationMax = 0
ps.tangentialAccelerationMin = 0
ps.tangentialAccelerationMax = 0
ps.linearDampingMin = 0
ps.linearDampingMax = 0
ps.sizeVariation = 0
ps.rotationMin = 0
ps.rotationMax = 0
ps.spinStart = 0
ps.spinEnd = 0
ps.spinVariation = 0
ps.offsetX = texture:getWidth() * 0.5
ps.offsetY = texture:getHeight() * 0.5
ps.defaultOffset = true
ps.relativeRotation = false
if (size == 0 or size > MAX_PARTICLES) then
error("Invalid ParticleSystem size.")
end
ps.sizes = {1}
ps.colors = {{r = 1, g = 1, b = 1, a = 1}}
ps.positionX = 0
ps.positionY = 0
ps.prevPositionX = 0
ps.prevPositionY = 0
ps.areaSpreadX = 0
ps.areaSpreadY = 0
ps.quads = {}
ps.spritebatch = love.graphics.newSpriteBatch(texture, size, 'stream')
ps.particles = {}
return ps
end
function P:draw(x, y, r, sx, sy, ox, oy, kx, ky)
self.spritebatch:clear()
for i = 1, #self.particles do
local p = self.particles[i]
self.spritebatch:setColor(p.color.r * 255, p.color.g * 255, p.color.b * 255, p.color.a * 255)
if #self.quads == 0 then
self.spritebatch:add(p.positionX, p.positionY, p.rotation, p.size, p.size, self.offsetX, self.offsetY)
else
self.spritebatch:add(self.quads[p.quadIndex], p.positionX, p.positionY, p.rotation, p.size, p.size, self.offsetX, self.offsetY)
end
end
love.graphics.draw(self.spritebatch, x, y, r, sx, sy, ox, oy, kx, ky)
end
function P:setPosition(x, y)
self.positionX = x
self.positionY = y
self.prevPositionX = x
self.prevPositionY = y
end
function P:setSpeed(min, max)
self.speedMin = min
if max == nil then
self.speedMax = min
else
self.speedMax = max
end
end
function P:addParticle(t)
if self:isFull() then
return
end
local p = {}
self:initParticle(p, t)
if self.insertMode == 'top' then
self:insertTop(p)
elseif self.insertMode == 'bottom' then
self:insertBottom(p)
elseif self.insertMode == 'random' then
self:insertRandom(p)
end
self.activeParticles = self.activeParticles + 1
end
function P:setParticleLifetime(min, max)
self.particleLifeMin = min
if max == nil then
self.particleLifeMax = min
else
self.particleLifeMax = max
end
end
function P:setEmissionRate(rate)
if rate < 0 then
error("Invalid emission rate")
end
self.emissionRate = rate
end
function P:initParticle(p, t)
local posX = self.prevPositionX + (self.positionX - self.prevPositionX) * t
local posY = self.prevPositionY + (self.positionY - self.prevPositionY) * t
p.positionX = posX
p.positionY = posY
local function random(min, max)
return min + rng:random() * (max - min)
end
p.life = random(self.particleLifeMin, self.particleLifeMax)
p.lifetime = p.life
if self.areaSpreadDistribution == 'uniform' then
p.positionX = p.positionX + random(-self.areaSpreadX, self.areaSpreadX)
p.positionY = p.positionY + random(-self.areaSpreadY, self.areaSpreadY)
elseif self.areaSpreadDistribution == 'normal' then
p.positionX = p.positionX + rng:randomNormal(self.areaSpreadX)
p.positionY = p.positionY + rng:randomNormal(self.areaSpreadY)
end
p.originX = posX
p.originY = posY
local dir = self.direction - self.spread/2 + rng:random() * self.spread
local speed = random(self.speedMin, self.speedMax)
p.velocityX = math.cos(dir) * speed
p.velocityY = math.sin(dir) * speed
p.linearAccelerationX = random(self.linearAccelerationMinX, self.linearAccelerationMaxX)
p.linearAccelerationY = random(self.linearAccelerationMinY, self.linearAccelerationMaxY)
p.radialAcceleration = random(self.radialAccelerationMin, self.radialAccelerationMax)
p.tangentialAcceleration = random(self.tangentialAccelerationMin, self.tangentialAccelerationMax)
p.linearDamping = random(self.linearDampingMin, self.linearDampingMax)
p.rotation = random(self.rotationMin, self.rotationMax)
p.sizeOffset = random(1, self.sizeVariation)
p.sizeIntervalSize = (1 - random(1, self.sizeVariation)) - p.sizeOffset
p.size = self.sizes[math.floor((p.sizeOffset - 0.5) * (#self.sizes - 1) + 1)]
local function calculate_variation(inner, outer, var)
local low = inner - (outer/2)*var
local high = inner + (outer/2)*var
local r = rng:random()
return low*(1-r)+high*r
end
p.spinStart = calculate_variation(self.spinStart, self.spinEnd, self.spinVariation)
p.spinEnd = calculate_variation(self.spinEnd, self.spinStart, self.spinVariation)
p.angle = p.rotation
if self.relativeRotation then
p.angle = p.angle + math.atan2(p.velocity.y, p.velocity.x)
end
p.color = {
r = self.colors[1].r,
g = self.colors[1].g,
b = self.colors[1].b,
a = self.colors[1].a
}
p.quadIndex = 1
end
function P:insertTop(p)
table.insert(self.particles, p)
end
function P:insertBottom(p)
table.insert(self.particles, 1, p)
end
function P:insertRandom(p)
local pos = rng:random(self.activeParticles)
table.insert(self.particles, pos, p)
end
function P:isFull()
return self.activeParticles == self.maxParticles
end
function P:update(dt)
if dt == 0 then
return
end
for particleIndex = #self.particles, 1, -1 do
p = self.particles[particleIndex]
p.life = p.life - dt
if p.life <= 0 then
table.remove(self.particles, particleIndex)
self.activeParticles = self.activeParticles - 1
else
local radialX
local radialY
local tangentialX
local tangentialY
local pposX = p.positionX
local pposY = p.positionY
radialX = pposX - p.originX
radialY = pposY - p.originY
local l = math.sqrt(radialX * radialX + radialY * radialY)
if l > 0 then
radialX, radialY = radialX / l, radialY / l
end
tangentialX = radialX
tangentialY = radialY
radialX = radialX * p.radialAcceleration
radialY = radialY * p.radialAcceleration
tangentialX, tangentialY = -tangentialY, tangentialX
tangentialX = tangentialX * p.tangentialAcceleration
tangentialY = tangentialY * p.tangentialAcceleration
p.velocityX = p.velocityX + (radialX + tangentialX + p.linearAccelerationX) * dt
p.velocityY = p.velocityY + (radialY + tangentialY + p.linearAccelerationY) * dt
p.velocityX = p.velocityX * 1 / (1 + p.linearDamping * dt)
p.velocityY = p.velocityY * 1 / (1 + p.linearDamping * dt)
pposX = pposX + p.velocityX * dt
pposY = pposY + p.velocityY * dt
p.positionX = pposX
p.positionY = pposY
local t = 1 - p.life / p.lifetime
p.rotation = p.rotation + (p.spinStart * (1 - t) + p.spinEnd * t) * dt
p.angle = p.rotation
if self.relativeRotation then
p.angle = p.angle + math.atan2(p.velocity.y, p.velocity.x)
end
local n = #self.sizes - 1
local s = (t * n) - math.floor(t * n)
local i = math.floor(t * n)
local k
if i == n then
k = i
else
k = i + 1
end
p.size = self.sizes[i + 1] * (1 - s) + self.sizes[k + 1] * s
local n = #self.colors - 1
local s = (t * n) - math.floor(t * n)
local i = math.floor(t * n)
local k
if i == n then
k = i
else
k = i + 1
end
p.color.r = self.colors[i + 1].r * (1 - s) + self.colors[k + 1].r * s
p.color.g = self.colors[i + 1].g * (1 - s) + self.colors[k + 1].g * s
p.color.b = self.colors[i + 1].b * (1 - s) + self.colors[k + 1].b * s
p.color.a = self.colors[i + 1].a * (1 - s) + self.colors[k + 1].a * s
local n = #self.quads
local s = (t * n) - math.floor(t * n)
local i = math.floor(t * n)
p.quadIndex = i + 1
end
end
if self.active then
local rate = 1 / self.emissionRate
self.emitCounter = self.emitCounter + dt
local total = self.emitCounter - rate
while (self.emitCounter > rate) do
self:addParticle(1 - (self.emitCounter - rate) / total)
self.emitCounter = self.emitCounter - rate
end
self.life = self.life - dt
if self.lifetime ~= -1 and self.life < 0 then
self:stop()
end
end
self.prevPositionX = self.positionX
self.prevPositionY = self.positionY
end
function P:resetOffset()
if #self.quads == 0 then
self.offsetX = self.texture:getWidth()*0.5
self.offsetY = self.texture:getHeight()*0.5
else
local x, y = self.quads[1]:getViewport()
self.offsetX = x*0.5
self.offsetY = y*0.5
end
end
function P:setBufferSize(size)
if size == 0 or size > MAX_PARTICLES then
error("Invalid buffer size")
end
self.spritebatch = love.graphics.newSpriteBatch(self.texture, size, 'stream')
self.maxParticles = size
self:reset()
end
function P:getBufferSize()
return self.maxParticles
end
function P:setTexture(tex)
self.texture = tex
if self.defaultOffset then
self:resetOffset()
end
end
function P:getTexture()
return self.texture
end
function P:setInsertMode(mode)
self.insertMode = mode
end
function P:getInsertMode()
return self.insertMode
end
function P:getEmissionRate()
return self.emissionRate
end
function P:setEmitterLifetime(life)
self.life = life
self.lifetime = life
end
function P:getEmitterLifetime()
return self.lifetime
end
function P:getParticleLifetime()
return self.particleLifeMin, self.particleLifeMax
end
function P:getPosition()
return self.position
end
function P:moveTo(x, y)
self.positionX = x
self.positionY = y
end
function P:setAreaSpread(distribution, x, y)
self.areaSpreadX = x
self.areaSpreadY = y
self.areaSpreadDistribution = distribution
end
function P:getAreaSpreadDistribution()
return self.areaSpreadDistribution
end
function P:getAreaSpreadParameters()
return self.areaSpreadX, self.areaSpreadY
end
function P:setDirection(direction)
self.direction = direction
end
function P:getDirection()
return self.direction
end
function P:setSpread(spread)
self.spread = spread
end
function P:getSpread()
return self.spread
end
function P:getSpeed()
return self.speedMin, self.speedMax
end
function P:setLinearAcceleration(xmin, ymin, xmax, ymax)
if xmax == nil and ymax == nil then
self.linearAccelerationMinX = xmin
self.linearAccelerationMinY = ymin
self.linearAccelerationMaxX = xmin
self.linearAccelerationMaxY = ymin
else
self.linearAccelerationMinX = xmin
self.linearAccelerationMinY = ymin
self.linearAccelerationMaxX = xmax
self.linearAccelerationMaxY = ymax
end
end
function P:getLinearAcceleration()
return self.linearAccelerationMin, self.linearAccelerationMax
end
function P:setRadialAcceleration(min, max)
if max == nil then
self.radialAccelerationMin = min
self.radialAccelerationMax = min
else
self.radialAccelerationMin = min
self.radialAccelerationMax = max
end
end
function P:getRadialAcceleration()
return self.radialAccelerationMin, self.radialAccelerationMax
end
function P:setTangentialAcceleration(min, max)
if max == nil then
self.tangentialAccelerationMin = min
self.tangentialAccelerationMax = min
else
self.tangentialAccelerationMin = min
self.tangentialAccelerationMax = max
end
end
function P:getTangentialAcceleration()
return self.tangentialAccelerationMin, self.tangentialAccelerationMax
end
function P:setLinearDamping(min, max)
if max == nil then
self.linearDampingMin = min
self.linearDampingMax = min
else
self.linearDampingMin = min
self.linearDampingMax = max
end
end
function P:setSizes(...)
self.sizes = {...}
end
function P:getSizes()
return self.sizes
end
function P:setSizeVariation(variation)
self.sizeVariation = variation
end
function P:getSizeVariation()
return self.sizeVariation
end
function P:setRotation(min, max)
if max == nil then
self.rotationMin = min
self.rotationMax = min
else
self.rotationMin = min
self.rotationMax = max
end
end
function P:setSpin(start, end_)
if end_ == nil then
self.spinStart = start
self.spinEnd = start
else
self.spinStart = start
self.spinEnd = end_
end
end
function P:getSpin()
return self.spinStart, self.spinEnd
end
function P:setSpinVariation(variation)
self.spinVariation = variation
end
function P:getSpinVariation()
return self.spinVariation
end
function P:setOffset(x, y)
self.offsetX = x
self.offsetY = y
self.defaultOffset = false
end
function P:getOffset()
return self.offsetX, self.offsetY
end
function P:setColors(...)
local args = {...}
if type(args[1]) == 'table' then
local t = args[1]
local nColors = #t
if nColors > 8 then
error("At most eight (8) colors may be used.")
end
self.colors = {}
for i = 1, nColors do
self.colors[i] = {
r = args[t[i][1]] / 255,
g = args[t[i][2]] / 255,
b = args[t[i][3]] / 255,
a = args[t[i][4] or 255] / 255
}
end
else
local cargs = #args
local nColors = math.floor((cargs + 3) / 4)
if cargs ~= 3 and (cargs % 4 ~= 0 or cargs == 0) then
error("Expected red, green, blue, and alpha. Only got "..(cargs % 4).." of 4 components.")
end
if (nColors > 8) then
error(L, "At most eight (8) colors may be used.")
end
self.colors = {}
for i = 1, nColors do
self.colors[i] = {
r = args[(i - 1) * 4 + 1] / 255,
g = args[(i - 1) * 4 + 2] / 255,
b = args[(i - 1) * 4 + 3] / 255,
a = args[(i - 1) * 4 + 4] / 255
}
end
end
end
function P:getColors()
local result = {}
for i = 1, #self.colors do
local c = self.colors[i]
result[i] = {
r = c.r * 255,
g = c.g * 255,
b = c.b * 255,
a = c.a * 255,
}
end
return result
end
function P:setQuads(...)
self.quads = {...}
if self.defaultOffset then
self:resetOffset()
end
end
function P:getQuads()
return unpack(self.quads)
end
function P:setRelativeRotation(enable)
self.relativeRotation = enable
end
function P:hasRelativeRotation()
return self.relativeRotation
end
function P:getCount()
return self.activeParticles
end
function P:start()
self.active = true
end
function P:stop()
self.active = false
self.life = self.lifetime
self.emitCounter = 0
end
function P:pause()
self.active = false
end
function P:reset()
self.particles = {}
self.activeParticles = 0
self.life = self.lifetime
self.emitCounter = 0
end
function P:emit(num)
if not self.active then
return
end
local num = math.min(num, self.maxParticles - self.activeParticles)
for i = 1, num do
self:addParticle(1)
end
end
function P:isActive()
return self.active
end
function P:isPaused()
return not self.active and self.life < self.lifetime
end
function P:isStopped()
return not self.active and self.life >= self.lifetime
end
function P:isEmpty()
return self.activeParticles == 0
end
return P