[XP][VX][VXA] Particle Emissor

Started by orochii, August 14, 2018, 05:28:10 pm

Previous topic - Next topic

orochii

August 14, 2018, 05:28:10 pm Last Edit: August 15, 2018, 07:47:01 pm by orochii
Particle Emissor
Authors: Orochii Zouveleki
Version: 0.5a
Type: Visual Object Class
Key Term: Misc Add-on



Introduction

Sometimes referred to as particle system, this is more like an utility for now but it's a class to be used in order to add particle effects to your game. It includes a lot of properties for your particles and I think it's pretty capable of doing some stuff. Might be kind of hard to use though, especially since I haven't made a proper easy integration with, say, maps and battles. But a capable scripter should be able to gie this thing a good use.


Features


  • Make a particle emitter

  • Customize particle emitter shape and emission properties (interval, particles per second, etc).

  • Support for both constant emission and particle bursts (bursts can also use additional images).

  • Properties for particles, such as size, speed, acceleration, rotation, color... Each of these can vary through the particle lifespan.




Screenshots




Demo

Particles.7z (~200Kb)
Particles.rb


Script

Spoiler: ShowHide

=begin
================================================================================
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
OZ Particle Emitter - Versión 0.5a
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Testeado en RGSS1
Autor: Orochii Zouveleki

Documentación:
''''''''''''''
module OZMath
  Este módulo incluye algunas operaciones comunes.

  def self.deg2rad(degrees) - Convierte grados a radianes.
Retorno: Numeric.
  def self.lerp(v,a,b) - Interpolación lineal entre números a y b
de acuerdo a v (0..1).
Retorno: Numeric.
  def self.clamp(v,min,max) - Restringe un valor v a un intérvalo [min,max]
Retorno: Numeric.
  def self.rand_range(a,b) - Número aleatorio entre a y b.
Retorno: Numeric.
  def self.rand_range_i(a,b) - Número aleatorio entero entre a y b.
Retorno: Numeric.
  def self.lerp_col(v, c1, c2) - Interpolación lineal entre Color c1 y c2
de acuerdo a v.
  Retorno: Color.

class FreeRange
Esta clase es una versión inútil de Range, con el objeto de soportar
valores de punto flotante.
Simplemente necesitaba algo que lo guardara, y no fuera Array. ¯\_(ツ)_/¯

  def initialize(first,last) -Inicializa objeto
  attr_reader :first -Valor inicial
  attr_reader :last -Valor final

class Particle < Sprite
  attr_reader :dead
  def initialize(bitmap,emissor,x,y,viewport=nil)
-Inicialización partícula
  def update -Lógica de partícula
  def get_property(p,modifier=nil,k=:number)
-Obtiene valor real de propiedad
  Retorno: Numeric, Array de Numeric o Color
  def get_max(p, k=:number) -
  Retorno: Numeric o Array de Numeric
  def get_modifier(mod)
    Retorno: Numeric 0..1
  # Métodos de utilidad internos
  def iter_modifiers(m, lm, sm)
  def get_property_color(p,modifier)
  def get_property_array(p,modifier)
  def get_property_number(p,modifier)
  def get_max_array(p)
  def get_max_number(p)

class ParticleEmissorProperties
 
  # Global attributes
  attr_accessor :viewport -Viewport usado por todos los sprites
  attr_accessor :simulation_space -:local para mover partícula con emisor
  attr_accessor :max_particles -Numeric, límite de sprites
  attr_accessor :duration -frames antes de reinicio de emisión
  attr_accessor :looping -si se repite el efecto
  attr_accessor :autoplay -emitir al iniciar
  attr_accessor :bitmaps -imágenes usadas por partículas (al azar)
 
  # Emission attributes
  attr_accessor :pps -Partículas por segundo
  attr_accessor :bursts -Array de ráfagas. Usa un tiempo t de
acuerdo al temporizador interno del emisor
y un número n de partículas a emitir en
el momento. bmp es usado para determinar
un bitmap personalizado (nil para usar
los otros al azar).
[[t1,n1,bmp],[t2,n2,bmp], (...)]
  attr_accessor :shape -Forma del emisor. :circle o :square
  attr_accessor :shape_a -Radio mínimo para :circle. Ancho para :square
  attr_accessor :shape_b -Radio máximo para :circle. Alto para :square
  attr_accessor :shape_angle -En círculos, delimita el arco de efecto.
