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);
}
}