Window.draw_morphを使う

この記事はDXRuby Advent Calendar 2013の2日目です。
1日目の記事はあおいたくさんのIntroduction to DXRubyでした。

なんか初っ端から気合の入った紹介記事だったが、2日目は急展開でマイナーな機能の活用ネタとして、Window.draw_morphを使ってみる。
draw_morphはxとyを4つずつ指定する制限付き自由変形(?)可能な描画メソッドである。

Window.draw_morph(x1, y1, x2, y2, x3, y3, x4, y4, image, hash={} )

他の人がこれを使っているところはいまだに見たことが無いし、使っているサンプルも1つしかない。biyo.rbだ。

サンプルにこれだけが入っているということは、まさに「あ」を「びよーん」ってのがやりたくてこんなメソッドを作ったと考えられるわけだが、実はそうではない。draw_morphの画像変形はオマケ機能である。

元々の発想は自由な4点指定で四角を描画するという部分で、単色でよかったのだ。単色の変形した四角を描画するためだけにイチイチImageを作ってられない。今から考えたらWindow.draw_triangle_fillとか作っておけばよかったんじゃないかと思うが、まあ、画像の変形もできるしこれはこれでよいかな、みたいな。
んじゃあ単色の4点指定で四角を描画できたとして、何がやりたかったのか、というと、これである。

いわゆるホーミングレーザーだ。AdventCalendar向けにちょっと頑張って作った。本来ならこれがサンプルに入っていなければならないはずだったのだが、作るのが面倒で・・・(ダメじゃん)
このコードの計算は「かなり」いい加減で、理屈からして全く適当。それっぽく動いたからいいかな、とかいうレベル。座標の計算もアレで、急角度で曲がる部分が綺麗に描画できていない。このあたりは丁寧に作り込んで行けばマシになるはず。衝突判定は点なので小さい敵だと貫通するかもしれない。このへんも改善の余地がある。あと、自機が敵より上にいる場合は想定していない。
まあ、実際に作ろうとしたら色々と難しいことはあるけども、それは動き側の話であって、その前にこういうのを描画するのに既存の機能では無理があったから、何が必要かを考えた結果がdraw_morphの実装に繋がった、という話だったのだ。

ちなみにこういう動きの理想としてはおそらくVector/Matrixを使った3D誘導計算で、いまどきのPCならRubyで書いても数十本のレーザーを綺麗に動かすことができるはずだ。具体的にはレーザーはロール方向の回転が自由だから、敵が常に上になるようにロールさせておいて、ピッチを上向きにずらすことで誘導させる。あとは敵と進行方向の角度のズレや距離などで誘導する角度や速度を調整してやればそれっぽくなる。それっぽいと言ってもレーザーは光なんだから質量は無く、慣性計算に基づいた軌跡を描くのもおかしな話なわけで、まあ、かっこよければすべてヨシである。
誰かロックオンレーザーが撃てるSTG作ってくれないかなあ。ということで今回のネタは以上。
次回、3日目は土屋つかささんです。お楽しみに〜。

最後にソースを貼っておく。座標回転にVectorクラスを使うのでDXRuby1.5dev用になっている。

require 'dxruby'

class HomingLaser < Sprite
  @@image = Image.new(1, 1, C_GREEN)

  def initialize(x, y, dx, dy, lock)
    super(x, y)
    @dx, @dy, @lock = dx, dy, lock
    @ary = [] # 軌跡の配列
    @loss = false # 消えたらtrue
    @hom_flg = false
    self.collision = [0, 0]
  end

  def update
    if @loss
      if @ary.size == 0
        self.vanish
      end
    else
      if @hom_flg # ホーミング中
        sx = @lock.x + @lock.image.width / 2 - self.x
        sy = @lock.y + @lock.image.height / 2 - self.y
        relative_angle = Math.atan2(sy, sx) / Math::PI * 180 - @angle # 角度差算出
        relative_angle -= 360 if relative_angle > 180
        relative_angle += 360 if relative_angle < -180
        temp = relative_angle * ((1000-Math.sqrt(sx*sx + sy*sy))/6000) # 角度補正
        @angle += temp
        temp = -temp if temp < 0
        @hom_speed += 0.5/(temp+0.4) # 強引な計算で加速度の補正(いまいち)
        self.xy += Vector.new(@hom_speed, 0).rotate(@angle)

        @loss = true if self === @lock or self.x < -200 or self.x > 840 or self.y < -200 or self.y > 680
      else # ホーミングするまで
        @hom_speed = Math.sqrt(@dx**2 + @dy**2)
        @dy -= 1
        self.x += @dx
        self.y += @dy
        @hom_flg = true if @dy < -1 # ホーミング開始
        @angle = Math.atan2(@dy, @dx) / Math::PI * 180
      end

      # 座標計算
      v1 = self.xy + Vector.new(0, -5).rotate(@angle)
      v2 = self.xy + Vector.new(0, 5).rotate(@angle)
      v3 = self.xy + Vector.new(-@hom_speed, 5).rotate(@angle)
      v4 = self.xy + Vector.new(-@hom_speed, -5).rotate(@angle)
      if @ary[0]
        @ary[0][0] = v4
        @ary[0][1] = v3
      end
      @ary.unshift([v1, v2, v3, v4, 255])
    end
  end

  def draw
    @ary.delete_if do |d|
      v1, v2, v3, v4, alpha = d
      Window.draw_morph(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, v4.x, v4.y, @@image, alpha:alpha, blend: :add)
      d[4] -= 10 # alpha値を下げる
      d[4] < 0 # 消えたものは配列から消す
    end
  end
end

laser = []
font = Font.new(32)

sp = Sprite.new(300, 320, Image.load_tiles("image/myship.png", 4, 1, true)[0])
se = Sprite.new(0, 50, Image.load_tiles("image/enemy2.png", 4, 1, true)[0])
#sp = Sprite.new(300, 320, Image.new(32, 32, C_WHITE))
#se = Sprite.new(0, 50, Image.new(128, 64, C_WHITE))
se_dx = 5

Window.loop do
  sp.x += Input.x*2
  sp.y += Input.y*2
  se.x += se_dx
  se_dx = -se_dx if se.x >= 640-se.image.width or se.x <= 0

  if Input.key_down?(K_SPACE)
    temp = rand * 30 - 15
    laser.push(HomingLaser.new(sp.x + 16 + temp, sp.y + 36, temp, 15, se))
  end

  Sprite.update(laser)
  sp.draw
  se.draw
  Sprite.draw(laser)
  Sprite.clean(laser)
  Window.draw_font(0, 0, laser.size.to_s, font)

  break if Input.key_push?(K_ESCAPE)
end