物理演算の実装方針
プラットフォーマーでは、プレイヤーキャラクターが足場に乗ったり、壁に衝突して止まったりしなければなりません。この挙動を実装するには大きく2つのやり方があると思います。
- 既存の物理シミュレーションエンジンを組み込む。これは比較的かんたんにアクションゲームっぽい挙動を実装できますが、一方で物理演算ライブラリの目的は通常「現実の物体の挙動をなるべく正確に再現すること」なので、ゲームのようにウソ物理演算をしたい場合は逆に変な調節が増えたりします。また、その物理演算ライブラリ自体の理解が必要ですし、物理演算ライブラリは意外とナイーブで壁貫通みたいな謎挙動との戦いも増えます。さらに、WASM-4に関してはカートリッジのサイズに限りがあるため、大掛かりな物理演算ライブラリを組み込むとカートリッジの容量を食ってしまうかもしれないのが心配です
- 簡素な物理演算を自力でコーディングする。これは物理演算についての基礎的な知識が必要で、コーディングも大変です。まあ矩形どうしの交差くらいなら大したことはないのですが、物体が回転したりチェーンのように繋がっての相互作用があったりすると、筆者の知識では実装は厳しいでしょう。その代わりアクションの挙動についてはすべて自分で制御できますし、調整も比較的簡単です。今回はシンプルなグラフィックスのシンプルなプラットフォーマーなので、まあそこまで複雑な物理演算は必要ないでしょう
そんなわけで、今回は自力で簡単な物理演算を実装する方針で行きたいと思います。もし自力で実装が難しいような挙動があれば、それはすっぱり諦めることにします。そういうところにこだわりだすとゲームが完成しないので……。
「衝突」を表現する
基本的な考えかたとしては、こんな感じでいきます。
- プレイヤーキャラクターのAABBを設定する
- プレイヤーキャラクターの周囲にある壁や足場のAABBを設定する
- プレイヤーキャラクターのAABBを速度に従って移動させた新たなAABBを設定し、壁や足場のAABBのひとつを交差判定する
- 双方が重なっていたら、X軸方向へ重ならない位置までプレイヤーキャラクターのAABBを戻す
- もう一度交差判定し、双方が重なっていたらY軸方向へ重ならない位置までプレイヤーキャラクターのAABBを戻す
- 交差していた場合はぶつかったわけなので、速度をゼロにする
- これを周囲すべての壁や足場のAABBに対して繰り返す
「壁と重なったら戻す」を繰り返しているだけです。まあこれだと1フレームで何ブロック分も動く速度が出ていた場合に正確に判定できず壁を突き抜けてしまうんですが、それは最高速度を設けることで防ごうと思います。また、円や傾いた矩形については難しいので考えないことにします。
コードはこんな感じになりました。まあここだけ見てもよくわからないとは思うのですが、プログラミング関連の記事だと言い張るためにコードを載せておきます。
fn move_body(&mut self, vx: f32, vy: f32, world: &World) {
// 壁となるAABBを集める
let walls = self.get_walls(world);
let mut aabb = AABB {
x: self.position.x,
y: self.position.y,
w: self.body_width,
h: self.body_height,
};
// 垂直方向に衝突判定
if vy != 0.0 {
aabb = aabb.translate(0.0, vy);
for wall in walls.iter() {
if aabb.collesion(*wall) {
aabb.y = if 0.0 < vy {
f32::min(aabb.y + aabb.h, wall.y) - aabb.h
} else {
f32::max(aabb.y, wall.b())
};
self.velocity.y = 0.0;
}
}
}
// 水平方向に衝突判定
if vx != 0.0 {
aabb = aabb.translate(vx, 0.0);
for wall in walls.iter() {
if aabb.collesion(*wall) {
aabb.x = if 0.0 < vx {
f32::min(aabb.x + aabb.w, wall.x) - aabb.w
} else {
f32::max(aabb.x, wall.r())
};
self.velocity.x = 0.0;
}
}
}
self.position.x = aabb.x;
self.position.y = aabb.y;
}
壁で跳ね返ったりしたい場合は、交差したときに速度をゼロにするのではなく、速度を反転させて少し遅くしたりすればいいでしょう。
ジャンプのニセ物理演算
私が今回作るゲームはプレイヤーキャラクターはなるべくプレイヤーの思い通りに動かせるように実装を頑張りたいと思います。たとえば、ジャンプの高さはプレイヤーが自在に調節できたら直感的です。
それで、ジャンプボタンを短く押したときは短い(低い)ジャンプ、長く押したときは長い(高い)ジャンプになって欲しいわけです。そんなわけで、「空中で上昇中にジャンプボタンを押していない場合は、急速に加速度を失う」というようなコードを書いて、ジャンプの高さを調節できるようにしました。
if !grounded && !input.is_button_pressed(wasm4::BUTTON_1) && self.velocity.y < 0.0 {
self.velocity.y *= 0.1;
}
これは、現実の物理とは違うニセ物理ですが、ゲームは現実を模倣するのが目的ではないのでこういうことがよくあります。
また、着地点を調整できるように、空中でもわずかに水平方向の加速度を与えられるようにします。ここでは地上に設置していたときの0.05倍の加速度を与えるようにしています。
let speed_scale = if grounded { 1.0 } else { 0.05 };
self.walk(speed_scale * input.horizontal_acceralation());
こんな感じで、何度もプレイを繰り返しながら、直感的な操作ができるように調整していきます。これに正解はないので、操作していて気持ちよければそれでOKです。実際のコードでは、上記以外にももっと様々な細かい調整がされていて、どうするかは開発者のアイデア次第です。自分のゲームの開発の最初のほうだとキャラクターの挙動はひどいもので、マリオなんかの挙動はよく調整されているなと感じます。
このあたりの調整は、開発の序盤である程度完成させておきたいです。プレイヤーキャラクターの挙動が変わると、ステージ全体も調整しなければならなくなってしまうからです。ジャンプがぎりぎり届く距離に設定した足場が、キャラクターの挙動の調整で届かなくなってしまったらゲームがクリアできなくなってしまいます。
次回予告
次回は、開発していたら遭遇した謎のノイズについて対策を考えていきます。いやまあこんな低レベルな(=ハードウェア寄りの)トラブルはWASM-4ならではという感じなのですが、筆者はふだんそんな低レベルな(=ハードウェア寄りの)プログラミングをしてないので、こういう生々しい不具合は新鮮で面白いです。