Chipmunkのエッセンス(2)

Chipmunkは古典力学のうちニュートン力学の一部を計算するものである。それだけでもゲーム用にリアルタイム演算するのは骨が折れる。厄介なのは衝突判定、衝突タイミングの算出、衝突後の状態の計算の3つで、最後のは力学に忠実に計算すればいいが、前の2つは忠実にやると計算量が大変なことになるので、いかにしてそれっぽく計算しつつ端折るかが勝負になる。たとえばChipmunkでは高速にすれ違う物体の位置が1フレームで入れ替わると衝突を検出できない。同様に長い棒が高速回転していてもダメである。まあこのへんはゲームだしって感じ。

■とりあえず簡単にクラス化する
前回の記事ではダラダラとコードを書いてしまったが、基本部分はあれでOKだろうということで、クラス化しておく。機能的にもカプセル化的にもいまいちなものだが、Chipmunkを使って何かしようと思う人は参考になるかもしれない。今後のサンプルコードが短くなるというのが主な理由である。ファイル名はcp.rbとしておこうか。

require 'chipmunk'
require 'dxruby'

class CPCircle
  attr_accessor :body, :shape

  def initialize(x, y, r, mass)
    # 慣性モーメントを計算する
    moment = CP::moment_for_circle(mass, 0, r, CP::Vec2.new(0, 0))

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

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

    # 画像作成
    @image = Image.new(r * 2, r * 2).circle_fill(r, r, r,C_WHITE).line(0, r, r, r, C_BLACK)

    @r = r
  end

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

class CPStaticBox
  attr_accessor :body, :shape

  def initialize(x1, y1, x2, y2)
    # CP::Space::STATIC_BODYを使うのでBodyは無し
    @body = nil

    # Shape作成
    verts = [CP::Vec2.new(x1, y1), CP::Vec2.new(x1, y2), CP::Vec2.new(x2, y2), CP::Vec2.new(x2, y1)]
    @shape = CP::Shape::Poly.new(CP::Space::STATIC_BODY, verts, CP::Vec2.new(0, 0))

    # 画像作成
    @image = Image.new(x2 - x1 + 1, y2 - y1 + 1, C_WHITE)

    @x, @y = x1, y1
  end

  def draw
    Window.draw(@x, @y, @image)
  end
end

# Spaceクラスにメソッド追加
class CP::Space
  # StaticなBody
  STATIC_BODY = CP::Body.new_static
  STATIC_BODY.p = CP::Vec2.new(0, 0)

  def add(s)
    self.add_body(s.body) if s.body
    self.add_shape(s.shape)
  end
end

前回ラストのコードと同じことをするためには、このように記述することになる。

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, 10, 1)

# 壁を作る
wall = CPStaticBox.new(0, 400, 639, 479)

# Spaceに追加
space.add(circle)
space.add(wall)

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

若干違うのは円の回転が見えるように線を入れたことと、描画がdrawからdraw_rotに変わって回転描画ができるようになったことだ。Body#aで物体の姿勢を取得することができるがラジアンなのでDXRubyで使う場合は変換してやる必要がある。あとは、Bodyを作るときにCP::moment_for_circleを使って慣性モーメントを計算しているところか。
なんにせよ、こうしておくと複数の物体を生成するときにコードがそんなに増えない。

■ボールを2個出す
ということで2個生成してぶつけてどうなるかを見てみよう。

require './cp'

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

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

# ボールを作る
circle1 = CPCircle.new(100, 100, 10, 1)
circle2 = CPCircle.new(95, 350, 10, 1)

# 壁を作る
wall = CPStaticBox.new(0, 400, 639, 479)

# Spaceに追加
space.add(circle1)
space.add(circle2)
space.add(wall)

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

3行増えただけである。クラス化してなかったらどんだけ増えたことやら。
さて、これを動かしてみると、1つは下のほうにあって、もう1つが上のほうから落ちてきて、ぶつかる。なんとなくそれっぽく横にずれて、地面を滑っていくのが見えただろう。
これが物理演算か!すげー!って思った人は素直な良い子である。普通レベルの人はちょっと拍子抜けしたのではなかろうか。何を期待しただろう。もっと景気良く跳ねるのを期待しただろうか。それともごろごろと転がっていくのを期待しただろうか。確かにそういう挙動をしてくれれば物理演算ぽいわけだが、それらはそのような特性を持った物体の反応である。予備知識無しに期待する挙動がおそらく人によって違うのは当たり前で、Chipmunkの物体はユーザ個々人の期待に沿うためにそれぞれが期待する挙動にあわせた物質特性の指定が必要である。
今回のこの動きも、物体に設定された特性に従って忠実に計算した結果なのだ。

■特性の指定
Chipmunkで物質の特性を設定するには、Shape#e=とu=を使う。eが弾性で、uが摩擦を表す。それぞれ0.0〜1.0で表現し、物体同士が衝突したときに両方の値の積で反応が決定する。2つの物体の弾性が両方とも1.0だと完全弾性になるし、摩擦が両方とも0.0だと理想的に滑るようになる。Chipmunkでは初期値はどちらも0.0。だからさっきのコードでは全く跳ねなかったし、滑っていたのである。

require './cp'

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

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

# ボールを作る
circle1 = CPCircle.new(100, 100, 20, 1)
circle2 = CPCircle.new(95, 350, 20, 1)

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

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

# Spaceに追加
space.add(circle1)
space.add(circle2)
space.add(wall)

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

全部1.0にしてみた。ついでに見やすいようにボールを大きくしておいた。どうだろう。完全弾性なのでよく跳ねるし、こんどはちゃんと回転するようになったはずだ。

■とりあえず
長いわりに進んでないがここまで。