【Unity】SphereColliderを自前の当たり判定に置き換えた場合の処理負荷

# 環境
Unity : Unity2020.2.0f1
CPU : AMD Ryzen 9 3300X 12-Core Processor (3.79GHz 12コア)

#はじめに
ColliderとRigidbodyが付いているオブジェクトの衝突判定は、OnCollisionやOnTrigger系のメソッドで取ることができます。

```cs
void OnCollisionEnter(Collision col)
{
}

void OnTriggerStay(Collider col)
{
}
```


ここで疑問が生まれました。
UnityのColliderに頼った場合と当たり判定を自前実装した場合、どちらの方が軽いのだろう？

今回はSphereColliderの処理時間と、自前の当たり判定の**処理時間を計測してみました**。

# Unityの物理エンジン = PhysX

Unityの物理演算は **PhysX** という物理演算エンジンを採用しています。 
公式ドキュメントを読むと、PhysXはオブジェクトの衝突時に発生する力や、接触点の座標などの計算を行っているようです。
参考 : https://docs.nvidia.com/gameworks/content/gameworkslibrary/physx/guide/Manual/AdvancedCollisionDetection.html


# 検証 : SphereCollider 1万個 vs コリジョン自前実装
SphereColliderを1万置いた場合の処理時間と、自前の当たり判定の処理時間を計測してみます。

# Sphere Collider 1万個
**SphereCollider**が付いた球オブジェクトを1万個置き、処理負荷を計測してみます。
SphereColliderの IsTriggerはONにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/7bd3719c-bfac-3eef-491e-3a3a8a544666.png)

球にはOnTriggerStayを実装したMonoBehaviourコンポーネントをアタッチします。

```MyObject.cs
using UnityEngine;

public class MyObject : MonoBehaviour
{
    public void OnTriggerStay(Collider other)
    {
    }
}
```

<img width=200 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/00e1e5ed-247c-6a4b-5a2f-b92b02f2d1ad.png">

## 結果
Updateにて毎フレーム500ms程度の処理負荷が発生しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/4d4e8a60-890d-c767-5bbe-8f7a4490ff62.png)

# 球の当たり判定 自前実装
次は球の当たり判定をC#で実装してみます。
球を覆うバウンディングボックス(AABB)で当たり判定を行った後、距離による判定を行います。

```CollisionScene.cs
using UnityEngine;

public class CollisionScene : MonoBehaviour
{
    [SerializeField] private int TotalNumber = 1000; // 球の合計数
    [SerializeField] private MyObject prefab; // Prefab
    [SerializeField] private MyObject[] instanceArray; 
    [SerializeField] private Vector3[] objectBoundingBoxMin; // 球ごとのバウンディングボックスの最小座標
    [SerializeField] private Vector3[] objectBoundingBoxMax; // 球ごとのバウンディングボックスの最大座標
    [SerializeField] private Vector3[] objectPosition; // 位置
    private int instanceNum; // 配置したオブジェクトの数

    private void Update()
    {
        for (int i = 0; i < instanceNum; i++)
        {
            // 位置のキャッシュ
            objectPosition[i] = instanceArray[i].transform.position;
            
            // Sphereが属するバウンディングボックスを計算
            objectBoundingBoxMin[i] = objectPosition[i] - instanceArray[i].HalfBoundingBoxSize;
            objectBoundingBoxMax[i] = objectPosition[i] + instanceArray[i].HalfBoundingBoxSize;
        }
        
        // 球同士の当たり判定
        for (int i = 0; i < instanceNum; i++)
        {
            for (int j = i + 1; j < instanceNum; j++)
            {
                // まずはバウンディングボックスによる判定を行う
                if (objectBoundingBoxMax[i].x < objectBoundingBoxMin[j].x) continue;
                if (objectBoundingBoxMin[i].x > objectBoundingBoxMax[j].x) continue;
                if (objectBoundingBoxMax[i].y < objectBoundingBoxMin[j].y) continue;
                if (objectBoundingBoxMin[i].y > objectBoundingBoxMax[j].y) continue;
                if (objectBoundingBoxMax[i].z < objectBoundingBoxMin[j].z) continue;
                if (objectBoundingBoxMin[i].z > objectBoundingBoxMax[j].z) continue;
    
                // 距離の計算
                // 補足 : Vector3.sqrMagnitureは内部的にはdoubleキャストされていますが、今回は精度は重要ではないのでfloatのまま距離を計算します
                var distance = LengthSqrt(objectPosition[i] - objectPosition[j]);
                float radius = instanceArray[i].Radius + instanceArray[j].Radius;
                // 球の当たり判定
                if (distance < radius * radius)
                {
                    instanceArray[i].OnHit();
                    instanceArray[j].OnHit();
                }
            }
        }
    }
    
    float LengthSqrt(Vector3 a)
    {
        return a.x * a.x + a.y * a.y + a.z * a.z;
    }

    void Start()
    {
        int n = Mathf.FloorToInt(Mathf.Pow(TotalNumber, 1f / 3f));
        instanceNum = n * n * n;
        instanceArray = new MyObject[instanceNum];
        objectBoundingBoxMin = new Vector3[instanceNum];
        objectBoundingBoxMax = new Vector3[instanceNum];
        objectPosition = new Vector3[instanceNum];

        int i = 0;
        for (int x = 0; x < n; x++)
        {
            for (int y = 0; y < n; y++)
            {
                for (int z = 0; z < n; z++)
                {
                    var instance = Instantiate(prefab);
                    instance.transform.position = new Vector3(x, y, z) - new Vector3(n, n, n);
                    instanceArray[i++] = instance;
                }
            }
        }
        instanceArray[instanceNum - 1].Rigidbody.velocity = new Vector3(-1f, -1f, 0f);
    }
}

```

