Chipmunkのエッセンス(4)

ChipmunkにはDemoプログラムが用意されていて、公式のダウンロードサイトで実行ファイルをダウンロードすることができる。http://chipmunk-physics.net/downloads.php
このデモはアルファベットのA〜Sのキーで色々なものを動かすことができるのだが、Pを押すと2Dアクションゲーのような動きをするデモが見れる。昨日の記事ではいまいちうまくいかなかったパターンがうまく行っているようにも見えるが、細かくチェックすると微妙に壁に埋まってるし、色々試していると変な動きをしたりもする。やはりこういうのは難しいらしい。
今回は自キャラの動きを制御することについての続き、再チャレンジである。

■Chipmunkの外と内
昨日のプログラムはChipmunkの世界(CP::Spaceオブジェクト)の中にあるBodyをChipmunkの外側から操作していたわけだが、ChipmunkのシステムはSpace#stepメソッドの中で一連の計算を順番にしていて、本来Body#vなどを変更すべきタイミングというのがある。
そのタイミングを知らせてくれる機能がBody#velocity_funcで、このメソッドにブロックを渡してやると、そのBodyのvとwを変更すべきタイミングでブロックを呼び出してくれる。メソッドの定義はこうだ。引数はブロック以外に無い。

Body#velocity_func(){|body, gravity, damping, dt|...}

このブロックが受け取るbodyはブロックを登録した先のbodyで、gravityはspaceに設定した重力、dampingは摩擦による減衰、dtはstepの引数となる。
これらの値はデフォルトのCのコードではこのように使われる。Rubyとは書き方が違うので読みにくいかもしれないが。

void
cpBodyUpdateVelocity(cpBody *body, cpVect gravity, cpFloat damping, cpFloat dt)
{
	body->v = cpvclamp(cpvadd(cpvmult(body->v, damping), cpvmult(cpvadd(gravity, cpvmult(body->f, body->m_inv)), dt)), body->v_limit);
	
	cpFloat w_limit = body->w_limit;
	body->w = cpfclamp(body->w*damping + body->t*body->i_inv*dt, -w_limit, w_limit);
	
	cpBodySanityCheck(body);
}

dampingは今までのbody#vとの積を取る形で減衰を表現している。それからfとm_inv(Rubyからは参照できないが質量の影響の逆数)の積とgravityを足したものにdtを掛けている。これらを足して、最後にv_limitで最大値に制限をかけている。ようするに、触れている物体との摩擦をdampingとして渡してくれてるのでvと掛けることでvを減らして、gravityはdtを掛けて、それらを足せば新しいvになるということだ。
同様にw(角速度)のほうも摩擦やトルクから値を計算している。wもw_limitにより上限の制限が入っている。
これが速度と角速度の計算であって、このデフォルトの計算を差し替える形でキーボード操作によるvの変更などを突っ込んでやれば、もう少しChipmunkの世界になじむはずである。
ちなみにこのコードの中には衝突による反応の計算は無いので、そのへんはChipmunkがやってくれることになる。
■velocity_funcを書いてみる
Body#velocity_funcを定義したコードはこのようになる。あんまり跳ねてもアレなので各値は調整してある。ボールは重力に引かれて落ちるように計算しているが、左右に関してはキーを押してないと移動量0、押している間は等速度運動するようになっている。昨日ダメだったパターンだ。ついでにスペースキーで浮くようにしてみた。

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.2 # 弾性(0.0〜1.0)
wall1.shape.u = 1.0 # 摩擦(0.0〜1.0)

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

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

circle.body.velocity_func do |body, gravity, damping, dt|
  body.v = CP::Vec2.new(Input.x * 120, body.v.y) + gravity * dt
  body.w = body.w * damping

  if Input.key_down?(K_SPACE)
    body.v = CP::Vec2.new(0, -100)
  end
end

# Space#stepで時間を進める。引数は秒。
Window.loop do
  space.step(1.0/60.0)
  circle.draw
  wall1.draw
  wall2.draw
end

fとtはキッパリと無視した。どうだろう。昨日よりもずっと良くなったのではなかろうか。Chipmunk内での物体の移動はChipmunkの世界の中でやれ、ということだ。
ただ、この例では壁の角ギリギリに乗った場合になかなか落ちない、という妙な挙動が見られる。これは重さにより下に転げ落ちるようにChipmunkがvを設定しているところを、毎回初期化してしまっているからである。この点についてはキャラが転げ落ちて欲しいのか、それとも床に乗っている限りは落ちて欲しくないのか、ゲームに期待する挙動に合わせて作り込むことになる。摩擦を減らすと滑り落ちるだろうし、自キャラが矩形ならまた話は違ってくるし、回転して欲しくなければ慣性モーメントをINFINITYにしてやればよい。設定次第である。
ちなみに壁の弾性を0にするとやっぱり少し壁に埋まる動きをする。この現象は避けられないようだ。

■最後に再びデモの話など
ChipmunkDemoのキャラは壁にくっついたりできるが、これをするには衝突判定の情報の取得が必要である。衝突判定については後日のネタにするが、デモと同じことをするにはRubyバインダでは取得できない情報が必要になりそうで、たぶんできないのではないかと思っている。まあこっちはDXRubyを使っているのでSpriteを駆使して判定すればどうにかできないことも無いが、そこまで力技で実現するほどでもないのかなーとかなんとか。
ということで今日はここまで。