Particle system

Started by wizard_wizzle (aka ZeldaHistorian), June 07, 2014, 07:55:06 AM

Previous topic - Next topic
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.