Chipmunkのエッセンス(3)

衝突判定が厄介な理由は前回述べたように、単位時間を経過した後では当たっていないが単位時間内には一時的に当たっていた、という状態の検出が非常に難しいところである。すり抜けを容認せず厳密に計算するのは生半可な計算では無理っぽい。直線運動する円と円なら簡単に算出できるが、重力に引かれた円になるととたんに難しくなり、複雑な形状をした多角形が回転しながら加速していたりすると全く手に負えない。衝突タイミングについてはそもそも衝突したかどうかを厳密に検出できない以上、計算できない。
物理演算では衝突タイミングと衝突点が算出できないと力学計算できないので、そこをどうにかしてそれっぽく算出するというのがキーになる。ゲーム的な衝突判定で検知したとしても、それはたぶん接触じゃなくある程度重なっている状態なわけで、接触点が出せない。物によって作り方は違うが、たとえば衝突した物体だけ時間経過を細かくして再移動させ接触点ぽい点を探すとか、めり込んだ距離から算出するとか、そういう近似的手法で処理したりするのである。
また、単位時間内に衝突した物体の反応を計算しなおすと、同じ単位時間内に他の物体とぶつかることになってしまう、ということは、物体が密集しているとよくある。この場合、そっちとの衝突をまた計算しなおすわけだが、これを繰り返していくといつまで経っても計算が終わらないという話になってしまい、ゲームで使えるものではなくなる。Chipmunkの場合はCP::Space#iterations=で再計算の回数を制限する。ちなみにデフォルトは10である。

■物体をカーソルキーで移動する
ゲームに使うのであれば、キャラを自分で移動させたいと思うこともあるだろう。自キャラが物理法則に縛られたり、他の物体に影響を与えたりするのが望ましいかどうかは作るゲームによるが、今回はそのようなことにチャレンジしてみよう。

require './cp'

# Spaceオブジェクトを作る
space = CP::Space.new

# 重力を設定する(yを+方向に)
# CP::Vec2はベクトルを表すオブジェクト。newの引数はxとy
space.gravity = CP::Vec2.new(0, 100)

# ボールを作る
circle = CPCircle.new(100, 100, 20, 1)

# ボールの特性を指定する
circle.shape.e = 1.0 # 弾性(0.0〜1.0)
circle.shape.u = 1.0 # 摩擦(0.0〜1.0)

# 壁を作る
wall1 = CPStaticBox.new(40, 400, 599, 479)
wall1.shape.e = 0.5 # 弾性(0.0〜1.0)
wall1.shape.u = 1.0 # 摩擦(0.0〜1.0)

wall2 = CPStaticBox.new(300, 300, 340, 399)
wall2.shape.e = 1.0 # 弾性(0.0〜1.0)
wall2.shape.u = 1.0 # 摩擦(0.0〜1.0)

# Spaceに追加
space.add(circle)
space.add(wall1)
space.add(wall2)

# Space#stepで時間を進める。引数は秒。
Window.loop do
  circle.body.p += CP::Vec2.new(Input.x * 5, 0)

  space.step(1.0/60.0)
  circle.draw
  wall1.draw
  wall2.draw
end

真ん中に壁を立ててみた。ついでに床を少し短くしておいた。自キャラはボールで、カーソルキーの左右でBody#pの値を変更している。キャラの位置を直接変更するコードである。これを動かすとキャラは確かに動くのだが、真ん中の壁に突入してもちょっと抵抗を受ける感じがあるだけで、突き抜けてしまう。物理演算が効いているのはキャラが跳ねたり床から落ちたりという動きを見ればわかるのだが、なぜ壁を突き抜けるのだろうか。
原因はボールをRubyのコードで強制的に動かしているところである。ChipmunkはBody#vで設定されているベクトルのぶんだけキャラを移動させ、衝突判定し、接触点を探る。衝突の反応はBody#vとwに設定され、次の移動に使われる。Rubyのコードで座標を変えてやると、Chipmunkはキャラが移動したことを認識できず、物理演算は破綻するのである。ワープしてるわけだ。
移動部分のコードを次のように変えてみよう。座標を変更するのではなく、速度を固定で設定する。

  circle.body.v = CP::Vec2.new(Input.x * 60, circle.body.v.y)

ちょっと移動は遅いが壁を突き抜けることは無くなった。でもなんかめり込む。これはChipmunkの衝突に対する反応の計算の特性で、よりめり込んでいる状態のほうが影響を大きく受けるというルールである。そういえば最初に物体を落としたときも、壁に少しめり込むような動きをしていた。これはChipmunkを使ううえで避けられない挙動と言える。
おそらく自キャラが壁にめり込むのを避けるには、いかにもゲーム的な一定の速度で動く物体というのを実装しないという選択をするしかない。物理的に考えればボタンを押した瞬間に急加速してその後一定の速度で動き続けて、ボタンを離した瞬間に停止する、というのはありえないのである。Chipmunkの世界では、その世界のルールに則った動作をさせなければならない。
よって、正しい実装はこのようになる。

  circle.body.apply_impulse(CP::Vec2.new(Input.x * 10, 0), CP::Vec2.new(0, 0))

Body#apply_inpulseメソッドは第1引数に加える力、第2引数に力を加える点を指定する。第2引数が(0,0)ならBody#vに値を足すのと同等である。重心からずれたところに力を加えると回転することになる。回転速度はBody#wで与えるのと同等で慣性モーメントから計算された値が設定される。
良く似たメソッドにapply_forceというのがあって、こちらはBody#fとBody#tが設定される。この値はstepするたびにvとwに加算されるので、fのほうは重力や風、浮力などを表すのに使える。tは何に使うのかわからん。頭をしばき続けて回転させるような感じか(意味不明)。
結局のところChipmunkで物体を動かそうと思うと、力を加えるという形で加速減速させるのが正解である。自キャラの動きが物理法則に縛られるというのは、Chipmunkのルールに沿って動くということに他ならない。他の物体に物理的な影響を与えるのならば、自キャラもまた物理的な影響を受けなければならないのである。それを外れた物体は物理的にいい感じには動いてくれない、ということだ。

■とりあえず
まったく進んでないがここまで。