パーティクルシステムその後

このあいだのコードでは新しい種類のParticleクラスを追加すると、それにあわせてParticleFactoryクラスも追加しなければいけなかったが、これが面倒なのでParticleFactoryは共通にしたくなった。拡張の容易さ重視である。しかしそのかわりにParticle側を作るのが難しくなったりParticleFactoryの使い方が難しくなったりParticleの生成や移動処理が遅くなったりするのはよろしくない。自分で書いておいてアレなのだが、トリレンマに真っ向から挑戦しようという話である。まあ、ソフトウェアは実装テクニックによる改善がありえるので別におかしな話ではない。努力すれば報われる。


コードは以下のようになった。

require 'dxruby'

class StandardParticle < Sprite
  PARAMETER = {:target => nil, 
               :image => nil,
               :angle_min => nil,
               :angle_max => nil,
               :speed_min => nil,
               :speed_max => nil,
               :alpha => 255,
               :lifetime => nil,
               :d_alpha => 0,
               :gravity => 0
  }

  def initialize(x, y, parent)
    @parent = parent

    self.x = x
    self.y = y
    self.image = parent.image
    self.target = parent.target
    self.alpha = parent.alpha
    @lifetime = parent.lifetime
    @angle = rand() * (parent.angle_max - parent.angle_min) + parent.angle_min
    @speed = rand() * (parent.speed_max - parent.speed_min) + parent.speed_min
    @dx = Math.cos(@angle / 180.0 * Math::PI) * @speed
    @dy = Math.sin(@angle / 180.0 * Math::PI) * @speed
  end

  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
    @parameter = {}
    (class << self;self;end).instance_eval do
      attr_accessor *(particle_class::PARAMETER.keys)
    end
    particle_class::PARAMETER.each do |k, v|
      self.send(k.to_s+"=", v)
    end
  end

  def generate(x, y, count)
    flag = self.empty?
    count.times { self << @particle_class.new(x, y, self) }
    @@all_objects.push(self) if flag
  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

hoge = ParticleFactory.new(StandardParticle)
hoge.angle_min = 0
hoge.angle_max = 360
hoge.speed_min = 0
hoge.speed_max = 5
hoge.lifetime = 120
hoge.d_alpha = -2
hoge.image = image
hoge.gravity = 0.05

Window.loop do
  if Input.mouse_push?(M_LBUTTON)
    hoge.generate(Input.mouse_pos_x, Input.mouse_pos_y, 200)
  end
  ParticleFactory.step
end

とりあえずParticleは今後色々作るんちゃうかってことで、StandardParticleに名前を変えた。中身は書き変わってはいるが基本的には前と変わっていない。定数PARAMETERが増えた。これはパラメータの名前シンボルと初期値のハッシュで指定する。定数PARAMETERはParticleFactoryが使う。angleとspeedの指定方法を変えたがこっちのほうがわかりやすいかなというだけで大きな意味は無い。
ParticleFactory側はごっそり変わった。まずArrayを継承するようになった。中身は配下のParticleで、ParticleはSpriteを継承して作るから、こうしておけば衝突判定とかでこいつをそのまま使えるようになる。
あと、ParticleFactory.newの引数にParticleのクラスを渡すようになった。initializeの中ではそこから定数PARAMETERを取得して、パラメータの名前シンボルを使って自身の特異メソッドとしてSetter/Getterを定義して、ついでに初期値を設定する。ここの処理が前と比べて遅くなっているはずだが、ParticleFactory.newが速度のネックになるほど呼ばれることはまず無いだろうと思う。
それから、ParticleFactory.stepを毎フレーム呼ぶようにした。メインループに隠蔽したければあとですればよいので今はこれでいい。

と、こんな感じに変更することで、ParticleFactoryを使う側はほぼ変わりなく、Particleを作る場合にParticleFactoryを追加する必要がなくなり、Particleを作る手間もさして増えていないのではなかろうか。基本的な実行速度も遅くなる要因は無いと思う。