物理エンジンを作る
今年最初の記事はまたオモムキを変えて、DXRuby用2D物理エンジン(モドキ)を作ってみよう。
物理エンジンとは
Box2DやChipmunkのような、物体の衝突をなんとなくそれっぽく計算してくれるライブラリである。基本的には剛体(変形しない物体)を扱い、物理エンジン上で管理されている限りそれらが重なったりめり込んだりすることは無い。
難しいところ
問題は物理シミュレーションがある時間単位に計算されるところで、例えば1フレームぶん移動させると衝突した物体はたいがいまず間違いなくめり込んでいる。しかし現実でそのようなことは起こらないので、何かしらの手法でめり込んでいないことにしないといけない。まずこれが最も基本的なことだがいきなり難しい。何が難しいのかと言うと、回転しながら移動する複雑な形状の物体同士が接触した瞬間の位置と姿勢と時間を求めることがきわめて難しいのである。この時点でゲームで使える完璧な物理シミュレーションは作れないことがわかる。実際にはそれっぽければいいので適当にごまかすことになる。
衝突後の反応については力学計算を真面目にするだけなのでそれほど難しくは無い。と思う。たぶん。。。
あと、作っていくと、例えば3つ以上の物体が1フレーム内で同時に衝突する、だとか、積み重ねて静止している物体の扱いだとか、拘束(チョウツガイやバネで繋がってる)とか、超高速移動or回転している物体だとか、凹型多角形だとか、3Dだとか、布やロープみたいな柔らかいものだとか、流体だとか、考えたくもない話が続く。まあ、難しいことはキリがない。
難しいことをしようとすると計算の時間は増えていくので、ゲームに使うならほどほど軽くないといけないし、厳密に、高機能に、高速に、ということを全部取りは無理なので、自分で使いたいレベルの物理エンジンを自分で作るのは間違ったアプローチではない。もちろん技術力の問題はあるが。
目指すところ
Chipmunkがあるのにわざわざ自分で作るのだから、何かしら理由がある。
(a) 遅くてももっと高機能なものが欲しい
(b) 低機能でももっと速いものが欲しい
というのが普通の感じだが、
(c) 作ってみたいだけ
というのもありがちである。今回は、
(d) DXRubyで扱いやすい簡単なものが欲しい
てな感じで、Chipmunkは使うのがちょい難しい感じなのでもっと低機能で遅いけど簡単に使えて扱いやすい感じのものを目指してみたい。
あと、俺は頭が残念な人なのであまり難しいものは作れない。かなり適当になるだろう。厳密な物理シミュレーションをしたければまあChipmunkとかを使うのがよい。
ひながた
物理演算する対象のオブジェクトを登録するPhysicsWorldクラスを作っておこう。PhysicsWorld#addで登録して、PhysicsWorld#stepで1フレーム分の処理を行う。とりあえず60fps限定にしておく。
# 物理演算空間 class PhysicsWorld attr_reader :objects, :hit_pair def initialize @objects = [] @hit_pair = [] end # Worldにオブジェクトを追加 def add(o) o.world = self @objects << o end # 1フレーム分進める def step # 更新 Sprite.update(@objects) # 衝突検出はDXRubyにやらせる @hit_pair.clear Sprite.check(@objects) # 衝突解決 collision_resolve # 描画と後処理 Sprite.draw(@objects) Sprite.clean(@objects) end # 衝突解決 def collision_resolve end end
物理エンジンの流れは、オブジェクトの状態更新→衝突判定→衝突解決→描画、という感じになる。衝突判定はDXRubyのSprite#checkを使って手を抜く。衝突解決処理は衝突判定と並んで物理エンジンの中核になる。今はカラッポ。
エンティティ
個々の物理オブジェクトはSpriteを継承して描画パラメータを持たせることにして、その上で物理演算に必要なパラメータを追加する。衝突判定、衝突解決と同レベルに大事なのが個々のオブジェクトの処理である。
# 物理演算用Sprite class PhysicsSprite < Sprite attr_accessor :v, :mass, :e, :av, :moment, :f, :force, :world def initialize(*) @v = 0 # 速度 @mass = 1 # 質量(適当な値) @e = 1 # 反発係数(とりあえずMAX) @av = 0 # 角速度 @moment = 1 # 慣性モーメント(適当な値) @f = 0 # 摩擦 @force = {} # 力 super end # 毎フレームの計算 def update # 登録されている力を加える @force.each do |key, value| @v += value end # 移動と回転 self.x += v.real self.y += v.imag self.angle += @av super end # 衝突したペアを保存する def hit(o) id1 = self.object_id id2 = o.object_id t = id1 < id2 ? [self, o] : [o, self] world.hit_pair << t unless world.hit_pair.include?(t) end end # 四角 class PhysicsBox< PhysicsSprite def initialize(x, y, w, h, image=Image.new(w, h, C_WHITE)) super(x, y, image) self.collision = [0, 0, w-1, h-1] # 衝突判定範囲 @w = w @h = h @mass = w * h # 質量 @moment = @mass * (w * w + h * h) / 12.0 # 慣性モーメント end end
これぐらいのパラメータがあればそれっぽい動きができるのではないか、と思うのだが、ともあれ摩擦については計算できる自信は無い。
ポイントとしては、速度vはComplexで設定すること、Spriteに衝突ペア配列を返す機能が無いので細工していること、毎フレームかかる力は@forceに設定すること、あたり。特に最後のやつは重力もオブジェクト単位に設定することにしている。重力の影響を受けないオブジェクトを特別扱いするのが面倒だったからである。@forceがハッシュになっているのは後で削除や変更をしやすいように。
サンプル
今はまだ衝突解決ができていないのでぶつかっても何も起きない。以下のようなコードを書いたら小さな四角が落下していくのを確認することができる。この程度である。
world = PhysicsWorld.new s = PhysicsBox.new(300, 0, 20, 20) s.force[:gravity] = 0.1i # 重力加速度を設定 world.add(s) Window.loop do world.step break if Input.key_push?(K_ESCAPE) end
おしまい
年初からごそごそやっていた物理エンジンの開発は派手に頓挫したので0からやりなおし。今回のコードも0から書いた。難しいものができるとは思わないが地味に進めて行けばなんとなくそれっぽいものができるんじゃないかと思っているが、ひょっとしたら再び頓挫するかもしれない。生温かい目で見守って欲しい。