ParticleSystem in Lua
Posted: Sun Mar 13, 2016 9:20 am
This is LOVE's ParticleSystem implemented in Lua, basically a port of this file.
Usage:
Please note that it hasn't been properly tested and is probably full of bugs and it hasn't been optimized!
Permission is given for doing anything you want with this file.
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