レーザーの判定とか

DXRuby公式サイトの掲示板に久しぶりに書き込みがあった。
内容は高速で移動する物体の衝突判定、ということだったのだが、簡単なゲームで扱うのはだいたい遅い物体と速い物体の衝突だろうということで、そのような回答になった。
これがもし、高速で移動する物体同士の判定だったりすると、それは俺には手に負えないものになってしまうからその手の衝突判定解説サイトへ誘導するしかなくなるのだが、普通の人はDXRubyでそういうことはしないだろうと思う。


さて、高速で移動する物体も1フレームの間に画面の端から端まで移動できたりするとスウィープはただの矩形になるし、それを超えると光速のレーザーまで同じ判定でいけるようになる。
縦や横に直線なら簡単だが、角度を持つレーザーの衝突判定はDXRubyExtensionを使うなら三角形2つを組み合わせた「回転した矩形」で表すことになる。
回転する処理は搭載していないから座標計算は自前でやることになるが、三角形や回転した矩形を衝突判定しようとすると大変なことになるから、とりあえずDXRubyExtensionがあるとラクだ。
どんなふうに書けばできるのか、考えてみるだけじゃなくて、実際に実装して動かしてみよう、ということで、簡単なサンプルを作ってみた。
今回はDXRuby1.0系で動く。
とはいえそこそこ動いて見えるものにしようとしたら、さすがにちょっと長くなってしまった。

#! ruby -Ks
require 'dxruby'
require 'dxrubyex'

image_houdai = Image.new(10,10,[0,255,0])

ENEMY_OBJECTS = []
ENEMY_COLLISIONS = []

class Enemy
  attr_reader :delete, :x, :y, :width, :height
  @@image = Image.new(50,50).circleFill(25,25,24,[255,0,255])
  @@image_damage = Image.new(50,50).circleFill(25,25,24,[255,200,255])
  def initialize
    @x = rand(590)
    @y = -49
    @speed = rand(5)
    @collision = CollisionCircle.new(self, 25, 25, 24)
    @delete = false
    @width = 50
    @height = 50
    @hp = 100
  end

  def update
    @y += 1
    if @y >= 480
      @collision.delete
      @delete = true
    else
      @collision.set(@x,@y)
      ENEMY_COLLISIONS.push(@collision)
    end
  end

  def hit(o) end

  def damage(o)
    @hp -= 1
    if @hp == 0
      @delete = true
    else
      Window.draw(@x, @y, @@image_damage,2)
    end
  end

  def draw
    Window.draw(@x, @y, @@image)
  end
end

class Laser
  attr_reader :collision, :enemy, :x, :y
  @@image_laser = Image.new(1,1,[128,255,255,255])
  def initialize(x, y)
    @x = x
    @y = y
    @collision = [CollisionTriangle.new(self, 0,0,0,0,0,0),CollisionTriangle.new(self, 0,0,0,0,0,0)]
    @collision[0].set(@x,@y)
    @collision[1].set(@x,@y)
    @angle = 0
    @enemy = []
  end

  def shot(d)
    @enemy.push(d)
  end

  def get_near_enemy
    near_distance = 800
    near_enemy = nil
    @enemy.each do |e|
      distance = Math.sqrt((@x - (e.x + e.width/2))**2 + (@y - (e.y + e.height/2))**2)
      if distance < near_distance
        near_distance = distance
        near_enemy = e
      end
    end
    return near_enemy
  end

  def rotation(x1,y1,x2,y2,x3,y3,x4,y4,angle)
    sina = Math.sin(angle/180.0*Math::PI)
    cosa = Math.cos(angle/180.0*Math::PI)
    result_x1 = x1 * cosa + y1 * sina
    result_y1 = x1 * sina - y1 * cosa
    result_x2 = x2 * cosa + y2 * sina
    result_y2 = x2 * sina - y2 * cosa
    result_x3 = x3 * cosa + y3 * sina
    result_y3 = x3 * sina - y3 * cosa
    result_x4 = x4 * cosa + y4 * sina
    result_y4 = x4 * sina - y4 * cosa
    return [result_x1, result_y1, result_x2, result_y2, result_x3, result_y3, result_x4, result_y4]
  end

  def update
    if Input.mouseDown?(M_LBUTTON)
      mx,my = Input.mousePosX, Input.mousePosY
      @angle = Math.atan2(mx - @x, @y - my) / Math::PI * 180
      x1,y1,x2,y2,x3,y3,x4,y4 = rotation(-5,-800,5,-800,5,0,-5,0,@angle+180)
      @collision[0].setRange(x1,y1,x2,y2,x3,y3)
      @collision[1].setRange(x2,y2,x3,y3,x4,y4)
    end
  end

  def draw(length)
    Window.drawEx(@x, @y, @@image_laser, :centerx=>0.5,:centery=>1,:scalex=>10,:scaley=>length,:angle=>@angle,:blend=>:add,:z=>1)
  end
end

laser = [Laser.new(145, 474), Laser.new(505, 474)]

count = 0
Window.loop do
  count += 1
  if count % 50 == 0
    ENEMY_OBJECTS.push(Enemy.new)
  end

  if Input.mouseDown?(M_LBUTTON)
    laser[0].update
    laser[1].update
  end

  ENEMY_OBJECTS.each do |e| e.update end

  laser.each do |l|
    Collision.check(l.collision, ENEMY_COLLISIONS) if Input.mouseDown?(M_LBUTTON)
    enemy = l.get_near_enemy
    enemy.damage(l) if enemy
    if Input.mouseDown?(M_LBUTTON)
      if enemy
        length = Math::sqrt(((enemy.x + enemy.width/2) - (l.x+5))**2 + ((enemy.y + enemy.height/2) - (l.y+5))**2)
      else
        length = 800
      end
      l.draw(length)
    end
  end

  ENEMY_OBJECTS.each do |e| e.draw end
  ENEMY_COLLISIONS.clear
  ENEMY_OBJECTS.delete_if do |e| e.delete end

  laser[0].enemy.clear
  laser[1].enemy.clear

  Window.draw(140,470,image_houdai)
  Window.draw(500,470,image_houdai)
end