[anguloInicio,anguloFin,angleStep]
 
  # Particle attributes
Los atributos de partícula suelen poseer un atributo modificador que modifica su
comportamiento de acuerdo a otro valor.
Ej. Si speed_modifier==:lifetime, la velocidad cambiará a lo largo de
la vida de la partícula.
Los atributos además pueden recibir valores en arrays o sueltos, así como rangos.
Los rangos pueden ser clase Range o FreeRange (clase hecha como parte de este script).
Ejs.
color = Color.new(0,0,0,0)
color = [Color.new(0,0,0,255),Color.new(32,128,196,160)]
speed = [1,0]
speed = [[-1,5,7],FreeRange.new(-3,3)]
Algunos atributos requieren ser encapsulados en un array siempre de un tamaño específico,
pero sus miembros internos pueden encapsularse en otro array o ser Range/FreeRange.

  attr_accessor :lifetime -Tiempo de vida de partículas (en frames)
  attr_accessor :speed -Velocidad [X,Y].
  attr_accessor :speed_modifier # :none AZAR :speed VELOCIDAD :lifetime VIDA RESTANTE
  attr_accessor :acceleration -
  attr_accessor :acceleration_modifier # :none AZAR :speed VELOCIDAD :lifetime VIDA RESTANTE
  attr_accessor :size -
  attr_accessor :size_modifier # :none AZAR :speed VELOCIDAD :lifetime VIDA RESTANTE
  attr_accessor :rotation -
  attr_accessor :rotation_modifier # :none AZAR :speed VELOCIDAD :lifetime VIDA RESTANTE
  attr_accessor :opacity -
  attr_accessor :opacity_modifier # :none AZAR :speed VELOCIDAD :lifetime VIDA RESTANTE
  attr_accessor :color -
  attr_accessor :color_modifier # :none AZAR :speed VELOCIDAD :lifetime VIDA RESTANTE
  def initialize(bitmaps=[]) -Inicializador, recibe bitmaps a usar.
  def get_random_bitmap -Devuelve un bitmap al azar de bitmaps.
Devuelve un bitmap blanco de 8x8 si no hay bitmaps.
 
class ParticleEmissor
  attr_accessor :properties
  attr_accessor :x
  attr_accessor :y
  def initialize(x, y, z, _properties=ParticleEmissorProperties.new)
  def update
  def create_new_particle(bmp)
  def get_shape_coordinate
  def dispose
================================================================================
=end

module OZMath
  def self.deg2rad(degrees)
    return degrees * Math::PI / 180
  end
 
  def self.lerp(v,a,b)
    return (b-a)*v + a
  end
 
  def self.clamp(v,min,max)
    return [ [ min, v ].max, max ].min
  end
 
  def self.rand_range(a,b)
    return rand * (b-a) + a
  end
 
  def self.rand_range_i(a,b)
    return rand(b-a)+a
  end
 
  def self.lerp_col(v, c1, c2)
    r = lerp(v, c1.red, c2.red)
    g = lerp(v, c1.green, c2.green)
    b = lerp(v, c1.blue, c2.blue)
    a = lerp(v, c1.alpha, c2.alpha)
    return Color.new(r,g,b,a)
  end
end

class FreeRange
  def initialize(first,last)
    @first = first
    @last = last
  end
 
  attr_reader :first
  attr_reader :last
end

