スクリプト上で Collider の設定をいじって当たり判定を変えるときは注意が必要かもしれない
とあるゲームにバグが出たので調べてみました。
前提
◆以下の内容を見ながら進めていきます。
◆ FixedUpdate から yield WaitForFixedUpdate まで流れをマニュアル通り physics cycle と呼びます。
◆ Unityのバージョンは 2017.2.1 です。
記事の内容
Unity で当たり判定を使う場合は OnTrigger と OnCollision を使用します。
この 2 つは 一度の physics cycle で複数のコライダーと判定することがあります。
ですが時には、
複数回当たってほしくない、一度だけでいい!
という場合もあります。そんな時は 、
GetComponent<Collider>().enabled = false;
といったようにコライダーを無効化して対処してきました。
しかし恐らくこの回避法は間違っている、というのが今回の内容です。
実験してみる
OnTriggerEnter と OnCollisionEnter それぞれの判定を調べるために
①:同時に 100 個の球をターゲットに飛ばす
②:1フレームずつ球をターゲットに飛ばし、計 100 球(100 フレーム分)飛ばす
といったことをしてみました。
また、① ② でターゲットに当たった時に
◎:GetComponent<Collider>().enabled = false;
を呼び出し、ターゲットの判定を消すとどうなるのかも調べました。
想定している挙動は一度だけ当たることです。
結果
※ 赤字は後々考察で使います。
※ FixedTimestep はデフォルトの 0.02 です。
◆ OnTriggerEnter の場合
①:同時に 100 個
全ての球が一度の physics cycle で判定されました。
②:1フレームずつ 100 個
判定が FixedTimestep 分だけズレたものがありますが、 全ての球が判定されました。
① + ◎:同時に 100 個、当たったらコライダー消す
1 つだけ判定されました。
② + ◎:1フレームずつ 100 個、当たったらコライダー消す
1 つだけ判定されました。
◆ OnCollisionEnter の場合
①:同時に 100 個
全ての球が一度の physics cycle で判定されました。
②:1フレームずつ 100 個
判定が FixedTimestep 分だけズレたものがありますが、 全ての球が判定されました。
① + ◎:同時に 100 個、当たったらコライダー消す
全ての球が一度の physics cycle で判定されました。
② + ◎:1フレームずつ 100 個、当たったらコライダー消す
何度か試すと 1 つか 2 つほど判定されます。
また FixedTimestep を 0.2 にすると更に多くの球が一度の physics cycle で判定されます。
21 球目以降は 1 球目の判定が始まった physics cycle では当たっておらず、
次の physics cycle ではコライダーが無いので判定されなかったと考えられます。
ちなみに
◎:GetComponent<Collider>().enabled = false;
を
◎:GetComponent<Collider>().isTrigger = false;
に変更しても結果は変わりませんでした。
コライダー自体の変更が反映されていないようです。
Stay, Exit の場合
上記の OnTriggerEnter, OnCollisionEnter 以外にも
OnTriggerStay, OnTriggerExit, OnCollisionStay, OnCollisionExit
の 4 種でも同様の実験を行いましたが、同じような結果だったので割愛します。
恐らく…
赤字で示した部分を見ると、
OnTrigger ではコライダーを消すことで判定がうまくいきますが、
OnCollision ではコライダーを消してもうまくいきません。
こうなってしまう原因は分かりません。
ただ、マニュアルにもある通り OnTrigger と OnCollidion は FixedUpdate の後に実行されるようです。
つまりコライダー同士がぶつかるなどの物理演算が終わった後に実行されます。
それなのに今更コライダーの設定変えられても困るよ!
ということかもしれないです。
なので本来は OnTrigger でもあまりコライダーを変更しない方が良いのかもしれないです。
結論
◆ スクリプトでコライダーを変更する場合は注意が必要かも。
特に OnTrigger や OnCollision では注意!
yield WaitForFixedUpdate で待ってから変えるのが一番安全かもしれない。
◆一度だけの判定はコライダーをいじるのではなく、こんな感じで bool などを使った方が良さそう。
bool triggerHit = false;
private void OnTriggerEnter(Collider other) {
if (triggerHit == false) {
triggerHit = true;//何らかの処理
}
}bool collisionHit = false;
private void OnCollisionEnter(Collision collision) {
if (collisionHit == false) {
collisionHit = true;//何らかの処理
}
}
おまけ 1:コライダーの変更は Physics と Game logic のどちらで変わるのか
以下の条件を追加してみます。
③:Application.targetFrameRate = 1 にした後、yield WaitForFixedUpdate ずつ球をターゲットに飛ばし、計100球(FixedTimestep x 100 秒分)飛ばす
④:FixedTimestep を 1 にした後、1フレームずつ球をターゲットに飛ばし、計 100 球(100 フレーム分)飛ばす
そのうえで、
③ + ◎:Application.targetFrameRate = 1、yield WaitForFixedUpdate ずつ 100 個、当たったらコライダー消す
④ + ◎:FixedTimestep 1、1フレームずつ 100 個、当たったらコライダー消す
の 2 つを OnCollisionEnter で試してみました。
もし ③ + ◎ でコライダーが消えるのが遅ければ Game logic の方で変わることに、
もし ④ + ◎ でコライダーが消えるのが遅ければ Physics の方で変わることになるはずです。
結果は、
③ + ◎:
④ + ◎:
となりました。ということで恐らく Physics の方で変わっているのだと思われます。
(当然といえば当然かもしれないけど…)
おまけ 2:処理落ちすると…
シューティングなどで球を連続で飛ばしている際に処理落ちすると、
一度の physics cycle 中に複数個の球が判定されやすくなるようです。
OnCollisionEnter で判定を取っている場合スコアを複数回送ってしまったりで非常に困ります。
記事の始めに書いたバグの原因はこれでした。
マニュアルを参考にすると一度の physics cycle で処理しきれなかった場合、
次の physics cycle は処理を待機し、その次から再び処理を行うようです。
このようにして遅れた分だけ物理演算で沢山移動するので、
一度の physics cycle で多くの判定されやすくなるのかな?と思います。
逆に言えば処理落ちしても最大で FixedTimestep 分だけ移動や判定が遅れるだけなのかな?
あくまで考察ですが、とりあえず bool などで確実に一度だけの処理にした方が良さそうです。