Unity

距離を見るヒット処理を試してみる

はじめに

純粋に計算式からヒット判定を求めてみたら
RaycastとかColliderとの負荷の違いはどうなんだろう。
ということでやってみました。

Unity2017.2.0f3で動作確認しています

参考

点と線の距離を求める
※ソースもそのまま流用できそうです。

説明はサイトを確認したほうが良いと思いますが、一応そのまま解説

直線とそれぞれのObjectの距離を求める場合。
直線の始点>終点と、直線の始点>Objectの二つの線と捉えると、平行四辺形の形になります。
ここで、平行四辺形の高さが、今回求めたい距離そのものとみなせます。

平行四辺形の高さは面積がわかれば以下の式で求まります。

高さh=面積/底辺の長さ

ということで直線距離A~Bと、その近くにあるPがあるとして
ベクトルABとベクトルAPの外積で面積Dが出てきます。
直線A~Bの長さが底辺の長さL
ということで直線とPとの距離Hは

H=D/L

あとはそのままScriptに落とし込んでみました。

実装

下記Scriptを準備します。
機能は、二点、マウスの左クリックは計算で求め、右クリックはRaycastAllで計算
それぞれカウンターでHitが1000回あつまるまでの時間を計測します。
※Scriptそのものは一番下参照

※Enemy(名前は何でも良いけど)Objectを作りEnemyControllerSphereColliderをAddしておきます。
 EnemyはPrefabとし、PlayerScriptEnemyObjにアタッチしておいてください。

player.png

enemy.png

結果

1000個のObjectを計算するのにどれくらいの時間がかかるのか求めました。
editor.png

動かします。
直線状にEnemyObjectが並んでいます。 さあ計算してみましょう!!
kekka.png
計算にかかった時間が表示されました!!
んー同じ?・・・
時間の計測方法に問題がありそうですけど、一旦置いておきます。

Profile.png
Profileを見てみると変化が出ていました!

RaycastAllはPhysicsが動いています。
Scriptだけだと計算式の方が処理負荷が重そうですが、Physicsも含めると計算式の方が軽そうです。

単純にこれだけでどうこうという事はありませんが、
まるっとヒット判定する場合は効果があるかもしれません。

Scriptサンプル

距離の算出計算
DistancePtoL.cs

public class DistancePtoL  {
    public float Distance(Vector3 Point,Vector3 Start,Vector3 End)
    {
        Vector3 StartToPoint, StartToEnd;
        StartToPoint = Point - Start;// スタート地点からPointまでのベクトル
        StartToEnd = End - Start;//スタート地点からEndまでのベクトル

        //StartToEndとStartToPointを外積して求められたベクトルの長さが、平行四辺形の面積になる
        float Area = Vector3.Magnitude( Vector3.Cross(StartToEnd, StartToPoint));

        //StartToEndの距離
        float Line = Vector3.Distance(Start, End);

        return Area / Line;//距離の算出
    }
}

本体はこんな感じ
PlayerScript.cs

[System.Serializable]
public class EnemyCore
{
    public GameObject EnemyBody;
    public EnemyController Controller;
}

//GameController
public class PlayerScript : MonoBehaviour {

    [SerializeField]
    private GameObject EnemyObj;//敵の基本Prefab

    [SerializeField]
    private List<EnemyCore> EnemyList = new List<EnemyCore>();//管理する敵リスト

    [SerializeField]
    private int EnemyCount = 1000;//作る仮想敵の数

    [SerializeField]
    private Vector3 _EnemyRange = new Vector3(5,5,1000);


    void Start()
    {
        //敵量産
        for (var i=0;i< EnemyCount; i++)
        {
            EnemyCore _Enemy = new EnemyCore();
            var EnemyPosition = new Vector3(0f,0f,UnityEngine.Random.Range(1f, _EnemyRange.z));
            _Enemy.EnemyBody = Object.Instantiate(EnemyObj, EnemyPosition, Quaternion.identity) as GameObject;
            _Enemy.EnemyBody.name = "Enemy[" + i + "]";
            _Enemy.Controller = _Enemy.EnemyBody.GetComponent<EnemyController>();
            _Enemy.Controller.PL = this.transform.GetComponent<PlayerScript>();
            EnemyList.Add(_Enemy);
        }
    }

    void Update () {
        if (Input.GetMouseButtonUp(0))
        {
            Debug.Log("PointToLine Time");
            //マウスクリック
            _HitCount = 0;
            _Timer = 0;
            StartCoroutine(TimerCount());
            HitTestUpdate(2);
        }

        if (Input.GetMouseButtonUp(1))
        {
            Debug.Log("RayCast Time");
            //マウスクリック
            _HitCount = 0;
            _Timer = 0;
            StartCoroutine(TimerCount());
            HitTestUpdate(1);
        }



    }

    private int _HitCount = 0;
    private float _Timer = 0;
    public void HitCount()
    {
        _HitCount++;
    }

    IEnumerator TimerCount()
    {
        while (true)
        {
            _Timer += Time.deltaTime;

            if (_HitCount >= 999)
            {
                Debug.Log("All Hit Timer :" + _Timer);
                yield break;
            }

            yield return null;
        }
    }


    void HitTestUpdate(int mode)
    {
        Vector3 Start = this.transform.position;
        Vector3 End = new Vector3(this.transform.position.x, this.transform.position.y, this.transform.position.z + 1000);
        RaycastHit[] hits;

        switch (mode) {
            case 1:
                hits = Physics.RaycastAll(Start, Vector3.forward, 1000);
                for(var i=0;i < hits.Length; i++)
                {
                    RaycastHit hit = hits[i];
                    HitCount();
                }


                break;
            case 2:
                for (var i = 0; i < EnemyList.Count; i++)
                {
                    EnemyList[i].Controller.HitUpdate(Start, End);
                }
                break;
        }
    }

}

最後に画面に並べられるスクリプト側
EnemyController.cs

public class EnemyController : MonoBehaviour {

    [SerializeField]
    public DistancePtoL HitCheck;
    public PlayerScript PL;

    void Start()
    {
        HitCheck = new DistancePtoL();
    }

    public void HitUpdate(Vector3 Start,Vector3 End)
    {
        var Dist = HitCheck.Distance(this.transform.position,Start,End);
        if (Dist < 10f)
        {
            PL.HitCount();
        }
    }
}

追記

コメントで教えていただいたのでProfileを正しく計測してみました。
・参考
Unityでスクリプトの一部分の処理をProfilerに表示してもらう

修正箇所はこちら PlayerScript.csの中になります。
ちなみに'Unity2017.2.0f3'では、Profiler.BeginSampleUnityEngine.Profilingに移動したようです。

    switch (mode) {
        case 1:
            UnityEngine.Profiling.Profiler.BeginSample("RaycastAllProcess");
            hits = Physics.RaycastAll(Start, Vector3.forward, 1000);
            for(var i=0;i < hits.Length; i++)
            {
                //RaycastHit hit = hits[i];
                HitCount();
            }
            UnityEngine.Profiling.Profiler.EndSample();

            break;
        case 2:
            UnityEngine.Profiling.Profiler.BeginSample("PointToLine Process");
            for (var i = 0; i < EnemyList.Count; i++)
            {
                EnemyList[i].Controller.HitUpdate(Start, End);
            }
            UnityEngine.Profiling.Profiler.EndSample();
            break;
    }

結果はこんな感じになりました。
profilekekka.png
Timeの数値が結果になります。
結構違いますね。