LoginSignup
1
3

【Unity】衝突・通過した時間を正確に計算する関数

Last updated at Posted at 2023-08-13

概要

以下の gif は、床にボールが衝突したら名前と時間をログに出力する検証動画です。

gif.gif

青いボールが先に衝突していますが、赤いボールのログが先に表示されています。また、どちらのボールも同時刻に衝突した事になっています。
これは、OnCollisionEnter, OnTriggerEnter が距離に関係なく処理順で呼ばれているためです。

大抵の場合は問題ありませんが、レースゲームのゴール等がこれでは困ります。
そこで、衝突・通過した時間を正確に計算できる関数を作成しました。

修正点や改良案などがありましたら、コメントをいただけると助かります。

コード

お手持ちの Vector3Util クラスに追加してご利用ください。

Vector3Util.cs
using System;
using UnityEngine;

namespace Utility
{
    public static class Vector3Util
    {
        public static Vector3 PerpendicularFootPoint(Vector3 a, Vector3 b, Vector3 p)
        {
            Vector3 ab = (b - a).normalized;
            float k = Vector3.Dot(p - a, ab);
            return a + k * ab;
        }
        public static float InverseLerp(Vector3 from, Vector3 to, Vector3 t)
            => InverseLerpUnclamped(from, to, PerpendicularFootPoint(from, to, t));
        public static float InverseLerpUnclamped(Vector3 from, Vector3 to, Vector3 t)
        {
            var fromDistance = (from - t).magnitude;
            if (fromDistance == 0f) return 0f;
            var toDistance = (to - t).magnitude;
            if (toDistance == 0f) return 1f;
            return fromDistance / (fromDistance + toDistance);
        }

        public static double CollisionTime(ContactPoint contact, Vector3 pos, Vector3 prevPos, Vector3 prevPrevPos, double? time = null, float? deltaTime = null)
        {
            deltaTime ??= Time.fixedDeltaTime;
            var virtualPos = prevPos - contact.impulse * deltaTime.Value;
            var contactOffset = contact.thisCollider.ClosestPoint(contact.point + pos - prevPrevPos) - pos;
            var beforePoint = prevPrevPos + contactOffset;
            var afterPoint = virtualPos + contactOffset;
            var deltaRatio = InverseLerp(afterPoint, beforePoint, contact.point);
            return (time ?? Time.fixedTimeAsDouble) - deltaTime.Value * deltaRatio;
        }
        public static double ClosestTime(Collider thisCollider, Collider otherCollider, Vector3 pos, Vector3 prevPrevPos, double? time = null, float? deltaTime = null)
        {
            var closestPoint = otherCollider.ClosestPoint(prevPrevPos);
            var contactOffset = thisCollider.ClosestPoint(closestPoint + pos - prevPrevPos) - pos;
            var beforePoint = prevPrevPos + contactOffset;
            var afterPoint = pos + contactOffset;
            var deltaRatio = InverseLerp(afterPoint, beforePoint, closestPoint);
            return (time ?? Time.fixedTimeAsDouble) - (deltaTime ?? Time.fixedDeltaTime) * deltaRatio;
        }
    }
}

使い方

OnCollisionEnter ではCollisionTime()を使用します。
引数にはContactPoint, 現在の座標、1フレーム前と2フレーム前の座標を入れてください。
ContactPoint がよく分からなければ、とりあえずcollision.GetContact(0)で大丈夫です。

OnTriggerEnter ではClosestTime()を使用します。
引数には自分と相手のコライダー、現在の座標、2フレーム前の座標を入れてください。

2フレーム前の座標が必要な理由は、接点が衝突・通過する前の座標を利用するためです。
OnCollisionEnter, OnTriggerEnter は衝突・通過した次のフレームで呼ばれるので、1フレーム前の座標だけでは足りません。

仕組み

通過直前から通過直後の長さに対して、通過地点から通過直後の長さの割合を計算します。
現在の時間から割合の分だけ時間を引けば、通過時の時間が求められます。
衝突時間を求める際は、通過直後の座標を予想して利用します。

image.png

image.png

サンプル

検証に使ったコードを改良して、正確な衝突時間を表示するサンプルを作りました。

gif.gif

0.000001m の差でも正確な時間が出せます。
OnCollisionEnter, OnTriggerEnter のどちらでも同じ値が出せます(要検証)。

image.png

コード

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    private static List<(string name, double time)> enterList = new();

    /// <summary>
    /// 値が変動し続けるので、キャッシュして使用
    /// </summary>
    private static double fixedTimeAsDouble;

    Rigidbody rb;
    Collider thisCollider;
    Vector3 prevPos, prevPrevPos;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        thisCollider = GetComponent<Collider>();
    }

    private void FixedUpdate()
    {
        if (enterList.Count > 0)
        {
            foreach (var item in enterList.OrderBy(e => e.time)) Debug.Log($"{item.name} : {item.time}");
            enterList.Clear();
        }

        prevPrevPos = prevPos;
        prevPos = rb.position;
        fixedTimeAsDouble = Time.fixedTimeAsDouble;
    }

    private void OnCollisionEnter(Collision collision)
    {
        enterList.Add((name, Vector3Util.CollisionTime(collision.GetContact(0), rb.position, prevPos, prevPrevPos, fixedTimeAsDouble)));
    }

    private void OnTriggerEnter(Collider other)
    {
        enterList.Add((name, Vector3Util.ClosestTime(thisCollider, other, rb.position, prevPrevPos, fixedTimeAsDouble)));
    }
}

参考文献

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3