Skip to content

Instantly share code, notes, and snippets.

@Rovsau
Created April 16, 2023 23:02
Show Gist options
  • Save Rovsau/3c51bc4fb48f0e74effe33fed9d4b408 to your computer and use it in GitHub Desktop.
Save Rovsau/3c51bc4fb48f0e74effe33fed9d4b408 to your computer and use it in GitHub Desktop.
Raycast with multiple detection ranges

Uses a PostFixedUpdate solution with Coroutine. (generates garbage)
Which solves the problem of physics events executing before FixedUpdate,
which would result in a 1-frame delay of detection,
as well as a 1-frame lingering when the target has gone out of range.

The coroutine is probably best replaced by injecting the method into the Player loop. (not provided)

The events are mostly there as an example. They probably need to be redesigned or replaced.

using System.Collections;
using UnityEngine;
using UnityEngine.Events;

public class Detection : MonoBehaviour
{
    [Header("Events")]
    public UnityEvent OnDetectedByVision;
    public UnityEvent OnDetectedByHearing;
    public UnityEvent OnDetectedByProximity;

    [Header("Detection Layers")]
    [SerializeField] private LayerMask _targetLayers;
    [SerializeField] private LayerMask _obstructionLayers;

    private LayerMask _layerMaskRaycast;

    [Header("Detection Ranges")]
    [SerializeField] private float _proximity = 3;
    [SerializeField] private float _hearing = 5;
    [SerializeField] private float _vision = 7;

    [Header("Gizmo Color")]
    [SerializeField] private Color _colorGizmo = new Color(1, 0, 0, 0.2f);

    [Header("Raycast Colors")]
    [SerializeField] private Color _colorNotInTargetLayer = Color.white;
    [SerializeField] private Color _colorProximity = Color.red;
    [SerializeField] private Color _colorHearing = Color.blue;
    [SerializeField] private Color _colorVision = Color.green;

    private float _maxRange;
    private bool _hasTarget;
    private Transform _target;
    private Vector3 _targetDirection;

    private Color _colorRaycast;

    private SphereCollider _collider;

    private Coroutine _postFixedUpdate;
    private WaitForFixedUpdate _waitForFixedUpdate;

    private void Awake()
    {
        _layerMaskRaycast = _targetLayers | _obstructionLayers;
        _maxRange = Mathf.Max(_proximity, _hearing, _vision);
        _collider = GetComponent<SphereCollider>();
        _collider.radius = _maxRange;
        _waitForFixedUpdate = new WaitForFixedUpdate();
    }
    private void OnEnable()
    {
        _postFixedUpdate = StartCoroutine(PostFixedUpdate());
    }

    private IEnumerator PostFixedUpdate()
    {
        while (true)
        {
            yield return _waitForFixedUpdate;
            RunDetection();
        }
    }

    private void RunDetection()
    {
        if (!_hasTarget) return;

        _targetDirection = _target.position - transform.position;

        if (Physics.Raycast(transform.position, _targetDirection, out RaycastHit hit, _maxRange, _layerMaskRaycast))
        {
            if (!IsLayerInMask(hit.transform.gameObject, _targetLayers))
            {
                Debug.DrawLine(transform.position, hit.point, _colorNotInTargetLayer);
                return;
            }

            if (hit.distance < _proximity)
            {
                _colorRaycast = _colorProximity;
                OnDetectedByProximity?.Invoke(); // work around to stop spammining while true?
            }
            else if (hit.distance < _hearing)
            {
                _colorRaycast = _colorHearing;
                OnDetectedByHearing?.Invoke();
            }
            else if (hit.distance < _vision)
            {
                _colorRaycast = _colorVision;
                OnDetectedByVision?.Invoke();
            }
            else
            {
                // This should only happen if using FixedUpdate, instead of Update or a custom PostFixedUpdate.
                Debug.Log("Target not in range");
                _colorRaycast = Color.black;
            }

            Debug.DrawLine(transform.position, hit.point, _colorRaycast);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        _target = other.transform;
        _hasTarget = true;
    }

    private void OnTriggerExit(Collider other)
    {
        _target = null;
        _hasTarget = false;
    }

    private void OnDisable()
    {
        StopCoroutine(_postFixedUpdate);
    }

    public static bool IsLayerInMask(GameObject gameObject, LayerMask layerMask)
    {
        return ((1 << gameObject.layer) & layerMask) != 0;
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = _colorGizmo;
        Gizmos.DrawSphere(transform.position, _proximity);
        Gizmos.DrawSphere(transform.position, _hearing);
        Gizmos.DrawSphere(transform.position, _vision);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment