円と円の当たり判定と当たった位置
2020/08/10

ずっと気になっていた3Dプリンターが、ついに我が家に届きました! しかし…これで何作ろう?(順番!)
さて今回はメガホンDEポンの、攻撃の当たり判定を紹介します。 単に当たっているかどうかだけではなく、当たったところからボールを跳ね返す処理をするために、接触した位置を取得する方法も書きますね。
円と円の当たり判定

メガホンの絵は三角形に近いので、円と三角形の方が良いかも知れません。 もっと言うと三角形より扇形の方が近いかも知れません。 ただし今回のゲームではその違いでプレイ感はほとんど変わらないと思います。 個人ブログということもあり、今回は悩まず単純な方を選びました。 本気で作る場合は色々と試して、 プレイ感に影響がありそうなら良い方、差がなければ実行速度効率の高い方を選ぶべきですけれど。
その円と円の当たり判定は以下のとおりです。 まずボールの円の中心位置を\((ballX,ballY)\)、メガホンの円の中心位置を\((weaponX,weaponY)\)とすると、 その距離は
\(\sqrt{(ballX-weaponX)^2+(ballY-weaponY)^2}\)
で求まります。
ボールの半径は\(16\)、メガホンの半径は\(12\)とすると、この距離が\(16+12=28\)以下であればぶつかっていることになります。フレーム単位でワープするとすり抜ける
『メガホンDEポン』は100ms単位でフレームを切り替えています。 つまり1秒間に10フレーム表示することになります。 これを10fpsと呼びます。
『メガホンDEポン』の中でボールは秒速120ピクセル程度動くようにしています。 つまり10fpsなら1フレームごとに12ピクセルほど動きます。 ゲームでは画像を作る処理負荷が高いので、 あるフレームから次のフレームの画像を作るときには途中は完全に飛ばします。 つまりボールの位置は12ピクセル分瞬間移動、ワープさせることになります。 つまりこの図の左のように飛び飛びに動きます。
この図の右では連続して動いているために、2つの円が当たっていますが、 左の図では飛び飛びのため、先程の式を用いて計算しても距離が28以下になるフレームがありません。 そのため一番近いフレームでも当たったと判定されずにそのまま過ぎてしまい、 見た目にはすり抜けて見える現象が発生します。
プログラム上で細かく刻んで解決
飛び飛び現象を解決する方法は悩ましいところですが、今回はわかりやすく細かく刻んで解決することにしました!
つまり画面は10fpsで更新しつつも、1フレームの処理の中でボールとキャラクターの移動は24分割することにしました。 具体的にはボールやキャラクターの移動速度を\(\frac{1}{24}\)にして移動処理や当たり判定処理を行い、それを24回繰り返します。 1フレームで12ピクセル動かすのに、0.5ピクセルずつ動かして当たり判定も処理して、それを24回繰り返す、ということですね。 それがすべて終わったら、画面に反映するようにします。
プログラム上で無限に分割することはできないのでどこまでいってもワープ処理にはなってしまいますが、 画面に表示されるときに重要なのは1ピクセル以上大きい情報ですので、 分割数を十分に多くして移動量が1ピクセル以下になれば問題ないということになります。
この方法は動かす対象が多かったり、分割数が多いと処理速度が追いつかずに問題になる可能性が高いですが、 『メガホンDEポン』では問題ない…と思います。 まあ問題が有っても個人ブログなので気にしないということで(汗
高速化の工夫
プログラムでは平方根の計算遅いので、
\(\sqrt{(ballX-weaponX)^2+(ballY-weaponY)^2} \leq 28\)
の代わりに、
\((ballX-weaponX)^2+(ballY-weaponY)^2 \leq 28^2\)
を計算するようにしています。ただし今回の例ではそこまで短時間に大量に計算するわけではないので、どちらでも大丈夫です。
例えば画面上にボールが100個あり、それぞれ当たっているかを判定をする場合は、 100個のボールがそれぞれ自分以外の99個と判定するので約1万回の計算が必要で問題になるかも知れませんが、 今回はたかだか24回ですからね。 なんというか、私が長いこと家庭用ゲーム機でプログラミングしていたせいで、無駄な計算を極端に嫌う体質になってしまいまして(汗
もし数学的に解決するなら…
まず前提として、図に黄色で示したメガホン円の中心は\((x_0,y_0)\)から\((x_1,y_1)\)に動き、 緑で示したボールの円の中止は\((X_0,Y_0)\)から\((X_1,Y_1)\)に動くとします。 それぞれ等速に動いていると想定すると、時間を表す実数 \(t (0 \leq t \leq 1)\)を用いてそれぞれの位置、正確には円の中心座標は、
メガホン円の中心座標: \( ( (x_1-x_0)t + x_0 , (y_1-y_0)t + y_0) \)
ボールの円の中心座標: \( ( (X_1-X_0)t + X_0 , (Y_1-Y_0)t + Y_0) \)
ボールの円の中心座標: \( ( (X_1-X_0)t + X_0 , (Y_1-Y_0)t + Y_0) \)
と表せます。この2点間の距離が両半径の合計すなわち28になるtを求めれば良いことになります。
少し見やすくするため以下のように置き換えます。
\( v_x = x_1-x_0 \)
\( v_y = y_1-y_0 \)
\( V_x = X_1-X_0 \)
\( V_y = Y_1-Y_0 \)
\( v_y = y_1-y_0 \)
\( V_x = X_1-X_0 \)
\( V_y = Y_1-Y_0 \)
これを用いると座標はそれぞれ以下になります。
メガホン円の中心座標: \( ( v_xt + x_0 , v_yt + y_0) \)
ボールの円の中心座標: \( ( V_xt + X_0 , V_yt + Y_0) \)
ボールの円の中心座標: \( ( V_xt + X_0 , V_yt + Y_0) \)
なお数学的には、置き換えた変数を最後に戻す必要がありますが、 プログラムで使用する場合は実際に計算してしまえば良いので、以降は置き換える前には戻すことは考えずに進めます。
この2点間の距離の2乗が\(28^2\)になるように式を立てると以下になります。
\( ((v_xt + x_0)-(V_xt + X_0))^2 + ((v_yt + y_0)-(V_yt + Y_0))^2 = 28^2 \)
これを\(t\)についてまとめていきます。
\( ((v_x-V_x)t+(x_0-X_0))^2 + ((v_y-V_y)t+(y_0-Y_0))^2 - 784 = 0 \)
\( ((v_x-V_x)^2+(v_y-V_y)^2)t^2 + 2((v_x-V_x)(x_0-X_0)+(v_y-V_y)(y_0-Y_0))t + ((x_0-X_0)^2+(y_0-Y_0)^2-784) = 0 \)
\( ((v_x-V_x)^2+(v_y-V_y)^2)t^2 + 2((v_x-V_x)(x_0-X_0)+(v_y-V_y)(y_0-Y_0))t + ((x_0-X_0)^2+(y_0-Y_0)^2-784) = 0 \)
ここで
\( A = (v_x-V_x)^2+(v_y-V_y)^2 \)
\( B = 2((v_x-V_x)(x_0-X_0)+(v_y-V_y)(y_0-Y_0)) \)
\( C = (x_0-X_0)^2+(y_0-Y_0)^2-784 \)
\( B = 2((v_x-V_x)(x_0-X_0)+(v_y-V_y)(y_0-Y_0)) \)
\( C = (x_0-X_0)^2+(y_0-Y_0)^2-784 \)
と置くと、元の式は以下になります。
\( At^2 + Bt + C = 0 \)
ここで\( A=0 \) の場合は、\(v_x-V_x = 0\) かつ \(v_y-V_y = 0\) のときのため \( B = 0 \) になります。 そうすと式から\(t\)が消えますね。 実はその状態は2つの円が並行かつ等速に移動していて、2つの円の距離が変わりません。 ここでは初期状態が離れている前提なので、今回の動きでは接触していないことになります。
\( A \neq 0 \) の場合は2次方程式の解の公式により以下が導けます。
\( t = \frac{-B\pm\sqrt{B^2-4AC}}{2A} \)
この式はまず \( B^2-4AC < 0 \) のときは解がありません。つまり接触していません。 \( B^2-4AC = 0 \) なら解は1つで、\( B^2-4AC > 0 \) なら解が2つありますが、 その解が \( 0 \leq t \leq 1 \) を満たしていなければ今回の動きでは同様に接触していません。 条件を満たす解が1つだけある場合は、その時間が接触ポイントです。 満たす解が2つある場合は、2つの円がちょうど接する距離になるのが2回あるということになります。 初期状態が離れている前提なので時間的に先に満たした方、つまりtの値が小さい方が接触したポイントとして意味を持ちます。
tが求まれば、接触した瞬間のそれぞれの円の中心座標は、元の式すなわち
メガホン円の中心座標: \( ( v_xt + x_0 , v_yt + y_0) \)
ボールの円の中心座標: \( ( V_xt + X_0 , V_yt + Y_0) \)
ボールの円の中心座標: \( ( V_xt + X_0 , V_yt + Y_0) \)
のtにそれを代入すれば求まります。
通常は1フレーム分まるまる動かしてしまうとかなりめり込んだ状態になりますので、 プログラムの処理として、それぞれこの接触位置まで戻すような形で、接触による跳ね返りの処理を行います。 そして残り時間つまり1フレーム時間の \( (1-t) \) 倍の分の処理を実行するとより正確でしょう。
まとめ
今回はメガホンDEポンで使用した当たり判定について書きました。
フレーム単位で工夫せずに処理すると抜けてしまう理由と、 そのプログラミング的な、および数学的な解決方法を紹介しています。 数学の方はプログラミングして試すなどはしていないので計算間違いがあったらスミマセン(汗
どちらが良いかは状況によりますが、簡易化できるところは徹底的に簡易化した方が良いです。 最初に円と円との当たり判定にしたこともそうですね。 数学方式を見てしまうと分割する方式はズルっぽく見えるかも知れませんが、簡単にできるなら簡単な方が良い場合が多いです。
次回は、接触した状態からの跳ね返りの処理を紹介しようと思います。 それでメガホンDEポンのネタとしては最後になると思います。
補足
・100個のボール同士ですべて当たり判定を行うには正確には \(\frac{100 \times 99}{2} = 4950\) 回の計算が必要です。・数式表現にMathJaxを使用しております。助かります!
・画像内のラスタライズ文字フォントにOpen Font LicenseのZen Antiqueを使用しております。
カテゴリー:メガホンDEポン,ゲームの数学
Copyright (C) Logic Lovers Inc.