pcat (6) 地面に立つ
前回は猫と地面、もとい丸と四角を描くだけというおそろしくつまらない内容でした。今回は丸と四角で衝突処理をして、丸が四角の上に乗っかるようにします。またつまらない回になりそうな予感がしないでもないです。
やることは以下のとおりです。
- 猫は初期位置から下に落ちてくるようにする。
- 猫は地面の上に接したらそれ以上落ちないようにする。
- 猫はキーボードの左右キーで左右に動けるようにする。
- 猫は下に地面がない場所に移動したら下に落ちるようにする。
コードは前回の終了時点のものをそのまま引き継いで使って、付け足していくようにします。
自由落下
イタリアの学者ガリレオ・ガリレイは、重いものほど早く落下するという当時の説が馬鹿げていると考えて、ピサの斜塔から重さの違う2つのものを落としてその間違いを明らかにしようとしたという逸話があります。現代の初歩的な物理の知識によれば、物体を空から落としたとき、空気抵抗がないものとすると、落下速度v、重力加速度g、経過時間tとすると
さて、今ゲームの初期状態では猫は空中に配置されます。ゲームが開始した瞬間そこから落ちていくようにします。比較のために、一旦先の
ついでに猫の初期位置は画面一番上にしておきます。
実行すると次の画面のようになります。
いまいちな感じがします。なので現実の落下速度の法則
ここでgの値をどうするかですが、今のゲームではメートル単位を使っていないし今後も使う予定もありません。したがって9.80665という値は意味をなしません。まず
ここで問題なのが経過時間tをどのように取るかです。一つの方法はCatにメンバ変数としてtをもたせて、落下を開始した瞬間からフレームの更新ごとにtを累積していく方法です。これでもおそらく機能するでしょうが落下が始まるたびにtをリセットしたり加算したり管理が面倒です。もう一つの方法は経過時間を直接扱うのではなく、現フレームの落下速度を前フレームから算出する方法です。
この落下速度を適用するのも難しくありません。
Gravityの値はいくつか試しましたが、結局9.8付近がちょうど良さそうでした。これを実行すると次のようになります。
まずまずな感じなのでこれを採用します。
地面の上に立つようにする
地面の上に立つとは、もう少し噛み砕くと、ちょうど地面の上に猫が接するような位置で止まるようにすることです。まず明らかに必要なことは、地面と猫がぶつかったことを検出する仕組みです。まずはそれだけを考えてみます。
今は猫は上から落ちてくるだけであるので、ぶつかったとき猫は必ず上方向から地面に入ってきたことになります。それ以外のケースは考えないようにします。

まずはここまで作ってしまいます。試験的にぶつかったことを通知するために、グローバルなフラグを使います。フラグがセットされているときは、updateでこれ以上落ちないようにしておきます。そうすることでぶつかったことを視覚的に確認することが出来ます。
今作らないといけないのはコメントにあるようにCatとGroundの交差判定を行う処理です。Catは一つの円、Groundは複数の長方形から出来ています。やることは、一つの円に対し、順番に複数の長方形と交差判定を行っていくことです。ここでまず問題になるのが、円と長方形の交差判定です。円同士や長方形同士の交差判定はさほど難しくないのですが、円と長方形の判定はやや複雑になります。猫を丸にしたことを少し後悔しています。
自力で考えてもいい方法が思いつかなかったのでStack Overflowのこの投稿を検討してみました。Web上のソースコードを利用するときに注意しておきたいのがライセンスです。Stack Overflowの投稿に適用されるライセンスについてはこちらやこちらに書かれています。先の投稿の場合CC BY-SA 2.5ということになります。Creative Commonsはかなり「ゆるい」といったらなんですが使いやすいライセンスです。なので先の投稿のコードを使っても良かったのですが、今回の場合、円と長方形の判定はさして大きな関心ごとではなく、円を長方形とみなして、長方形同士の判定にしても支障はないでしょう。という言い訳をして、猫は丸の見た目をしているにも関わらず、当たり判定があるのはその円に外接する正方形とすることにします。

