物理エンジンを作る2

前回は中身のほぼ無いコードだった。今回はこれにちょっとだけ追加しよう。いきなり難しい計算などは無理ゲーなので、とりあえずは回転しない四角を動かすところから。

ComplexVector

速度の表現をComplexでやるのはよいのだが、2Dの力学計算は基本的にVectorを使う。でもVectorにある計算色々はComplexには無いので追加する。組み込みのライブラリにメソッドを追加するならRefinementsの出番である。

module ComplexVector
  refine Complex do
    def normalize
      self / self.magnitude
    end

    def dot(c)
      self.real * c.real + self.imag * c.imag
    end
  end
end

とりあえずこんだけ。必要になったら追加していくことにする。
PhysicsWorldクラスのほうでこれをusingする。

class PhysicsWorld
  using ComplexVector

うむ。

衝突解決

衝突応答とも言う。かなり適当にごりごりーっと書く。

  # 衝突解決
  def collision_resolve
    @hit_pair.each do |ary|
      o1, o2 = *ary

      # 当たってなかったら何もしない
      unless o1 === o2
        return
      end

      # 1フレーム前まで戻す
      toxy1 = o1.xy
      toa1  = o1.angle
      toxy2 = o2.xy
      toa2  = o2.angle

      o1.xy    -= o1.v
      o1.angle -= o1.av
      o2.xy    -= o2.v
      o2.angle -= o2.av

      # ちょっとずつ進めて当たった時間を求める
      tmp = [o1.v.magnitude, o2.v.magnitude].max
      time = 0
      flag = true
      1.upto(tmp.to_i) do |i|
        time = 1.0 / tmp * i
        o1.xy    += o1.v * (1.0 / tmp)
        o1.angle += o1.av * (1.0 / tmp)
        o2.xy    += o2.v * (1.0 / tmp)
        o2.angle += o2.av * (1.0 / tmp)

        if o1 === o2
          flag = false
          break
        end
      end

      # 当たらなかったので元の状態に戻す(これは当たってるはず)
      if flag
        time = 1
        o1.xy    = toxy1
        o1.angle = toa1
        o2.xy    = toxy2
        o2.angle = toa2
      end

      normal, depth = # 作用法線、衝突深さ
      if o1.collision.size == 4 and o2.collision.size == 4 # 四角と四角
        test_obb_obb(ary)
      end

      # 当たってない位置まで戻す補正処理
      o1.xy -= normal * depth / (1.0 / o1.mass + 1.0 / o2.mass) * (1.0 / o1.mass)
      o2.xy += normal * depth / (1.0 / o1.mass + 1.0 / o2.mass) * (1.0 / o2.mass)

      # 撃力を加える
      apply_impulse(ary, normal)

      # 残りの時間ぶん進める
      time = 1 - time
      o1.xy    += o1.v * time
      o1.angle += o1.av * time
      o2.xy    += o2.v * time
      o2.angle += o2.av * time
    end
  end

  # とりあえず回転しない四角と四角の判定
  def test_obb_obb(ary)
    o1, o2 = *ary
    depth = Float::INFINITY
    normal = nil

    [[o1.x, o2.x+o2.w, -1+0i], [o2.x, o1.x+o1.w, 1+0i], [o1.y, o2.y+o2.h, -1i], [o2.y, o1.y+o1.h, 1i]].each do |t|
      if t[0] < t[1]
        if t[1] - t[0] < depth
          depth = t[1] - t[0]
          normal = t[2]
        end
      end
    end

    [normal, depth]
  end

  # 撃力を加える(とりあえず止める)
  def apply_impulse(ary, normal)
    ary[0].v -= normal.dot(ary[0].v) * normal
    ary[1].v -= normal.dot(ary[1].v) * normal
  end

collision_resolveメソッドではまず、位置と状態を前のフレームまで戻して少しずつ進めてなるべく正確な衝突位置を求める努力をする。これをせずに1フレーム動かした結果だけで判定しようとすると結構辛いことになる。うまいこと計算で出せるならそのようにするのがより軽い。例えば円と円の衝突などでは一発で算出できる。らしい。
無駄が多そうではあるが、DXRubyのSprite.checkを使って先に判定していてcollision_resolveが呼ばれるときには当たっていることが前提であるので、これが呼ばれる回数も限定されるはず。
んで、PhysicsSpriteのcollision.sizeを見て形状を判定して(いまは四角しかない)、衝突の向きと深さを求め、その値を使って当たっていない位置まで戻す強引な補正を入れて、撃力を加え、残りの時間を進める。まあ、結構適当ではある。
補正処理ではなんだか回りくどい計算をしているが、質量をFloat::INFINITYとして無限にする場合(壁とか)には、無限が分子側に来ると計算できないので、細工して分母側にしてやる必要があってこうなる。逆に質量を0にするのは禁止事項である。
apply_impusleは本来であれば衝突位置から反発を計算して撃力を加えるメソッドだが、現時点では反発係数などの計算はしていなくて、とりあえずその場で停止させるように計算している。衝突位置の計算難しい。

PhysicsSprite側

ちょっとだけ。

class PhysicsSprite < Sprite
  def xy
    Complex(self.x, self.y)
  end

  def xy=(c)
    self.x, self.y = c.rect
  end

メソッドを2個追加した。Sprite#xとSprite#yが別々だと色々と面倒なのでxyとしてComplexで扱えるようにしてある。

サンプル

これで四角が質量無限の四角と衝突できるようになった。反発はしないが、例えばジャンプゲーで自キャラと壁の衝突は意外に厄介なものであり、この手の物理エンジンで処理して楽をするというのはよくある話である。このようなコードを書くとジャンプゲーっぽい動きを確認することができる。

world = PhysicsWorld.new

s1 = PhysicsBox.new(300, 0, 20, 20)
s1.force[:gravity] = 0.1i # 重力加速度を設定
world.add(s1)

# 床
s2 = PhysicsBox.new(0, 460, 640, 20)
s2.mass = Float::INFINITY
world.add(s2)

# 空中の床
s3 = PhysicsBox.new(220, 360, 200, 20)
s3.mass = Float::INFINITY
world.add(s3)

# 空中の壁
s4 = PhysicsBox.new(200, 340, 20, 40)
s4.mass = Float::INFINITY
world.add(s4)

# 空中の壁
s5 = PhysicsBox.new(420, 340, 20, 40)
s5.mass = Float::INFINITY
world.add(s5)

Window.loop do
  s1.v = Input.x * 3 + s1.v.imag * 1i
  s1.v = s1.v.real - 4i if Input.key_push?(K_Z)
  world.step
  break if Input.key_push?(K_ESCAPE)
end


コードはこちらに置いてある。

おしまい

回転できる四角を作ろうと思うとたぶんかなり難しいのだろうな、などと思いつつ。簡単な物理エンジンなので四角が回転できなくてもいいんじゃないの、とかも思いつつ。
どこまでできるかわからないが、もしかしたらこれで終わってしまう可能性も・・・?