物理エンジンを作る4

四角を回転させることにチャレンジしてみよう。回転した四角同士の衝突判定はDXRubyで普通に可能だが、衝突した反応を計算するためには、衝突点と作用線を求める必要がある。それがあれば先日の計算にそれらをぶち込むことで結果が計算できる、はずである。
ところで結論を先に書いてしまうと今回のロジックは色々ダメだったのでボツとなる。やはり四角を回転させるのは難しい。世の中にはきちんと動いている物理エンジンがたくさんあるのでパクってこれば済む話だがオリジナルで作ってみたのが敗因である。
ではまずComplexVectorの機能追加から。

ComlexVector

ついでに情けないメソッド名間違いがあったのでこっそりなおしておく。

module ComplexVector
  refine Complex do
    def perp
      Complex(-self.imag, self.real)
    end

    def rperp
      Complex(self.imag, -self.real)
    end
  end
end

左90度回転の追加である。
それにしてもRefinementsで拡張したComplexVectorはものすごい便利なのだが、まさかDXRubyに組み込むわけにもいかんだろうし機能が揃ってきたらサンプルに入れるぐらいか。

回転した四角の処理

衝突時の作用線は衝突した辺のベクトルと垂直になる。また、その辺と衝突した頂点の位置が衝突点となる。厳密なことを言い始めると色々ありそうだが、とりあえずそうすることにしておこう。
四角同士が衝突する場合、必ず頂点と辺がぶつかるはずなので、そのペアを検出することで、衝突点と作用線を取得することができる。このへんの機能がDXRubyの衝突判定にあってくれればラクになりそうではあるが、物理エンジンを作る場合以外で使い道も無いだろうし、それを算出できそうなロジックにもなっていないので、その案は却下とする。
また、同じ角度になった四角同士の場合(床と自キャラなど)は2点で衝突することになる。この場合はそれぞれの点で撃力を計算して加えるので衝突点と作用線は複数返してまとめて処理するような感じにしておく必要がある。
とりあえず判定処理はこんな感じに。強引だが。

  # 四角と四角の判定
  def test_obb_obb(o1, o2)
    result = []

    # 頂点をそれぞれ計算
    p11 = (Complex(o1.collision[0]  , o1.collision[1]  ) - o1.center_xy).rotate(o1.angle) + o1.xy + (o1.offset_sync ? 0 : o1.center_xy)
    p12 = (Complex(o1.collision[2]+1, o1.collision[1]  ) - o1.center_xy).rotate(o1.angle) + o1.xy + (o1.offset_sync ? 0 : o1.center_xy)
    p13 = (Complex(o1.collision[2]+1, o1.collision[3]+1) - o1.center_xy).rotate(o1.angle) + o1.xy + (o1.offset_sync ? 0 : o1.center_xy)
    p14 = (Complex(o1.collision[0]  , o1.collision[3]+1) - o1.center_xy).rotate(o1.angle) + o1.xy + (o1.offset_sync ? 0 : o1.center_xy)
    p21 = (Complex(o2.collision[0]  , o2.collision[1]  ) - o2.center_xy).rotate(o2.angle) + o2.xy + (o2.offset_sync ? 0 : o2.center_xy)
    p22 = (Complex(o2.collision[2]+1, o2.collision[1]  ) - o2.center_xy).rotate(o2.angle) + o2.xy + (o2.offset_sync ? 0 : o2.center_xy)
    p23 = (Complex(o2.collision[2]+1, o2.collision[3]+1) - o2.center_xy).rotate(o2.angle) + o2.xy + (o2.offset_sync ? 0 : o2.center_xy)
    p24 = (Complex(o2.collision[0]  , o2.collision[3]+1) - o2.center_xy).rotate(o2.angle) + o2.xy + (o2.offset_sync ? 0 : o2.center_xy)

    # 重心から頂点までの線分と交差する相手の辺を求め、作用線、深さ、衝突位置を取得する
    o1_center = o1.xy + (o1.offset_sync ? 0 : o1.center_xy)
    [p11, p12, p13, p14].each do |point|
      [[p21, p22], [p22, p23], [p23, p24], [p24, p21]].each do |line|
        if intersection(o1_center, point, line[0], line[1])
          normal = (line[1] - line[0]).rperp.normalize
          depth = Complex.polar((point - line[0]).magnitude, (point - line[0]).angle - (line[1] - line[0]).angle).imag
          if depth > 0
            result << [-normal, depth, point]
          end
        end
      end
    end

    o2_center = o2.xy + (o2.offset_sync ? 0 : o2.center_xy)
    [p21, p22, p23, p24].each do |point|
      [[p11, p12], [p12, p13], [p13, p14], [p14, p11]].each do |line|
        if intersection(o2_center, point, line[0], line[1])
          normal = (line[1] - line[0]).rperp.normalize
          depth = Complex.polar((point - line[0]).magnitude, (point - line[0]).angle - (line[1] - line[0]).angle).imag
          if depth > 0
            result << [normal, depth, point]
          end
        end
      end
    end

    result
  end

  # 線分の衝突判定
  EPS = 0.00001
  def intersection(a1, a2, b1, b2)
    (a2-a1).cross(b1-a1) * (a2-a1).cross(b2-a1) < EPS and (b2-b1).cross(a1-b1) * (b2-b1).cross(a2-b1) < EPS
  end

Sprite#offset_syncとSprite#collisionの設定に対応すると頂点の計算は少し面倒になる。
重心から頂点までの線分と相手の辺で判定すると角ギリギリのあたりで判定がおかしくなる。これは想定していたが、動かしてみたらやっぱりおかしいのでこの方法はそれそのものがまずい。角にきわめて近い部分が衝突する場合にどのように処理するかは物理エンジンによって違うようだし、このへんは調べながらおいおい、って感じか。
んで、このおかしい状態だと2Dジャンプゲーもまともに作れなくなってしまうので、このロジックはいったんボツとする。

おしまい

このようなコードを書くとなんとなくそれっぽく反応しているのが見える。

world = PhysicsWorld.new

s1 = PhysicsBox.new(100, 190, 60, 60)
s1.v = 2
s1.e = 0.0 # 反発係数
world.add(s1)

s2 = PhysicsBox.new(500, 230, 40, 40)
s2.v = -2
s2.e = 0.0 # 反発係数
world.add(s2)

s3 = PhysicsBox.new(300, 400, 40, 40)
s3.v = -2i
s3.e = 0.0 # 反発係数
world.add(s3)

Window.loop do
  world.step
  break if Input.key_push?(K_ESCAPE)
end


ソース全体はこちら。おかしかった計算式などいろいろ修正している。
あと、摩擦が無いと重力に引っ張られている状態もいろいろ変なのでこういう動きをさせるならもっと作りこむ必要がありそうだ。
四角が回転する動きが自然にできる用途というとよくある城崩しゲーみたいな印象があるが、そういうものを作るには相当きちんと作らないとうまく動かないはずなので、そもそも回転させる需要が無いのではないかとも思う。