ParticleSystem in Lua

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
Santos
Party member
Posts: 383
Joined: Sat Oct 22, 2011 7:37 am

ParticleSystem in Lua

Post by Santos » Sun Mar 13, 2016 9:20 am

This is LOVE's ParticleSystem implemented in Lua, basically a port of 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
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.

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
Attachments
ParticleSystem.lua
(16.62 KiB) Downloaded 62 times
Last edited by Santos on Mon Sep 25, 2017 2:49 pm, edited 1 time in total.

User avatar
ivan
Party member
Posts: 1619
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: ParticleSystem in Lua

Post by ivan » Sun Mar 13, 2016 10:04 am

One thing I would suggest, is to allocate the particles ahead of time.
So for example, you can have a table of about 2000-3000 "inactive" particles,
and whenever a new particle is created you move it from the "inactive" table to the "active" table.
When a particle is destroyed you just move it back to the "inactive" table.
This way you're not allocating tables all the time.

Post Reply

Who is online

Users browsing this forum: No registered users and 27 guests