ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】物理演算を(なるべく)行わずにオブジェクトの衝突位置を取得する方法

time 2018/09/30

というわけで、今回はUnityの衝突判定に関するお話です。

例えば「攻撃を受けた方向によって被ダメージモーションを変える」という場合、衝突が発生した位置を知る必要があります。
OnCollisionEnter()なら取得できますが、RigidbodyIsKinematicやコライダーの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.localPositionStart()時の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方式と選択する形になりそうです。

down

コメントする