エミッタを考える
パーティクルシステムにおいて、エミッタというのがパーティクルを生成するオブジェクトになるから、これがないと始まらない。が、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したほうがいいんじゃね?とかそういうことになっちゃったりするかもしれない。知らんけど。
この手の記事というのはきちんとした設計ができてて、それを順番に解説しながら実装していきまーすみたいなものが多い気がするのだが、このパーティクルシステムは完全に試行錯誤のスクラップ&ビルド状態なので見てる人はそれなりの覚悟が必要だ。