エミッタを考える

パーティクルシステムにおいて、エミッタというのがパーティクルを生成するオブジェクトになるから、これがないと始まらない。が、Rubyで書く場合はユーザが作るクラスで適当にパーティクルを生成してもらえば済むわけで、わざわざエミッタというクラスを用意してそれを使わせるのはいかがなものか、という気もする。

もともとパーティクルシステムを考え始めたときには、個々のParticleと、それを生成して制御するParticleFactoryと、生成方法をどうにかするEmitterという三階層の設計をぼんやりと想像していた。前回までは前2つしかなかったわけだ。
んじゃあそもそもエミッタは何をするために作るのかと言うと、パーティクルの生成パターンを制御するためだ。前回の状態ではとりあえず乱数で方向と速度が決まってしまうし、それを例えば360度均等に360個飛ばそうとすると、方向と速度の範囲を固定にして1個ずつ生成するしかない。それってどうなの?という感じだ。頑張ったけどイマイチなのだ。
ユーザのコードとParticleFactoryの間にEmitterクラスを挟むことで、ある種の生成パターンに限定して簡単に生成できるようになる、かもしれない。ここでいう生成パターンというのは一定時間ごとに一定数のパーティクルを生成するとか、ある形状の範囲内に限定してランダム生成するとか、そういう感じの話で、わざわざ書かなくてもいいようなお約束のコードをパッケージしてご提供すると言う話だ。
といいつつそのへんを作りこむのはまだ先なんだけども、とりあえずEmitter層を追加してみたコードは以下のようになった。

require 'dxruby'

class StandardParticle < Sprite
  attr_accessor :dx, :dy, :lifetime, :parent

  def update
    if @lifetime
      @lifetime -= 1
      if @lifetime == 0
        self.vanish 
        return
      end
    end

    if self.alpha > 0
      self.alpha += @parent.d_alpha
      self.alpha = 0 if self.alpha < 0
    end

    self.x += @dx
    self.y += @dy
    @dy += @parent.gravity
  end
end

class ParticleFactory < Array
  @@all_objects = []

  def self.step
    Sprite.update @@all_objects
    Sprite.draw @@all_objects
    Sprite.clean @@all_objects
    @@all_objects.delete_if {|v| v.empty?}
  end

  def initialize(particle_class)
    @particle_class = particle_class
  end

  def generate(count)
    flag = self.empty?
    count.times do |i|
      temp = @particle_class.new
      yield temp, i
      self << temp
    end
    @@all_objects.push(self) if flag
  end
end

class SampleEmitter
  attr_accessor :gravity, :d_alpha

  def initialize(particle_class)
    @factory = ParticleFactory.new(particle_class)
  end

  def generate(count)
    @factory.generate(count) do |p, i|
      yield p, i
    end
  end
end


image = Image.new(3,3)
image[1,0] = [128, 255, 255, 255]
image[1,2] = [128, 255, 255, 255]
image[0,1] = [128, 255, 255, 255]
image[2,1] = [128, 255, 255, 255]
image[1,1] = C_WHITE

emitter = SampleEmitter.new(StandardParticle)
emitter.d_alpha = -2
emitter.gravity = 0

Window.loop do
  if Input.mouse_push?(M_LBUTTON)
    emitter.generate(360) do |particle, i|
      particle.parent = emitter
      particle.image = image
      particle.x = Input.mouse_pos_x
      particle.y = Input.mouse_pos_y
      particle.dx = Math.cos(i / 180.0 * Math::PI) * 3
      particle.dy = Math.sin(i / 180.0 * Math::PI) * 3
      particle.lifetime = 120
    end
  end
  ParticleFactory.step
end

前回のコードではParticleFactoryが前面に出ていたが、これは俺の考えでは隠蔽されるべきものだし、機能を持ちすぎている。だいたいParticleオブジェクトを生成するときに使うパラメータをParticleFactoryオブジェクトが持つ意味がない。頑張ったけど意味が無かった。
今回はEmitterはほぼ何もしていなくて、イテレータで生成したParticleオブジェクトを渡してくるようにしたから、ユーザのコードでParticleオブジェクトに値を設定するようになっている。こんなもんはこれでいいんじゃないの。Emitter側で何かしらパターンをつけるのであれば、ユーザが設定せずに勝手に設定される項目というのができてもよい。ただ、ParticleとEmitterはそれぞれ別々に入れ替え可能にしておきたいから、Particleのパラメータとして標準のインターフェイスを定義しておいたほうがいいだろう。
こっからこのEmitterクラスに何かしらの機能を付け足していけば便利になるのか使いやすくなるのか、はたまたゴミになるのか、そのへんは今後いじってみないとわからないから、これで行くぜーってことは無い。例えばEmitterモジュールにしてSpriteにmix-inしたほうがいいんじゃね?とかそういうことになっちゃったりするかもしれない。知らんけど。
この手の記事というのはきちんとした設計ができてて、それを順番に解説しながら実装していきまーすみたいなものが多い気がするのだが、このパーティクルシステムは完全に試行錯誤のスクラップ&ビルド状態なので見てる人はそれなりの覚悟が必要だ。