Chipmunkのエッセンス

最近いじってわかってきたことを個人的メモ。

■Chipmunkとは
オープンソースで開発されている2D物理演算ライブラリである。物体に重さや形状などの情報を設定すると、衝突した反応などをリアル(2Dだけど)に計算してくれる。この分野で最もメジャーなのはBox2Dで、まあ次点と言ったところか。RubyバインダもあるのでDXRubyと併用すればそれっぽいゲームが作れる。かもしれない。
ChipmunkはCで書かれているが概念としてはオブジェクト単位となっていて、オブジェクト指向言語と相性がよい。

■基本的なところ
最も基本となるクラスはSpace、Body、Shapeの3つである。
SpaceはChipmunkが扱う物理世界を表し、このオブジェクトに登録した物体について物理現象を再現してくれる。
Bodyは剛体(力を加えても変形しない物体)を表すが、その言葉のイメージとは少し違って、質量や座標は持つが形状や物質特性は持たない。
ShapeはBodyとセットになる形状や物質特性の情報である。
なので、Spaceオブジェクトを生成し、BodyとShapeを生成して関連付けてSpaceに登録して、Spaceの時間を進める、というのが基本の流れになる。

■第一歩
普通にイメージする物体は、1点の座標と姿勢情報と形状を持ち、物理的なパラメータとして重さ、重心、移動ベクトル、角速度、物理特性などがあったりするが、それらは結局は1つの物体の情報であり、わかれるものではない。Chipmunkではそれらのうち、座標と姿勢情報、重さ、重心、移動ベクトル、角速度がBodyで、形状と物理特性(弾性、摩擦)がShapeという形にわかれている。これは妙に思えるかもしれないが、Shapeは円や多角形といった簡単な形状しか扱えないから、これらを組み合わせて複雑な形状を作るためには必要な設計である。とはいえ、Shapeに重さがなく、Bodyに重さの合計と慣性モーメントを持つあたり、やっぱり微妙に変で、直感的ではない。
とりあえず簡単な例を挙げるところから。

require 'chipmunk'

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

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

# Bodyを作る。第1引数は質量、第2引数は慣性モーメント(回転しにくさ)
body = CP::Body.new(CP::INFINITY, CP::INFINITY)

# 円形のShapeを作る。第1引数は関連付けるBody、第2引数は半径、Bodyの座標に対する円の中心の相対位置
shape = CP::Shape::Circle.new(body, 10, CP::Vec2.new(0, 0))

# SpaceにBodyとShapeを登録
space.add_body(body)
space.add_shape(shape)


# Space#stepで時間を進める。引数は秒。
space.step(1.0/60.0)

# Body#pはBodyの座標をCP::Vec2で返す。
p body.p.y #=>0.0

space.step(1.0/60.0)
p body.p.y #=>0.02777777777777778
space.step(1.0/60.0)
p body.p.y #=>0.08333333333333334
space.step(1.0/60.0)
p body.p.y #=>0.16666666666666669

このように使う。Spaceに重力を設定してあるのでstepするたびにBodyオブジェクトのy座標が移動しつつ加速しているのがわかるだろうか。
まずポイントになるのはSpaceに重力(というか重力加速度?)を設定しているところで、これを設定しないと無重力になる。無重力ではせっかくの物理演算が無意味になるかというとそうでもなく、物体同士の衝突に対する反応はきちんと計算できるので意味はある。まあ、物理演算っぽさを出すには重力の影響は欲しいところではある。ビリヤードゲームなどトップビューのものを作る場合はその限りではない。
Bodyを生成している引数はちょっと癖がある。質量は物体同士の相対的な反応を算出するために使われる。ここでは無限大にしているが、無限大にしても重力に引かれる速度は無限にはならず、重力加速度により一定である(当たり前)。有限の質量を持つ物体をぶつけてもびくともしないという意味になる。第2引数の慣性モーメントは回転動作に対する質量のようなもので、これが大きいと回転しにくくなる。無限にすると回転しなくなる。Shapeで設定する形状ごとに慣性モーメントを計算するサポートメソッドがあるので、本来はそれを使って計算して設定する。質量、慣性モーメント共に、複数のShapeを組み合わせて形状を作る場合はそれらの合計値を設定する。
Bodyの座標は初期値(0,0)である。Bodyは大きさを持たない点だが質量があり、慣性モーメントがある。概念上は無限に小さい質点に近い。Shapeを設定しないと形状が無いことになるので衝突が発生しない。このサンプルでは衝突相手がいないわけで、Shapeを設定する意味は無い。
Shapeの引数にはBodyがあるので、Shape側からBodyへの参照を作る。あと、円の半径と相対位置を指定する。これがBodyの衝突判定に使われる。Shapeの形状は他に線を意味するSegmentと、多角形を意味するPolyがある。
で、これらをSpaceに登録して、Space#stepする、というわけだ。

■DXRubyと連携させてみる
数字をテキストで出力させていても面白くもなんともないので、Bodyの位置に円を描画してみよう。

require 'chipmunk'
require 'dxruby'

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

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