class Particle < Sprite
  attr_reader :dead
 
  def initialize(bitmap, emissor, x, y, viewport=nil)
    super(viewport)
    self.bitmap = bitmap
    self.blend_type = 1
    @ref = emissor if emissor.properties.simulation_space==:local
    @x = x
    @y = y
    if @ref==nil
      @x += emissor.x
      @y += emissor.y
    end
    # Lifetime is constant
    @lifetime = get_property(emissor.properties.lifetime)
    @starting_lifetime = @lifetime
    @dead = false
    # Modifiers are also constant
    @speed_modifier = emissor.properties.speed_modifier
    @acceleration_modifier = emissor.properties.acceleration_modifier
    @size_modifier = emissor.properties.size_modifier
    @rotation_modifier = emissor.properties.rotation_modifier
    @opacity_modifier = emissor.properties.opacity_modifier
    @color_modifier = emissor.properties.color_modifier
   
    # Others are processed
    @speed = emissor.properties.speed
    @top_speed = get_max(emissor.properties.speed,:array)
    @acceleration = emissor.properties.acceleration
    @size = emissor.properties.size
    @rotation = emissor.properties.rotation
    @opacity_ = emissor.properties.opacity
    @color = emissor.properties.color
    # Initialize speed
    if @speed_modifier==:lifetime
      @current_speed = get_property(@speed,0,:array)
    else
      @current_speed = get_property(@speed,nil,:array)
    end
    # Update
    update
  end
 
  def iter_modifiers(m, lm, sm)
    return (m==:lifetime) ? lm : (m==:speed) ? sm : nil
  end
 
  def update
    super
    # Lifetime update
    return if @dead==true
    @lifetime -= 1
   
    # Buffer modifiers
    lm = get_modifier(:lifetime)
    sm = get_modifier(:speed)
    # Set modifier buffers to each
    speed_mod = iter_modifiers(@speed_modifier,lm,sm)
    accel_mod = iter_modifiers(@acceleration_modifier,lm,sm)
    size_mod = iter_modifiers(@size_modifier,lm,sm)
    rot_mod = iter_modifiers(@rotation_modifier,lm,sm)
    opacity_mod = iter_modifiers(@opacity_modifier,lm,sm)
    color_mod = iter_modifiers(@color_modifier, lm, sm)
    # Update speed
    accel = get_property(@acceleration,accel_mod,:array)
    @current_speed[0] += accel[0]
    @current_speed[1] += accel[1]
    # Update size
    self.zoom_x = get_property(@size[0],size_mod) if size_mod != nil
    self.zoom_y = get_property(@size[1],size_mod) if size_mod != nil
    # Update angle
    self.angle += get_property(@rotation,rot_mod)
    self.opacity = get_property(@opacity_,opacity_mod)
    # Update color
    self.color = get_property(@color, color_mod, :color)
    # Update position
    sm = speed_mod==nil ? 1 : speed_mod
    @x += @current_speed[0]*sm
    @y += @current_speed[1]*sm
    if @ref==nil
      self.x = @x
      self.y = @y
    else
      self.x = @x + @ref.x
      self.y = @y + @ref.y
    end
   
    if (@lifetime <= 0)
      @dead = true
      self.visible = false
    end
  end
 
  # Returns: Any number / array of number
  def get_property(p,modifier=nil,k=:number)
    return get_property_color(p, modifier) if k==:color
    return get_property_array(p, modifier) if k==:array
    return get_property_number(p,modifier)
  end
  # Returns: Any number / array of number
  def get_max(p, k=:number)
    return get_max_array(p) if k==:array
    return get_max_number(p)
  end
  # Returns: 0..1
  def get_modifier(mod)
    if mod==:lifetime
      return (@starting_lifetime-@lifetime)*1.0/@starting_lifetime
    end
    if mod==:speed
      s = @current_speed[0].abs + @current_speed[1].abs
      ts= @top_speed[0].abs + @top_speed[1].abs
      return (s*1.0/ts)
    end
    return 0
  end
 
  # "HELPERS" (or internal methods)
  def get_property_color(p,modifier)
    if p.is_a?(Array)
      m = modifier==nil ? rand() : modifier
      a = (p.size * m).floor
      a = p.size-1 if a>=p.size
      b = a+1
      b = a if b>=p.size
      c1 = p[a]
      c2 = p[b]
      l = (m * p.size) - a
      return OZMath.lerp_col(l, c1, c2)
    elsif p.is_a?(Color)
      return p
    end
    return Color.new(0,0,0)
  end
  def get_property_array(p,modifier)
    val = []
    for i in 0...p.size
      val[i] = get_property_number(p[i], modifier)
    end
    return val
  end
  def get_property_number(p,modifier)
    # If modifier set to none
    if (modifier == nil)
      if p.is_a?(Numeric)||p.is_a?(Color)
        return p
      elsif p.is_a?(Array)
        return 0 if p.size==0
        a = rand(p.size)
        return p[a]
      elsif p.is_a?(Range) || p.is_a?(FreeRange)
        return OZMath.rand_range(p.first, p.last)
      end
    end
    # Modifier must be 0..1
    if p.is_a?(Numeric)
      return p * modifier
    elsif p.is_a?(Array)
      return 0 if p.size==0
      a = (p.size * modifier).floor
      a = OZMath.clamp(a, 0, p.size-1)
      return p[a]
    elsif p.is_a?(Range) || p.is_a?(FreeRange)
      return OZMath.lerp(modifier, p.first, p.last)
    end
  end
 
 
  def get_max_array(p)
    val = []
    for i in 0...p.size
      val[i] = get_max_number(p[i])
    end
    return val
  end
  def get_max_number(p)
    if p.is_a?(Numeric)
      return p
    elsif p.is_a?(Array)
      a = 0
      p.each {|v| a = v if a<v}
      return a
    elsif p.is_a?(Range) || p.is_a?(FreeRange)
      return p.last
    end
  end
 
