【Unity】衝突と接触に応じてイベントを発火させるコンポーネント

`Collider Event Invoker` は衝突と接触の両方のイベントハンドラを登録するための自作コンポーネントです。個人的によく使うのですが、その場でなんとなく新しいものを書いてしまうことがあり、気付いたら手元に３つあったのでここで比較、整理します。（[現在の最終形](#書き直したもの)）

# 書き直す前

<details><summary>タイプ１</summary>

```ColliderEventInvoker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System.Linq;

public class ColliderEventInvoker : MonoBehaviour
{
    // public GameObject collisionTarget;

    public bool useCollisionEvent;
    public GameObject[] specificCollisionTargets;
    public bool useTriggerEvent;
    public GameObject[] specificTriggerTargets;
    public UnityEvent onCollisionEventEnter = new UnityEvent();
    public UnityEvent onTriggerEventEnter = new UnityEvent();
    public float collisionDetectionThreshold = 1;
    Vector3 collisionDetectionThresholdVector;

    void Awake()
    {
        collisionDetectionThresholdVector = new Vector3(
            collisionDetectionThreshold,
            collisionDetectionThreshold,
            collisionDetectionThreshold
        );
    }

    void OnCollisionEnter(Collision other)
    {
        if (useCollisionEvent && other.relativeVelocity.sqrMagnitude > collisionDetectionThresholdVector.sqrMagnitude)
        {
            if (specificCollisionTargets.Length == 0)
            {
                onCollisionEventEnter.Invoke();
            }
            else
            {
                if (specificCollisionTargets.Contains(other.gameObject))
                {
                    onCollisionEventEnter.Invoke();
                }
            }
        }
    }

    void OnTriggerEnter(Collider other)
    {
        if (useTriggerEvent)
        {
            if (specificTriggerTargets.Length == 0)
            {
                onTriggerEventEnter.Invoke();
            }
            else
            {
                if (specificTriggerTargets.Contains(other.gameObject))
                {
                    onTriggerEventEnter.Invoke();
                }
            }
        }
    }
}

```
</details>


<details><summary>タイプ２</summary>

```ColliderEventInvoker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System.Linq;

namespace liveasnotes.Misc.Old
{
    public class ColliderEventInvoker : MonoBehaviour
    {
        public enum ColliderDetection
        {
            Everything,
            ByTag,
            ByObject,
            Nothing,
        }

        [Header("Collision Event")]
        public ColliderDetection collisionDetectionFor;
        public string collisionTargetTag = nameof(collisionTargetTag);
        public GameObject[] collisionTargetObjects;
        public UnityEvent onCollisionEventEnter = new UnityEvent();
        public UnityEvent onCollisionEventExit = new UnityEvent();
        public float collisionEnterThreshold = 1;
        Vector3 collisionEnterThresholdVector;

        [Header("Trigger Event")]
        public ColliderDetection triggerDetectionFor;
        public string triggerTargetTag = nameof(triggerTargetTag);
        public GameObject[] triggerTargetObjects;
        public UnityEvent onTriggerEventEnter = new UnityEvent();
        public UnityEvent onTriggerEventExit = new UnityEvent();

        void DoWithValidate(
            ColliderDetection _method,
            bool _isUnlocked,
            bool _isTagMatch,
            GameObject[] _specifiedTargets,
            GameObject _detectedObject,
            UnityEvent _process
        )
        {
            if (_isUnlocked)
            {
                switch (_method)
                {
                    case ColliderDetection.Everything:
                        _process.Invoke();
                        break;

                    case ColliderDetection.ByTag:
                        if (_isTagMatch)
                        {
                            _process.Invoke();
                        }
                        break;

                    case ColliderDetection.ByObject:
                        if (_specifiedTargets.Contains(_detectedObject))
                        {
                            _process.Invoke();
                        }
                        break;
                }
            }
        }

        void Awake()
        {
            collisionEnterThresholdVector = new Vector3(
                collisionEnterThreshold,
                collisionEnterThreshold,
                collisionEnterThreshold
            );
        }

        void OnCollisionEnter(Collision _other)
        {
            DoWithValidate(
                collisionDetectionFor,
                (_other.relativeVelocity.sqrMagnitude > collisionEnterThresholdVector.sqrMagnitude),
                _other.gameObject.CompareTag(collisionTargetTag),
                collisionTargetObjects,
                _other.gameObject,
                onCollisionEventEnter
            );
        }

        public void OnCollisionEnterInvoke()
        {
            onCollisionEventEnter.Invoke();
        }

        void OnCollisionExit(Collision _other)
        {
            DoWithValidate(
                collisionDetectionFor,
                true,
                _other.gameObject.CompareTag(collisionTargetTag),
                collisionTargetObjects,
                _other.gameObject,
                onCollisionEventExit
            );
        }

        public void OnCollisionExitInvoke()
        {
            onCollisionEventExit.Invoke();
        }

        void OnTriggerEnter(Collider _other)
        {
            DoWithValidate(
                triggerDetectionFor,
                true,
                _other.CompareTag(triggerTargetTag),
                triggerTargetObjects,
                _other.gameObject,
                onTriggerEventEnter
            );
        }

        public void OnTriggerEnterInvoke()
        {
            onTriggerEventEnter.Invoke();
        }

        void OnTriggerExit(Collider _other)
        {
            DoWithValidate(
                triggerDetectionFor,
                true,
                _other.CompareTag(triggerTargetTag),
                triggerTargetObjects,
                _other.gameObject,
                onTriggerEventExit
            );
        }

        public void OnTriggerExitInvoke()
        {
            onTriggerEventExit.Invoke();
        }
    }
}

```
</details>


<details><summary>タイプ３</summary>

```ColliderEventInvoker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System.Linq;
using NaughtyAttributes;

namespace liveasnotes.Misc
{
    public class ColliderEventInvoker : MonoBehaviour
    {
        public enum ColliderDetection
        {
            Nothing,
            Everything,
            ByTag,
            ByObject,
        }

        [Header("Collision Event")]
        public bool enableCollisionEvent;
        public ColliderDetection collisionDetectionFor = ColliderDetection.Nothing;

        bool _isCollisionTargetTagUsed => collisionDetectionFor == ColliderDetection.ByTag;
        [EnableIf(nameof(_isCollisionTargetTagUsed))] public string collisionTargetTag = "Untagged";
        bool _collisionTargetObjectsUsed => collisionDetectionFor == ColliderDetection.ByObject;
        [EnableIf(nameof(_collisionTargetObjectsUsed))] public GameObject[] collisionTargetObjects;

        public UnityEvent onCollisionEventEnter = new UnityEvent();
        public UnityEvent onCollisionEventExit = new UnityEvent();
        public float collisionEnterThreshold = 1;
        Vector3 collisionEnterThresholdVector;

        [Header("Trigger Event")]
        public bool enableTriggerEvent;
        public ColliderDetection triggerDetectionFor = ColliderDetection.Nothing;

        bool _triggerTargetTagUsed => triggerDetectionFor == ColliderDetection.ByTag;
        [EnableIf(nameof(_triggerTargetTagUsed))] public string triggerTargetTag = "Untagged";
        bool _triggerTargetObjectsUsed => triggerDetectionFor == ColliderDetection.ByObject;
        [EnableIf(nameof(_triggerTargetObjectsUsed))] public GameObject[] triggerTargetObjects;

        public UnityEvent onTriggerEventEnter = new UnityEvent();
        public UnityEvent onTriggerEventExit = new UnityEvent();

        void DoWithValidate(
            ColliderDetection _method,
            bool _isUnlocked,
            bool _isTagMatch,
            GameObject[] _specifiedTargets,
            GameObject _detectedObject,
            UnityEvent _process
        )
        {
            if (_isUnlocked)
            {
                switch (_method)
                {
                    case ColliderDetection.Everything:
                        _process.Invoke();
                        break;

                    case ColliderDetection.ByTag:
                        if (_isTagMatch)
                        {
                            _process.Invoke();
                        }
                        break;

                    case ColliderDetection.ByObject:
                        if (_specifiedTargets.Contains(_detectedObject))
                        {
                            _process.Invoke();
                        }
                        break;
                }
            }
        }

        void Awake()
        {
            collisionEnterThresholdVector = new Vector3(
                collisionEnterThreshold,
                collisionEnterThreshold,
                collisionEnterThreshold
            );
        }

        void OnCollisionEnter(Collision _other)
        {
            if (enableCollisionEvent)
            {
                DoWithValidate(
                    collisionDetectionFor,
                    (_other.relativeVelocity.sqrMagnitude > collisionEnterThresholdVector.sqrMagnitude),
                    _other.gameObject.CompareTag(collisionTargetTag),
                    collisionTargetObjects,
                    _other.gameObject,
                    onCollisionEventEnter
                );
            }
        }

        public void OnCollisionEnterInvoke()
        {
            onCollisionEventEnter.Invoke();
        }

        void OnCollisionExit(Collision _other)
        {
            if (enableCollisionEvent)
            {
                DoWithValidate(
                    collisionDetectionFor,
                    true,
                    _other.gameObject.CompareTag(collisionTargetTag),
                    collisionTargetObjects,
                    _other.gameObject,
                    onCollisionEventExit
                );
            }
        }

        public void OnCollisionExitInvoke()
        {
            onCollisionEventExit.Invoke();
        }

        void OnTriggerEnter(Collider _other)
        {
            if (enableTriggerEvent)
            {
                DoWithValidate(
                    triggerDetectionFor,
                    true,
                    _other.CompareTag(triggerTargetTag),
                    triggerTargetObjects,
                    _other.gameObject,
                    onTriggerEventEnter
                );
            }
        }

        public void OnTriggerEnterInvoke()
        {
            onTriggerEventEnter.Invoke();
        }

        void OnTriggerExit(Collider _other)
        {
            if (enableTriggerEvent)
            {
                DoWithValidate(
                    triggerDetectionFor,
                    true,
                    _other.CompareTag(triggerTargetTag),
                    triggerTargetObjects,
                    _other.gameObject,
                    onTriggerEventExit
                );
            }
        }

        public void OnTriggerExitInvoke()
        {
            onTriggerEventExit.Invoke();
        }
    }
}

```
</details>

名前空間の付け方からして、タイプ１→２→３の順番で作られたっぽいので、この順番で掲載。

`collisionDetectionThreshold` プロパティは初期からあるっぽく、どうも衝突速度に応じて接触・衝突の判定をキャンセルできるようにしているらしい。なぜこんな実装なのかは忘れた。ゆっくりめり込んでくるオブジェクトに反応させないためなんだろうけど。でも、OnCollision~ で渡される collider の `.relativeVelocity.sqrMagnitude` プロパティに依存してるから、 trigger のときは使えない。

また、タイプ１では、検知したい対象が特に設定されていない or 設定されていて且つ当たった対象が検知対象、という場合にイベントを発火させている。動的。

タイプ２からは、イベント発火条件がシリアライズされた Enum の値で分岐する静的なものになっている。動的である必要があまりないということかも。

`DoWithValidate` 関数で、検証とその結果に応じた実行をひとまとめにするようになったのも特徴。
引数に条件式を放り込ませる設計になっていて、条件判定の初期段階で真偽の評価が走るので、大量のオブジェクトにアタッチすると負荷が上がるかも。
引数に bool 値 `_isUnlocked` が設定できるけど、OnCollisionEnter のとき以外は true が入ってるのでほとんど意味ないかも。

あと、検知モードの設定と判定タグの設定が 接触用と検知用で分かれてるので、ひとつのコンポーネントで両方をそれぞれ違う感じで取得できるようにしようとしてるっぽい。何かバグのきっかけになりそう。

タイプ３だと、検知モードの選択によってインスペクタ上での変数欄の表示が切り替わるようにしようとしていたり、デバッグ用に一時的に当たり判定を切るための bool も用意されている。ちょっと冗長かな。

あと、判定用タグのデフォルト値がタイプ２でプロパティ名だったのが、タイプ３だと `Untagged` になってる。

# 書き直したもの

今欲しい機能とコードの見通しのしやすさを考えて、以下のスクリプトを改めて書いた。

private 関数の引数を減らしたり、併せてプロパティも減らしている。
OnTrigger 系と OnCollision 系のイベントの両方を別の発火条件で設ける必要があるなら、別オブジェクトに分けることにする。（実際、そういうときはこのコンポーネントを使うかとは関係なく、別オブジェクトに分けてしまうことも多いので、これで良いと思う。）

```L4VColliderEventInvoker.cs
using UnityEngine;
using UnityEngine.Events;
using System.Linq;

namespace liveasnotes.L4Variant
{
    public class L4VColliderEventInvoker : MonoBehaviour
    {
        public enum Detectable
        {
            Everything,
            Tag,
            Object,
            Nothing,
        }

        public Detectable filter;
        public string targetTag = "Untagged";
        public GameObject[] targetObjects;

        [Header("Collision Event"), Space(10)]
        public UnityEvent onCollisionEnter = new UnityEvent();
        public UnityEvent onCollisionExit = new UnityEvent();

        [Header("Trigger Event")]
        public UnityEvent onTriggerEnter = new UnityEvent();
        public UnityEvent onTriggerExit = new UnityEvent();

        void InvokeIfValid(UnityEvent _handler, GameObject _)
        {
            if (_handler != null)
            {
                switch (filter)
                {
                    case Detectable.Everything:
                        _handler.Invoke();
                        break;

                    case Detectable.Tag:
                        if (_.CompareTag(targetTag))
                        {
                            _handler.Invoke();
                        }
                        break;

                    case Detectable.Object:
                        if (targetObjects.Contains(_))
                        {
                            _handler.Invoke();
                        }
                        break;

                    case Detectable.Nothing:
                        break;
                }
            }
        }

        void OnCollisionEnter(Collision _other)
        {
            InvokeIfValid(onCollisionEnter, _other.gameObject);
        }

        void OnCollisionExit(Collision _other)
        {
            InvokeIfValid(onCollisionExit, _other.gameObject);
        }

        void OnTriggerEnter(Collider _other)
        {
            InvokeIfValid(onTriggerEnter, _other.gameObject);
        }

        void OnTriggerExit(Collider _other)
        {
            InvokeIfValid(onTriggerExit, _other.gameObject);
        }
    }
}

```