I've been playing around with the idea of creating a particle system in pure Lua that can run on the Solarus engine. Unfortunately, I'm pretty new to Lua and the draft that I'm working on now doesn't appear to be working. I think the idea behind it is sound, but I can't get it to display correctly so I can test it!
particles.lua (based off of a Love engine particle system, found at https://github.com/SimonLarsen/sienna/blob/master/particles.lua)
local particle_system = {}
function particle_system:new(game)
local object = {}
setmetatable(object, self)
self.__index = self
object:initialize(game)
return object
end
function particle_system:initialize(game)
self.game = game
if not self.time then self.time = 1 end
if not self.count then self.count = 5 end
if not self.color then self.color = {255, 255, 255} end
if self.type == "sparkle" then
self.alive = true
self.particles = {}
for i=1, self.count do
self.particles[i] = {}
self.particles[i].x = x
self.particles[i].xspeed = math.random(-100,100)
self.particles[i].y = y
self.particles[i].yspeed = math.random(-200,50) + (self.ysp or 0)
end
return self
elseif self.type == "dust" then
self.alive = true
self.time = 0
self.x = x
self.y = y
return self
end
end
function particle_system:on_update()
self.time = self.time - 1
if self.type == "sparkle" then
if self.time < 0 then
self.alive = false
return
end
for i,v in ipairs(self.particles) do
v.x = v.x + v.xspeed*dt
v.yspeed = v.yspeed + 500*dt
v.y = v.y + v.yspeed*dt
end
elseif self.type == "dust" then
if self.time > 0.25 then
self.alive = false
return
end
end
end
function particle_system:on_draw(dst_surface)
if self.type == "sparkle" then
for i,v in ipairs(self.particles) do
dst_surface:fill_color(self.color, 0.5+v.x, 0.5+v.y, 1, 1)
end
elseif self.type == "dust" then
dst_surface:fill_color(self.color, self.x-self.time*16, self.y-self.time*16, 1, 1)
dst_surface:fill_color(self.color, self.x+self.time*16, self.y-self.time*16, 1, 1)
dst_surface:fill_color(self.color, self.x-self.time*16, self.y+self.time*16, 1, 1)
dst_surface:fill_color(self.color, self.x+self.time*16, self.y+self.time*16, 1, 1)
else
dst_surface:clear()
end
end
function particle_system:set_type(type)
self.type = type
end
function particle_system:set_position(x, y, layer)
self.x = x
self.y = y
self.layer = layer or 1
end
function particle_system:set_particle_count(count)
self.count = count
end
function particle_system:set_particle_color(color)
self.color = color
end
function particle_system:set_decay_time(time)
self.time = time
end
function particle_system:set_y_speed(ysp)
self.ysp = ysp
end
return particle_system
An example map script:
local map = ...
local game = map:get_game()
local particle_system = require("particles")
function sensor:on_activated()
local emitter = particle_system:new(game)
emitter:set_type("sparkle")
emitter:set_position(100, 100)
end
function map:on_update()
if emitter ~= nil then emitter:on_update() end
end
function map:on_draw(dst_surface)
if emitter ~= nil then emitter:on_draw(dst_surface) end
end
Any thoughts? Is this a lost cause or am I just way off base?
The method looks good to me: make a separate file, call it with require, forward on_draw() calls to it.
There is a first error that explains why nothing happens: in your map script, the emitter variable is a local to the sensor:on_activated() function. So it is always nil in map:on_update() and map:on_draw(). Simply declare it local to the map script instead of local to a function.
Then, using on_update() is probably a bad idea because you don't know how often the engine calls map:on_update(). It will work, but the recommended way is to use a timer. With a timer you can control the delay. And since Solarus 1.2, you can repeat a timer by returning true from its callback.
Thanks for the feedback, Christopho!
New particles.lua:
local particle_system = {}
function particle_system:new(game)
local object = {}
setmetatable(object, self)
self.__index = self
return object
end
function particle_system:initialize(game)
self.game = game
if not self.type then
print("Aborting. Call 'particle_system:set_type' before initializing.")
return false
end
if not self.time then self.time = 1 end
if not self.count then self.count = 5 end
if not self.color then self.color = {255, 255, 255} end
self.particles = {}
if self.type == "sparkle" then
self.alive = true
for i=1, self.count do
self.particles[i] = {}
self.particles[i].x = self.x
self.particles[i].xspeed = math.random(-100,100)
self.particles[i].y = self.y
self.particles[i].yspeed = math.random(-200,50) + (self.ysp or 0)
end
return self
elseif self.type == "dust" then
self.alive = true
if not self.time then self.time = 0 end
return self
end
end
function particle_system:set_type(type)
self.type = type
end
function particle_system:set_position(x, y, layer)
self.x = x
self.y = y
self.layer = layer or 1
end
function particle_system:set_particle_count(count)
self.count = count
end
function particle_system:set_particle_color(color)
self.color = color
end
function particle_system:set_decay_time(time)
self.time = time
end
function particle_system:set_y_speed(ysp)
self.ysp = ysp
end
function particle_system:is_active()
return self.alive
end
function particle_system:on_update(dt)
self.time = self.time - dt
if self.type == "sparkle" then
if self.time < 0 then
self.alive = false
return
end
for i,v in ipairs(self.particles) do
v.x = v.x + v.xspeed*dt
v.yspeed = v.yspeed + 500*dt
v.y = v.y + v.yspeed*dt
end
elseif self.type == "dust" then
if self.time > 0.25 then
self.alive = false
return
end
end
end
function particle_system:on_draw(dst_surface)
if self.type == "sparkle" then
for i,v in ipairs(self.particles) do
dst_surface:fill_color(self.color, 0.5+v.x, 0.5+v.y, 1, 1)
end
elseif self.type == "dust" then
dst_surface:fill_color(self.color, self.x-self.time*16, self.y-self.time*16, 1, 1)
dst_surface:fill_color(self.color, self.x+self.time*16, self.y-self.time*16, 1, 1)
dst_surface:fill_color(self.color, self.x-self.time*16, self.y+self.time*16, 1, 1)
dst_surface:fill_color(self.color, self.x+self.time*16, self.y+self.time*16, 1, 1)
else
--dst_surface:clear()
end
end
return particle_system
New map script:
local map = ...
local game = map:get_game()
local particle_system = require("particles")
local emitter = particle_system:new(game)
function sensor:on_activated()
emitter:set_type("sparkle")
emitter:set_position(100, 100)
emitter:set_particle_count(50)
emitter:set_decay_time(10)
emitter:initialize()
sol.timer.start(map, 1000, function()
if emitter ~= nil then emitter:on_update(1) end
return true
end)
end
function map:on_draw(dst_surface)
if emitter ~= nil then emitter:on_draw(dst_surface) end
end
The script now operates without error and as expected, but the display is not correct. I usually only get one particle on screen and it disappears without moving. It probably has to do with the drawing method that I used (dst_surface:fill_color) since I don't know if it allows for multiple fill_color calls on one surface. If all else fails, I can re-write it to use sprites, I just thought this method would be faster.
I suggest to add print() instructions to debug your variables. Apparently, the y value of your particles is to high, so they all end up outside the screen, too much to the south.
Okay, the display issue has been corrected and the "sparkle" (renamed to just spark) particle emitter now works! I've also added alpha fade to these particles, so they start to disappear as they age. If anyone could come up with or point me to other examples of particle emitter mathematics, I will add them, but I'm not very good at this kind of thing. I'd like to get a few more emitter types built in and then post this in the "Your Projects" board for others to use.