end

class ParticleEmissorProperties
  # Global attributes
  attr_accessor :viewport
  attr_accessor :simulation_space
  attr_accessor :max_particles
  attr_accessor :duration
  attr_accessor :looping
  attr_accessor :autoplay
  attr_accessor :bitmaps
 
  # Emission attributes
  attr_accessor :pps #Particles Per Second
  attr_accessor :bursts #Array [[t1,n1],[t2,n2]]
  attr_accessor :shape # :circle :square
  attr_accessor :shape_a
  attr_accessor :shape_b
  attr_accessor :shape_angle #circle only, [a,b,c]
 
  # Particle attributes (can receive array, range, etc)
  attr_accessor :lifetime
  attr_accessor :speed
  attr_accessor :speed_modifier # :none :speed :lifetime
  attr_accessor :acceleration
  attr_accessor :acceleration_modifier # :none :speed :lifetime
  attr_accessor :size
  attr_accessor :size_modifier # :none :speed :lifetime
  attr_accessor :rotation
  attr_accessor :rotation_modifier # :none :speed :lifetime
  attr_accessor :opacity
  attr_accessor :opacity_modifier # :none :speed :lifetime
  attr_accessor :color
  attr_accessor :color_modifier # :none :speed :lifetime
 
  def initialize(bitmaps=[])
    @viewport = nil
    @simulation_space = :global
    @max_particles = 1000
    @duration = 200
    @looping = true
    @autoplay = true
   
    @pps = 24
    @bursts = []
    @shape = :circle
    @shape_a = 8
    @shape_b = 0
    @shape_angle = [0,360,0]
   
    @lifetime = 60
    @speed = [FreeRange.new(-1,1),FreeRange.new(-1,1)]# ??
    @speed_modifier = :none
    @acceleration = [0,0]
    @acceleration_modifier = :none
    @size = [FreeRange.new(0.5,1.0),FreeRange.new(0.5,1.0)]
    @size_modifier = :none
    @rotation = 2
    @rotation_modifier = :none
    @opacity = FreeRange.new(192,32)
    @opacity_modifier = :lifetime
    @color = [Color.new(255,255,255,255),
              Color.new(255,255,0,255),
              Color.new(0,0,255,255)]
    @color_modifier = :lifetime
    @bitmaps = bitmaps
  end
 
  def get_random_bitmap
    if @bitmaps.size==0
      b = Bitmap.new(8,8)
      b.fill_rect(b.rect, Color.new(255,255,255,255))
      @bitmaps[0] = b
      return b
    end
    return @bitmaps[rand(bitmaps.size)]
  end
end

