前回は、ブロック崩しゲームの弾を飛ばして、壁と弾性衝突するシーンを作りました。
今回はここにブロックを並べて、ブロックと弾がぶつかったらブロックが消えるという処理を解説します。
すぐ終わると思ったのに、予想以上に時間がかかりました。これはEditorの問題ではなく、私が物理エンジンHavokの理解に時間がかかったことが原因です。
- 1. Playter.tsを作ってPlayerとなるmeshのPhysicsBodyを適切に設定する
- 2. BlockManager.tsから各ブロックのobservable を作り、球と衝突したら requestAnimationFrameでフレームレートを下げてからblockを削除する
- 3. Player.tsをアタッチしたメッシュに、微細な速度減衰がデフォルトで入っているので無効化する
- 4. 球がXZ平面のみで動くように、Y方向の速度成分を常に0にする
- 5. 動作確認
- 6. おわりに
環境 Babylon.js Editor v5.1.10
1. Playter.tsを作ってPlayerとなるmeshのPhysicsBodyを適切に設定する
今回のブロック崩しでは、Playerは細長いブロック状のメッシュです。これが動いて球と衝突する必要があります。
球と衝突するには物理エンジンの反映が必要です。物理エンジンを有効にする場合、meshを作ったあとでaggregateというrigidbody相当のコンポーネントを別で定義します。
ここまではUnityでも同じなのですが、Havokの場合aggregateの定義を細かく設定しないと、期待した動作にならないという問題があります。
たとえば、meshとaggregateは完全に別々の物体?のように振る舞うため、設定によっては、meshを動かしても物理エンジン用のaggregateだけがその場に取り残されるという現象まで起きてしまいます。
これを回避するには、PhysicsBodyが持っている2x3種類のパラメータのうち、適切なパターンを指定する必要があります。
下記の記事のおかげで整理できました。
この記事をもとに検証し、Playerとなるmeshはこのように設定しました。
this._aggregate.body.setMotionType(PhysicsMotionType.DYNAMIC); this._aggregate.body.setPrestepType(PhysicsPrestepType.ACTION);
ちなみに、6種類のパラメータの組み合わせをわかりやすく表示しているPlaygroundはこちらです。
このサンプルコードで確認できます。
https://playground.babylonjs.com/#E9R16H#1
2. BlockManager.tsから各ブロックのobservable を作り、球と衝突したら requestAnimationFrameでフレームレートを下げてからblockを削除する
以前のBabylon.js Editor v4のときは、Block meshをInspectorで1つずつ作って配置していました。UnityでいうInstantiateではうまくいかない箇所があったためです。
今回はいけそうだったので、BlockManager.tsというファイルを作ってコード上で生成しました。
Babylon.jsの場合、MeshBuilder.CreateBox()という関数を使います。これをfor文で8回繰り返して8個のブロックを作りました。
このあと、observableを使ってblock別に衝突検知をつけました。ただし、単なる衝突検知だと、ブロックが衝突直後に消えて、球が弾性衝突しないで停止してしまうという問題が発生しました。
そのため、いろいろ試した結果、requestAnimationFrame()を使ってブロックを消す処理を2フレームだけ遅らせることで、弾性衝突→ブロックが消えるという処理にしました。
const observable = (aggregate.body as any).getCollisionObservable?.();
if (observable && typeof observable.add === "function") {
const observer = observable.add((collisionEvent: any) => {
// notify the shared GameManager instance
GameManager.instance?.setBlockCount(1);
// unregister observer
try { observable.remove(observer); } catch { /* ignore */ }
// mark this block for deferred removal using the existing _blocks array
const found = this._blocks.find(b => b.mesh === block);
if (found) {
found.marked = true;
} else {
this._blocks.push({ mesh: block, aggregate, marked: true });
}
if (!this._removalScheduled) {
this._removalScheduled = true;
requestAnimationFrame(() => requestAnimationFrame(() => {//2フレームだけ処理を遅らせる
this._processPendingRemovals();//ここでブロックやブロックのPhysicsBodyを削除する
this._removalScheduled = false;
}));
}
});
}
本来は、物理的無処理が発生した直後に呼ばれるscene.onAfterPhysicsObservableを使う方がよいはずなのですが、onAfterPhysicsObservableを使ってもうまく解決できなかったのでこうなりました。
もう少しHavokを使いこなせると、別の解決策が出るかもしれません。
この記事も参考にさせていただきました。
3. Player.tsをアタッチしたメッシュに、微細な速度減衰がデフォルトで入っているので無効化する
弾性衝突させるには、restitution=1 (反発係数) とfriction=0 (摩擦係数) にします。もちろんこのように設定していました。
this._aggregate = new PhysicsAggregate(
this._mesh,
PhysicsShapeType.BOX,
{
mass: 0,
restitution: 1,
friction: 0
},
scene
);
しかし、何度か球とブロックと衝突すると球の速度が明らかに減衰しました。いろいろ調べると、動く物体 (PhysicsMotionType.DYNAMICにしたもの) は、速度減衰を受けるのがHavokの仕様のようです。
この速度減衰を無視するには、以下のようにsetLinearDampingとsetAngularDampingで2種類の値を0にする必要があります。これらの値を表示させたところ、LiearDampingは0でしたが、AngularDampingは0.1になっていました。そのため、何度か衝突することで速度低下が起きていました。
this._aggregate.body.setLinearDamping(0); this._aggregate.body.setAngularDamping(0);
このように設定したところ、繰り返し弾性衝突できるようになりました。
4. 球がXZ平面のみで動くように、Y方向の速度成分を常に0にする
何度か衝突しているうちに、球がY方向に移動してしまって、枠外に出ることがありました。不定期に発生し、発生しないこともありました。
また、単純にpotision.y=0みたいな処理を入れても、y方向のpositionには反映されませんでした。
結論として、PhysicsBodyのy方向の速度を常にゼロにすることで解決できました。
5. 動作確認
いろいろと試行錯誤した結果、このように動かせるようになりました。
6. おわりに
Havoksを使うと、高度な処理は間違いなくできると思いますが、今回みたいな単純な処理で構成されるゲームを作るには、相当に慣れが必要そうです。
次回は、GUIを追加して、残りのブロックの数やクリアメッセージなどを表示させる方法を解説します。


