本記事はHTML5 Japan Cup 2014で優秀賞を頂いたEnraged Fowlsというゲームについての技術解説後編です。前編はこちらです。
物理エンジン
前編ではスリングショットコントローラーについて説明しましたが、ゲーム本体の技術要素についてはほぼ触れていませんでした。Enraged Fowlsでは弾が飛んでいく動きや、弾がぶつかって崩れるブロックの動きなどを実現するために、物理エンジンを使用しています。物理エンジンは文字通り、物理的な運動をコンピューター上で模擬してくれるライブラリで、特に今回のアプリのような物体同士の衝突を処理する必要がある場合に有用です。Enraged Fowls技術解説後編では、この物理エンジンについて簡単に説明します。
cannon.js
3次元を扱えるものに限っても、JavaScriptで実装された物理エンジンはいくつかあります。Enraged Fowlsではその中からcannon.jsという物理エンジンを採用しました。これは他の物理エンジンの多くが他言語製物理エンジンの移植で、インターフェースに違和感があったのに対して、cannon.jsはJavaScriptで新たに作成された物理エンジンなので、インターフェースが素直に見えたことと内部構造がシンプルだったためです。
なお、実際のEnraged Fowlsのコードは自作のライブラリを使っていて、物理エンジンの説明向きではなかったため、今回使用しているサンプルコードは、すべて説明用に単純化したEnraged Fowlsから抜き出したものになっています。単純化したコードの全文は以下にありますので、適宜参照してください。
全体的な流れ
物理エンジンは、物体の位置や向きを計算してくれるだけで、物体の表示には関知しません。表示に関しては、three.jsをいつも通りに使うことになります。両者を組み合わせる場合の基本的な流れは、次のようになります。
- 物理エンジンを初期化
- three.jsを初期化
- 物理エンジンに物体(形状・位置・向き・速度など)を登録
- three.jsに物体(形状)を登録
- 物理エンジンの物体とthree.jsの物体を関連付け
- 以下を繰り返し
- 物理エンジンに登録された物体の次の位置と向きを計算
- three.jsの物体の位置と向きを、物理エンジンの関連する物体と同期
- three.jsの表示を更新
物理エンジンとthree.jsで、別個に物体を扱っているのが冗長に感じられるかもしれませんが、これには理由があります。両者を区別していることで、1つの物体に対して2種類のモデルを使用できるのです。例えばthree.js内で複雑な人型として扱われているオブジェクトを、物理エンジン内では単なる直方体として扱い、当たり判定を簡略化することができます。
上記のリストで重要なのは、5と6-2です。基本的にはオブジェクトの位置と向きは物理エンジンが管理し、オブジェクトの(表示される)形状はthree.jsが管理しますが、それぞれの情報の関連付けは開発者に委ねられます。5と6-2は、この情報の関連付けを処理しています。
本記事での解説は、上記の物理エンジンに関わる部分を抜き出す形で行います。
物理エンジンを初期化
1 2 3 4 5 6 7 8 |
AngryBirds.Game.prototype = { constructPhysicalWorld: function() { this.cannonWorld = new CANNON.World(); this.cannonWorld.gravity.set(0,-9.82,0); this.cannonWorld.broadphase = new CANNON.NaiveBroadphase(); // ...snip... }, } |
cannon.jsでは、物理エンジンの実体はCANNON.Worldオブジェクトです。物理エンジン全体に関わる設定や処理は、このオブジェクトを通じて行います。設定しているプロパティはそれぞれ、gravityは名前の通り重力、broadphaseは「衝突している可能性のあるオブジェクトの組み合わせを見つけるアルゴリズム」です(注1)。設定できる値はほかにもありますが、とりあえずこの2つをこの通りに設定しておけば動作します。
注1: 通常、物理エンジンは衝突判定をBroad PhaseとNarrow Phaseの2段階に分け、はじめにBroad Phaseで衝突している可能性のあるオブジェクトの組み合わせをなるべく素早く見つけ出し、次にそれらのオブジェクトが実際に衝突しているかどうかを、Narrow Phaseで厳密に検査します。Broad Phaseのアルゴリズムは、物理エンジンのパフォーマンスに大きく影響を与えるため、cannon.jsではプロパティとして与える形でその変更を容易にしています。
物理エンジンに物体を登録
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
AngryBirds.Game.prototype = { constructPhysicalWorld: function() { // 地面 var groundShape = new CANNON.Plane(); var groundWeight = 0; // 固定 this.cannonGround = new CANNON.RigidBody(groundWeight, groundShape); // ...(1) this.cannonGround.quaternion = new CANNON.Quaternion(); this.cannonGround.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); // ...(2) // 鳥 var birdShape = new CANNON.Sphere(0.5); var birdWeight = 10; this.cannonBird = new CANNON.RigidBody(birdWeight, birdShape); // ...(3) this.cannonBird.position.set(0, 2.5 + 2 + 0.1, 0); // CANNON.Worldに追加 this.cannonWorld.add(this.cannonGround); this.cannonWorld.add(this.cannonBird); // ...snip... }, } |
cannon.jsでは、物体はCANNON.RigidBodyオブジェクトとして表され、物体の位置や向きの設定などはこのオブジェクトを通じて行います。ただし、物体の形状はCANNON.RigidBodyオブジェクトからは分離されていて、その指定にはCANNON.Shapeオブジェクトのサブクラスを使用します。上記の例では、CANNON.Plane(平面)とCANNON.Sphere(球体)を利用していますが、それ以外にも以下のような形状が使用可能です。
- CANNON.Sphere: 球体
- CANNON.Plane: 平面
- CANNON.Box: 直方体
- CANNON.ConvexPolyhedron: 凸型多面体
- CANNON.Compound: 上記の組み合わせ
なお平面はデフォルトではz軸に垂直ですが、cannon.jsでは重力はy軸に垂直なので、地面として使用するためにquaternionを設定して、x軸に対して90度回転させています。
物理エンジンの物体とthree.jsの物体を関連付け
物理エンジンの物体とthree.jsの物体を関連付けについては、特に決まったやり方はありません。管理する物体の数が少なければ、単純に1つの物体に対してcannon.js用とthree.js用、2種類のオブジェクトを用意すればいいでしょう。今回の単純化したコードでは、threeFooとcannonFooという2変数で管理しています。
1 2 3 4 5 6 7 8 9 |
AngryBirds.Game = function(opts) { // ...snip... this.threeGround = null; this.threeBlocks = null; this.threeBird = null; this.cannonGround = null; this.cannonBlocks = null; this.cannonBird = null; }; |
実際のEnraged Fowlsでは、three.jsのオブジェクトとcannon.jsのオブジェクトを1つにまとめて使うためのc3.jsという簡単なライブラリを作成しましたが、まだ公開するような完成度ではないので、ここでは説明を省略します。
物理エンジンに登録された物体の次の位置と向きを計算
1 |
this.cannonWorld.step(1.0/24.0); |
物体の位置を更新するには、CANNON.Worldオブジェクトのstepメソッドに経過時間を渡します。これだけで先にaddした物体間の衝突が処理され、位置や向きがそれぞれの運動状態に応じて更新されます。
three.jsの物体の位置と向きを、物理エンジンの関連する物体と同期
1 2 |
this.threeBird.position.copy(this.cannonBird.position); this.threeBird.quaternion.copy(this.cannonBird.quaternion); |
先ほどCANNON.Worldオブジェクトのstepメソッドを呼び出しましたが、このメソッドはCANNON.RigidBodyの位置と向きを更新するだけです。表示位置を更新するには、描画の前にthree.js側のオブジェクトとcannon.jsのオブジェクトの位置を同期する必要があります。cannon.jsはthree.jsと組み合わせて使うことが初めから想定されているため、これは非常に簡単です。three.jsのオブジェクトのpositionプロパティ(位置)とquaternionプロパティ(向き)に、cannon.jsのオブジェクトのそれらをcopyしてください。これだけで位置と向きの同期が実現されます。
ここまでの実装を行うと、次のように衝突がそれっぽく扱われるようになります。
まとめ
モデリングツールを覚えるような時間もモチベーションもなかったため、Enraged Fowlsはthree.jsに用意されている基本図形だけでできています。それでも物理エンジンを使用すれば、その動きで見た目をある程度はカバーできていると言っていいのではないでしょうか。
今回の記事から分かる通り、物理エンジンの利用は存外に簡単です。初期位置を登録してstepメソッドを呼び出し結果を表示に反映させる、これだけです。three.jsを使用して単なるビューアーに留まらないインタラクティブなアプリを作るのであれば、物理エンジンを採用しない理由はありません。本記事を参考に、ぜひいろいろと試してみてください。