Chipmunkのエッセンス(5)

物理演算の本質は衝突の処理である。少し前に書いたように衝突タイミングと接触点を算出するのが難しいが、これは擬似的な算出方法でなんとかできる。しかしそれ以外にも厄介な問題が潜んでいる。今回はそのあたりを少し。

接触した状態
たとえば固定された床があったとして、その上に四角いブロックが乗っていたとする。このブロックは停止しているが、毎ステップの計算で重力加速度により下向きに加速され、下に移動した結果、床と衝突して、跳ねる。もしブロックの上に他のブロックがたくさん乗っていたら、衝突と反応の連鎖でブロックが何もしていないのにぴょんぴょん飛び跳ねることになる。
まじで?
試してみよう。現在のcp.rbにはStaticじゃない箱のクラスが無いのでおもむろに追加する。

class CPBox
  attr_accessor :body, :shape

  def initialize(x, y, width, height, mass)
    # 頂点配列作成
    verts = [CP::Vec2.new(-width/2, -height/2),
             CP::Vec2.new(-width/2, height/2),
             CP::Vec2.new(width/2, height/2),
             CP::Vec2.new(width/2, -height/2)]

    # 慣性モーメントを計算する
    moment = CP::moment_for_box(mass, width, height)

    # Bodyを作る
    @body = CP::Body.new(mass, moment)
    @body.p = CP::Vec2.new(x + width / 2, y + height / 2)

    # Shape作成
    @shape = CP::Shape::Poly.new(@body, verts, CP::Vec2.new(0, 0))

    # 画像作成
    @image = Image.new(width, height, C_WHITE)

    @x, @y = x, y
    @width, @height = width, height
  end

  def draw
    Window.draw_rot(@body.p.x - @width / 2, @body.p.y - @height / 2, @image, @body.a * 180.0 / Math::PI)
  end
end

ちょっと長くなるので続きは以下に。
次に、箱を4つほど床の上に重ねた状態で作成するようなコードを書く。

require './cp'

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

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

# 箱を作る
boxes = []
4.times do |i|
  box = CPBox.new(100, i * 50 + 200, 200, 50, 1)
  box.shape.e = 1.0 # 弾性(0.0〜1.0)
  box.shape.u = 1.0 # 摩擦(0.0〜1.0)
  boxes.push(box)
end

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

# Spaceに追加
boxes.each do |box|
  space.add(box)
end
space.add(wall)

# Space#stepで時間を進める。引数は秒。
Window.loop do
  space.step(1.0/60.0)
  boxes.each do |box|
    box.draw
  end
  wall.draw
end

これを動かすと、真っ白なのでわかりにくいかもしれないが(適当に色をつけてください)、縦に積み重なった状態で開始する。1ステップごとの重力加速度は微々たるものなので跳ねる挙動は目に見えないが、じーっと見ていると横にわずかにズレたりするのがわかるだろう。重力に引かれる物体は静止しないのである。摩擦MAXにもかかわらず、だ。

Chipmunkでは物体の表面に薄く「接触範囲」というものを持っていて、この範囲内であれば衝突はしていないが接触していると判定する。この物理学的にありえない計算により、静止問題を押さえつけようとしているようだが、完璧とまではいかないようだ。
この手の問題は軽い処理が必要なゲーム用物理演算には鬼門と言える。

■多体問題
現実の世界でも、物体を積み上げると1つの物体が他の複数の物体と同時に接触するという状態が発生し得る。このとき、どれか1つを動かすと、その力は同時に他の物体にかかるはずである。この「同時」というのが難しい。シミュレーションされた世界の中では衝突判定は1つずつ解決されるものであり、同時に衝突したからといって同時に力を加える計算は気が遠くなるほど厄介で、普通の物理演算ライブラリでは普通にひとつずつ計算する。
例えば、4個の同じ大きさのブロックを縦にぴったり(同じx座標に)並べた状態で、横に縦長のブロックを置き、真横に動かす。すると計算上はまさに理想的に同時に衝突することになる。イメージとしてはすべてのブロックは衝突した結果、真横に移動する気がする。ところが上から順に衝突が解決されたとしたら、縦長のブロックの一番上の部分と、並んだブロックの一番上のやつが衝突し、縦長のブロックは少し斜めになる。2番目のブロックは斜めになった縦長とぶつかり、以下省略。結果的に、理想的に同時に衝突したのとは明らかに違う挙動になる。
試してみよう。

require './cp'

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

# 箱を作る
boxes = []
4.times do |i|
  box = CPBox.new(300, i * 60 + 200, 200, 50, 1)
  box.shape.e = 1.0 # 弾性(0.0〜1.0)
  box.shape.u = 1.0 # 摩擦(0.0〜1.0)
  boxes.push(box)
end

# 壁を作る
wall = CPBox.new(40, 200, 100, 230, 10)
wall.shape.e = 1.0 # 弾性(0.0〜1.0)
wall.shape.u = 1.0 # 摩擦(0.0〜1.0)

# Spaceに追加
boxes.each do |box|
  space.add(box)
end
space.add(wall)

wall.body.velocity_func do |body, gravity, damping, dt|
  body.v += CP::Vec2.new(Input.x * 10, 0)
  body.w = body.w * damping
end

# Space#stepで時間を進める。引数は秒。
Window.loop do
  space.step(1.0/60.0)
  boxes.each do |box|
    box.draw
  end
  wall.draw
end

縦長のブロックは左右にカーソルキーで動かすことができるようにしてある。適当な速度でぶつけてみよう。縦長のブロックは派手に回ることはないが、若干斜めになったのがわかるだろう。4つのブロックも少し傾いたはずだ。

まあ、ゲーム用に軽い物理演算を作るなら、こんなところで重い計算をしてられない、ということだ。多体問題は解決するには厄介すぎる問題である。

■おしまい
今回はこのへんで。