物理エンジンを作る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
コードはこちらに置いてある。
おしまい
回転できる四角を作ろうと思うとたぶんかなり難しいのだろうな、などと思いつつ。簡単な物理エンジンなので四角が回転できなくてもいいんじゃないの、とかも思いつつ。
どこまでできるかわからないが、もしかしたらこれで終わってしまう可能性も・・・?