# Bodyを作る。第1引数は質量、第2引数は慣性モーメント(回転しにくさ)
body = CP::Body.new(CP::INFINITY, CP::INFINITY)

# Bodyの初期位置設定
body.p = CP::Vec2.new(100,100)

# 円形のShapeを作る。第1引数は関連付けるBody、第2引数は半径、第2引数はBodyの座標に対する円の中心の相対位置
shape = CP::Shape::Circle.new(body, 10, CP::Vec2.new(0, 0))

# SpaceにBodyとShapeを登録
space.add_body(body)
space.add_shape(shape)

image = Image.new(20,20).circle_fill(10,10,10,C_WHITE)

# Space#stepで時間を進める。引数は秒。
Window.loop do
  space.step(1.0/60.0)
  Window.draw(body.p.x-10, body.p.y-10, image)
end

これを動かすと白い円が落ちていくはずだ。
Chipmunkの座標に単位は無く、全体で統一されているだけである。1を1メートルと考えてもいいし、1ピクセルと考えてもいいし、お好きなようにどうぞ、ということだ。DXRubyと連携させる場合はピクセル単位と考えて扱うとラクになる。
気をつけるべきポイントはBodyの座標で、ShapeはBodyの座標を中心に半径10ピクセルの円と設定しているから、つまり円の中心座標ということであり、画像の描画時の指定をそこにすると位置がズレる。この例ではxyともに半径を引いて描画している。そうすると今度は画面の左にはみ出してしまうので初期位置をちょい右に設定しておいた。
Bodyの座標はShapeで設定した形状の重心に位置していなければならない。通常、円であればその中心がBodyの座標になるから、Shapeの相対座標で言うところの(0,0)が中心になる。これをずらすと重さが偏った円が表現できるわけだし、円をたくさん円状に並べて重心を中心(なにもないとこ)に設定してやればポンデリング的なものも表現できる。重心をどこに置くかを決めるのはShapeの相対位置の指定方法であり、複雑な形状を作る場合はユーザがそれを計算してやる必要がある。ということになる。ここがChipmunkの残念なところのひとつである。

■StaticなBody
物理演算ゲーでよくあるのはサイドビューで下に重力がかかり、モノを投げるゲームである。このタイプのゲームにはコース的な障害物が必須となる。ChipmunkではSpaceは無限に大きな空間であり、一番下に地面は無い。普通にコースのBodyを作るとコースごと下に落ちてしまうので、落ちない属性というものが用意されている。

require 'chipmunk'
require 'dxruby'

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

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

# Bodyを作る。第1引数は質量、第2引数は慣性モーメント(回転しにくさ)
body = CP::Body.new(1, CP::INFINITY)
body.p = CP::Vec2.new(100,100)

# 円形のShapeを作る。第1引数は関連付けるBody、第2引数は半径、第2引数はBodyの座標に対する円の中心の相対位置
shape = CP::Shape::Circle.new(body, 10, CP::Vec2.new(0, 0))

# SpaceにBodyとShapeを登録
space.add_body(body)
space.add_shape(shape)

# StaticなBody
static_body = CP::Body.new_static
static_body.p = CP::Vec2.new(0, 0)

# 壁を作る
x1 = 0
y1 = 400
x2 = 639
y2 = 479
verts = [CP::Vec2.new(x1, y1), CP::Vec2.new(x1, y2), CP::Vec2.new(x2, y2), CP::Vec2.new(x2, y1)]
wshape = CP::Shape::Poly.new(static_body, verts, CP::Vec2.new(0, 0))
space.add_shape(wshape)

image = Image.new(20,20).circle_fill(10,10,10,C_WHITE)
wall_image = Image.new(640, 80, C_WHITE)

# Space#stepで時間を進める。引数は秒。
Window.loop do
  space.step(1.0/60.0)
  Window.draw(body.p.x-10, body.p.y-10, image)
  Window.draw(0, 400, wall_image)
end

これを動かすと円が下の壁にボトっと落ちる。
とりあえず、円が無限の質量を持つと不都合極まりないので1にしておいた。それはさておき今回のポイントはStaticなBodyと四角形の壁である。
StaticなBodyとはCP::Body.new_staticで生成できる特殊なBodyオブジェクトで、これは作ってもSpaceに登録できない(エラーになる)。Spaceに登録できないので重力に引かれることは無く動かない。Shapeを関連付けることはでき、こっちはSpaceに登録できる。Shapeの形状はStaticなBodyの座標をベースに相対で表現されて、衝突判定を持つ。Bodyが動かないのでShapeも動かない。ようするに、障害物を作るためのオブジェクトである。
四角形の壁を作るにはCP::Shape::Poly.newを使う。凸型の形状の多角形を作ることができて、座標の配列を指定して作る。ちょっとめんどい。StaticなBodyの座標を(0,0)にしておけばスクリーン座標で設定できてラクになる。また、マニュアルやエラーメッセージでは時計回りと出ているが、スクリーン座標が下向きに+yとなる2Dゲー座標系(数学とyが逆)だから画面上では反時計回りの指定となる。要注意。

■とりあえず
長くなってきたのでここまで。