class ParticleEmissor
  attr_accessor :properties
  attr_accessor :x
  attr_accessor :y
 
  def initialize(x, y, z, _properties=ParticleEmissorProperties.new)
    @particles = []
    @properties = _properties
    @playing = @properties.autoplay
    @timer = @properties.duration
    @x = x
    @y = y
    @z = z
    @fps = @pps = 0 # Particle creation control variables
  end
 
  def update
    # Create control variables
    to_delete = []
    count = 0
    # Update existing particles.
    @particles.each {|p|
      if p.dead==true
        to_delete.push(p)
      else
        count += 1
        p.update
      end
    }
    # Remove old particles from array
    @particles = (@particles-to_delete)
   
    # Playing particle effect (creates new particles if it's playing)
    return if (!@playing)
    @timer -= 1
    if @timer <= 0
      if @properties.looping
        @timer = @properties.duration
      else
        @playing = false
      end
    end
    return if count > @properties.max_particles
   
    # Current second particles
    @fps += 1
    if @fps > Graphics.frame_rate
      @pps = @fps = 0
    end
    expected_pps = @properties.pps * @fps / Graphics.frame_rate
    particles_to_create = expected_pps - @pps
   
    # TODO - Bursts
    curr_time = @properties.duration - @timer
    @properties.bursts.each { |burst|
      if burst[0]==curr_time
        burst[1].times {|n| create_new_particle(burst[2])}
      end
    }
   
    if particles_to_create > 0
      # Create particle x times
      particles_to_create.times {|n| create_new_particle(nil)}
    end
    @pps = expected_pps
  end
 
  def create_new_particle(bmp)
    return if @particles.size >= @properties.max_particles
    # Create new particles
    if bmp==nil
      b = properties.get_random_bitmap
    else
      b = bmp
    end
    coord = get_shape_coordinate
    p = Particle.new(b, self, coord[0], coord[1], @viewport)
    p.z = @z
    @particles.push(p)
  end
 
  def get_shape_coordinate
    coord = [0,0]
    case @properties.shape
    when :circle
      _ap = @properties.shape_angle
      angle = OZMath.rand_range(_ap[0], _ap[1])
      angle = (angle / _ap[2]).floor * _ap[2] if _ap[2] > 0
      radius = OZMath.rand_range(@properties.shape_a, @properties.shape_b)
      rad = OZMath.deg2rad(angle)
      coord[0] = Math.cos(rad)*radius
      coord[0] = Math.sin(rad)*radius
    when :square
      ah = @properties.shape_a/2
      bh = @properties.shape_b/2
      coord[0] = OZMath.rand_range(-ah,ah)
      coord[1] = OZMath.rand_range(-bh,bh)
    end
    return coord
  end
 
  def dispose
    @particles.each {|p| p.dispose }
  end
end



Instructions

You can refer to the usage in the demo inside Scene_Title, as it showcases some of the features. But basically it works as an sprite, by changing parameters and other stuff. I did however sepparate the particle emissor properties from the emitter object, as preparation for an editor.

Most basic usage:

@yourvariable = ParticleEmissor.new(x, y, z)
#At update
@yourvariable.update
#At dispose
@yourvariable.dispose

Some extra stuff I did at the demo

p = ParticleEmissorProperties.new
# By default, all particles use a white autogenerated 8x8 square.
# This is how you assign custom bitmaps. Pass an array, and it will select one at random.
p.bitmaps = [RPG::Cache.picture("particle1")]
# Defining bursts.
p.bursts = [
      [100,32,RPG::Cache.picture("particle2")] #At the hundredth frame, spawns 32 particles using "particle2" as bitmap.
      ]
@particles = ParticleEmissor.new(320, 160, 600, p) #This is how you pass custom properties to an emitter object.



Compatibility

Afaik, none. It's an independent class.


Credits and Thanks


  • Unity, I tried ripping off as much as possible.




Author's Notes

License is Creative Commons 0. Free for use, commercial or non-commercial. You can use, share, modify it. Whatever. :^)
A lot of comments are in Spanish. Sorry! Had no time to change it. But other comments are in English just because. So there is some balance! (?)

orochii

Here is the roadmap for the script.
- Stop/pause effect support.
- Loop points. Both for entry and exit.
- Audio playback.
- Particle system editor.

There is some other tweaks to do that I've just considered, specifically for bitmap loading (in order to make it compatible with the editor idea, and for simplicity). But yeah.

Salut!