2020/11/09
というわけで、今回はUnityの衝突判定に関するお話です。
例えば「攻撃を受けた方向によって被ダメージモーションを変える」という場合、衝突が発生した位置を知る必要があります。
OnCollisionEnter()
なら取得できますが、Rigidbody
のIsKinematic
やコライダーのIsTrriger
が使えないので物理演算が働いてしまいます。なのでトリガー系と変わらない感覚で位置を取得できる方法がないか模索してみました。
テスト用スクリプトの作成
今回「当てる側」「当てられる側」の両方にCube状のオブジェクトを用意します。分かりやすくするため、当てる側のBoxCollider
は大きめにしておきます。
また「当てる側」に「CollisionTest」というタグを設定し、「当てられる側」に以下のスクリプトを設定しました。
using System.Linq; using UnityEngine; public class CollisionTest : MonoBehaviour { public GameObject collisionPointTest; private const string COLLISION_TAG = "CollisionTest"; private void OnCollisionEnter(Collision c) { if (c.gameObject.tag == COLLISION_TAG) { ContactPoint contactPoint = c.contacts .Where(contact => contact.otherCollider.tag == COLLISION_TAG) .FirstOrDefault(); Instantiate(collisionPointTest, contactPoint.point, Quaternion.identity); } } private void OnTriggerEnter(Collider c) { if (c.gameObject.tag == COLLISION_TAG) { Vector3 position = c.ClosestPoint(transform.position); Instantiate(collisionPointTest, position, Quaternion.identity); } } }
collisionPointTest
は青色のSphereのプレハブです。これを「衝突が発生した位置に生成させたい」のが今回の目的です。
物理演算を抑えつつOnCollisionEnter()を使う
当てる側のRigidbody.isKinematic
を有効にした状態でコライダーのIsTrriger
を無効にし、当てられた側のOnCollisionEnter()
を使用する方法です。この方法ならCollision.contacts
を参照することで正確な位置を得られます。
ただしCollision
系を使うので当てる側・当てられる側両方にRigidbody
が必要になり、また片方はIsKinematic
を無効にする必要があります。物理演算させたい場合は問題ありませんが、今回のように「衝突位置だけ取得し物理演算は行わない」というケースには向かない気がします。
それでもCollision
系を使いたい場合、IsKinematic
が無効になっている側のTransform.position
に「常に初期位置を代入し続ける」ことで、一応それっぽい動きにはなりますが、(当然ながら) 常に初期位置に固定するとオブジェクトが動かなくなるので工夫が必要です。
先ほどのGIFアニメーションの場合、当てる側の判定用オブジェクトを杖のboneの子オブジェクトとして設定し、Transform.localPosition
にStart()
時のlocalPosition
を代入し続けました。親はboneに沿って自動的に動くので、子はオブジェクト移動を気にせずに位置を固定し続けることができます。
スクリプトで書くと以下のような感じ。
using UnityEngine; public class ColliderTestHit : MonoBehaviour { Vector3 defaultPosition; void Start() { defaultPosition = transform.localPosition; } void Update() { transform.localPosition = defaultPosition; } }
「判定時の処理も1つのスクリプトでやりたい!」な人は、子オブジェクトのOnCollisionEnter()
を検知したら親オブジェクトに判定情報を渡すような仕組みを作りましょう。
具体的なやり方は以下の記事をご参照ください。
https://gomafrontier.com/unity/1756#i-2
また、position
に代入し続けても物理演算によって一瞬吹き飛んでしまう可能性はあります。本記事のタイトルに「なるべく」と付いているのはそのためです。
Collider.ClosestPoint()を使う
OnTriggerEnter()
内でCollider.ClosestPoint()
を使って「コライダー内の最も近い点」を取得する方法です。
ClosestPoint()
というメソッドがあること自体知りませんでしたが、これを使うだけでもだいたいの位置は求められる感じです。あくまでも「コライダー内の」なので正確性はCollision
使用時には劣りますが、「攻撃を受けた方向を知る」くらいの用途であれば問題なさそうですね。
ただ当てる側のコライダーが大きい場合、当てられる側のオブジェクト内にめり込んでしまうことがあるので注意が必要です。また当てる側のコライダーの移動スピードが速い場合も位置のズレが大きくなります。
今回のテストでもちょっとCubeと近づいただけでめり込む確率が高くなりました。
これを許容できるのであればTrigger
のまま取得できるので、ゲームと場面に応じてCollision
方式と選択する形になりそうです。