パーティクルというもの

しばらくダラダラとツイッターに書いていたのでブログには初登場のパーティクルなのだが。DXRubyに組み込んで簡単に使えるといいなあ、と思っていたのだ。
どういうものかというと、動きのパラメータを定義して、その通りに勝手に動くオブジェクト、という感じで、ゲームではエフェクトなどに使われる。丁度、DXRubyFw時代のSpriteが半分ぐらいは近いのではないか。あれのコマンド実行をなくした自動移動するものって感じだろう。
3D空間のパーティクルではもともとのワールドがリアルなため、衝突判定や物理演算が無いとイマイチ感であふれてしまうかもしれないが、2Dだと単純にエフェクトを作るという用途でいいような気がする。DXRuby1.4のSpriteで一つ一つコード書いてくのもしんどいので、ちょっとしたものは簡単に作れるといいな、と思ったわけだ。
先日のSceneにゲームループを突っ込むのもそうだけども、ちょっとだけ凝ったゲームを手軽に作るにはもう一段ほど上の機能が必要になると考えていて、パーティクルの機能もその延長である。

まあ、とりあえずRubyで書いてみた。


画面はこんな感じになるが、見ても何もわからないかもしれない。DXRuby1.4で動作するコードを全部載せておくので動かしてみてもらえるといいと思う。

プロトタイプレベルのParticleFactoryクラスを実装すると、こんなコードが動くようになるイメージだ。

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
hoge.angle_rand = 360
hoge.speed_rand = 10
hoge.lifespan = 120
hoge.d_alpha = -2
hoge.image = image

Window.testloop do
  if Input.mouse_push?(M_LBUTTON)
    hoge.x = Input.mouse_pos_x
    hoge.y = Input.mouse_pos_y
    hoge.generate(100)
  end
end

角度と速度に対してランダムの範囲と切片(今回は使ってないけど)とその他いろいろ設定してParticleFactory(仮名)を生成、そいつに生成座標を設定しつつgenerate(100)すると100個のパーティクルが生成されて、あとは自動で動く。lifespanのフレームが過ぎると勝手に消える。あとはもっとたくさんのパラメータ(重力とか摩擦とか)や、画像の回転、生成座標範囲の指定などを追加するといろいろなことができるようになるんじゃなかろうか。
現状では全部Rubyで書いているが100個程度の生成ではボタンを連打してもぜんぜん遅くない。これを1000個にするとちょっと連打すると遅くなってしまうが、そんなに生成するものなのかというとそんなこともなく、ひょっとしたらRubyで書いておけば実用レベルには十分なのではないかと思ったりしなくもないわけだが、とりあえずCで実装することを目標にはしておく。
以下はライブラリ側のコードである。

require 'dxruby'

module Window
  def self.testloop
    loop do
      yield
      ParticleFactory.update
      ParticleFactory.draw
      ParticleFactory.clean
    end
  end
end

class Particle < Sprite
  def initialize(x, y, ang, spd, parent)
    @parent = parent
    @lifespan = parent.lifespan
    @angle = ang
    @speed = spd
    self.x = x
    self.y = y
    self.image = parent.image
    self.target = parent.target
    self.alpha = parent.alpha
  end

  def update
    if @lifespan
      @lifespan -= 1
      if @lifespan == 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 += Math.cos(@angle / 180.0 * Math::PI) * @speed
    self.y += Math.sin(@angle / 180.0 * Math::PI) * @speed
  end
end

class ParticleFactory
  attr_accessor :target, :image, :x, :y, :angle_rand, :angle_intercept, :speed_rand, :speed_intercept, :alpha, :lifespan, :d_alpha
  @@objects = []

  def self.update
    Sprite.update @@objects
  end
  def self.draw
    Sprite.draw @@objects
  end
  def self.clean
    Sprite.clean @@objects
    @@objects.delete_if do |a|
      a.size == 0
    end
  end

  def initialize
    @particles = []
    @target = nil
    @image = nil
    @x = @y = @dx = @dy = @angle_rand = @angle_intercept = @speed_rand = @speed_intercept = 0
    @alpha = 255
    @lifespan = nil
    @d_alpha = 0
  end

  def generate(count)
    @@objects.push Array.new(count) {
      Particle.new(@x, @y, rand() * @angle_rand + @angle_intercept, rand() * @speed_rand + @speed_intercept, self)
    }
  end
end