長方形同士の交差判定は比較的難しくない、というよりSFMLに用意されているのでそれを使います。sf::Rect<T>のメンバ関数intersectsです。intersects関数を利用するためには、sf::Rect<T>が必要になります。これはsf::Shapeを継承するsf::CircleShapeとsf::RectangleShapeのメンバ関数getGlobalBoundsで取得することが出来ます。この2つを使って交差判定を書きます。
実行すると次のようになります。
背景が青だと黒丸が見にくいので白にしました。
上の場合はあまり明らかではないけど、落下速度の設定によっては地面にめり込んでしまいます。試しにconst float Gravity = 29.8;
として、Gravityを9.8から29.8に変更してみます。
今度ははっきりと地面にめり込んでいるのが確認できます。期待するのは地面にめり込まずにちょうど接する位置で止まることです。次はこれを修正することにします。
あるフレームのCat::updateにおいて、ちょうど交差していると判定されるとします。このとき、交差していると判定される以上、猫と地面は接しているのではなく、少なくとも1ピクセル以上は重なっている、つまりめり込んでいることになります。別の言い方をすると、ちょうど接しているフレームというのは交差判定からは検出できないことになります。したがって、地面に接する位置で止まるようにするために取れる手段は、いったん交差していることを検出してから、ぎりぎり交差しない位置まで猫を押し戻すことです。
問題となるのが、どれだけ押し戻せばいいかです。いくつか方法が考えられますが最も単純なのは次のようなものです。
- 上から落ちてくることはわかりきっているので、押し戻す方向は上方向である。
- ①猫を1ピクセル上に移動させてみる。
- ②ぶつかった地面のブロックと交差判定を行う。
- まだ交差していれば、交差しなくなるまで①②を繰り返す。
- 交差しなかったときがちょうど接する位置なので、このフレームの位置として更新する。
これをコードにしてみます。
実行すると、ちょうど地面の上に丸が触れるところで止まっているようになります。
一見うまく行っているように見えるですが、微妙なバグを含んでいます。それはSFMLのShape(を継承するCircleShapeやRectangleShapeを含む)のdraw関数の浮動小数点数の扱いが原因です。drawの動作をいくつか試して確認してみたところ、どうもfloatとして保持するpositionを実際に描画するときにはround(四捨五入)した位置に描画するようです。
具体的にどのようになるかというと、CircleShapeのc1とc2があるとします。c1のposition.yが100.4でc2のposition.yが100.5だとします。そうすると、c1はピクセル座標100に、c2はピクセル座標101に描画され、1ピクセルのずれが生じます。この挙動が望ましいかどうかは場合によります。今回の場合は、そのような動作は期待していませんでした。
どのような結果になるかを、先のコードを書き換えて見てみます。変更点は以下のとおりです。
- ぶつかったら更新をストップすることで、ごまかしていたのをやめる。
- 地面と交差していたら落下速度を0にリセットする。
- Cat::draw関数で、shape_変数のコピーを作成して、切り捨て(truc)と四捨五入(round)で違いを確認できるようにする。
これを取り込んだコードは次のようになります。
実行すると、3つのCircleShapeが描画されます。
左の円から順に上記コードのコメントにある①②③です。よく見ると②と③は下まで行ったらガクガク震えているのがわかります。①は安定しています。
さらによく目を凝らしてみると、①の下に1ピクセルの隙間があることがわかります。これは交差判定の処理が原因のバグです。sf::Rect<float>、つまりsf::FloatRectのintersects関数は、矩形の保持する情報の型であるfloatを使って交差判定を行います。そのため、例えば地面のトップが10.0、猫のボトムが10.1となったとき、これは交差すると判定されます。そして猫を1ピクセル上に移動させるので、猫のボトムは9.1となります。これをdrawのとき切り捨てて描画するので1ピクセルの隙間が出来ます。これも合わせて修正するには猫の当たり判定を行う矩形の座標も切り捨て(trunc)してしまう必要があります。
こうすることで①で見られた1ピクセルの隙間もなくなり、期待する結果が得られます。
しかし、これらは応急処置に過ぎず、根本的な解決策ではありません。今解決策を追求していくと、本題から逸れてしまうので一旦これで進めてみます。
地面の上を歩く、そして落ちる
次のステップは、着地後に左右に歩けるようにすることです。まずキーを受け付けなければいけません。SFMLでキーボード入力を検出するには2つの方法があります。
- ウィンドウのイベントから取得する方法。
- sf::KeyboardのisKeyPressed関数を使う方法。
イベントで取得するにはmain関数のメインループのイベントを処理しているところに処理を追加します。
イベントでは、キーを押しっぱなしにしたときなど、正確に毎フレームの入力を取得することが出来ません。イベントではゲームプレイの操作感に影響するような厳密な入力の処理を必要とするものを扱うべきではないでしょう。isKeyPressed関数を使えば、正確にキーの状態を取得することが出来ます。今回は手抜きしてイベントの方で処理します。isKeyPressedを使う方法はこちらのチュートリアルが参考になるかと思います。
上のコードではCat::moveという関数で猫を左右に移動させています。この関数はまだ用意していないので追加します。
実行すると次のようになります。
左右に動けるようになって、地面のないところでは再び落ちていくようになりました。
次やることはジャンプできるようにすることです。しかし、だいぶ長くなってきたので一旦ここで切ります。