## 結果
処理時間を毎フレーム80ms程度にまで削減できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/963fdfef-3de9-d565-f6e3-2832183a8f5e.png)


## おまけ : バウンディングボックス(AABB)による当たり判定を省くと重くなる
何も考えずにすべての球同士で距離判定を行うと、かなり重くなります。


**処理時間 : 約8000ms**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/3a220e45-b697-ca98-a4af-4bbedadc76cc.png)

```cs
    private void Update()
    {
        // 球同士の当たり判定
        for (int i = 0; i < instanceNum; i++)
        {
            for (int j = i + 1; j < instanceNum; j++)
            {

                // 距離の計算
                var sqrDistance = LengthSqr(instanceArray[i].transform.position - instanceArray[j].transform.position); 
                
                // 球の当たり判定
                float radius = instanceArray[i].Radius + instanceArray[j].Radius;
                if (sqrDistance < radius * radius)
                {
                    instanceArray[i].OnHit();
                    instanceArray[j].OnHit();
                }
            }
        }
    }
```

# まとめ

## 処理時間の比較
SphereColliderを1万個おいた場合 : 処理負荷 約**530 ms**
球の当たり判定を自前実装した場合 : 処理負荷 約**80 ms**

## 所感
・OnCollision系、OnTrigger系メソッドは衝突時に力の計算、当たった位置などを計算しているので重い
・衝突判定を自分で実装すると必要な情報だけを計算できるので、軽くできる
(ただし、ちゃんと考えて実装しないと逆に重くなる)


# 関連
衝突判定
https://ja.wikipedia.org/wiki/%E8%A1%9D%E7%AA%81%E5%88%A4%E5%AE%9A

当たり判定の高速化 – AABB木の使用
http://ftvoid.com/blog/post/364

The PhysX API
https://docs.nvidia.com/gameworks/content/gameworkslibrary/physx/guide/3.3.4/Manual/API.html

GameWorks Library > PhysX > PhysX 3.3.4
https://docs.nvidia.com/gameworks/#gameworkslibrary/physx/physx_v3_3.htm%3FTocPath%3DGameWorks%2520Library%7CPhysX%7C_____1

Rigid Body Collision
https://docs.nvidia.com/gameworks/content/gameworkslibrary/physx/guide/3.3.4/Manual/RigidBodyCollision.html

Rigid Body Overview
https://docs.nvidia.com/gameworks/content/gameworkslibrary/physx/guide/3.3.4/Manual/RigidBodyOverview.html