using UnityEngine;
///
/// A dependency-free starting point for enemy vision in Unity.
/// Add it to an enemy, assign a target, and tune range/FOV/occlusion in the Inspector.
/// Free to use in personal and commercial projects. Attribution is appreciated, not required.
///
public sealed class SimpleVisionSensor : MonoBehaviour
{
[Header("Target")]
[SerializeField] private Transform target;
[Min(0f)] [SerializeField] private float targetSampleHeight = 1f;
[Header("Vision")]
[Min(0.1f)] [SerializeField] private float range = 15f;
[Range(1f, 360f)] [SerializeField] private float horizontalFieldOfView = 90f;
[Min(0f)] [SerializeField] private float eyeHeight = 1.65f;
[SerializeField] private LayerMask occluders = ~0;
[Header("Debug")]
[SerializeField] private Color visibleColor = new Color(0.2f, 1f, 0.65f, 1f);
[SerializeField] private Color blockedColor = new Color(1f, 0.3f, 0.25f, 1f);
public bool CanSeeTarget { get; private set; }
public Vector3 LastSeenPosition { get; private set; }
private void Update()
{
CanSeeTarget = Evaluate(target);
}
/// Tests any candidate using range, horizontal angle, and a final occlusion ray.
public bool Evaluate(Transform candidate)
{
if (candidate == null) return false;
Vector3 origin = transform.position + Vector3.up * eyeHeight;
Vector3 sample = candidate.position + Vector3.up * targetSampleHeight;
Vector3 toTarget = sample - origin;
if (toTarget.sqrMagnitude > range * range) return false;
Vector3 planarDirection = Vector3.ProjectOnPlane(toTarget, Vector3.up);
if (planarDirection.sqrMagnitude < 0.0001f) return false;
if (Vector3.Angle(transform.forward, planarDirection) > horizontalFieldOfView * 0.5f)
return false;
float distance = toTarget.magnitude;
if (Physics.Raycast(origin, toTarget / distance, out RaycastHit hit, distance,
occluders, QueryTriggerInteraction.Ignore)
&& hit.transform.root != candidate.root)
return false;
LastSeenPosition = sample;
return true;
}
private void OnDrawGizmosSelected()
{
Vector3 origin = transform.position + Vector3.up * eyeHeight;
Gizmos.color = CanSeeTarget ? visibleColor : blockedColor;
Gizmos.DrawWireSphere(origin, range);
const int segments = 24;
Vector3 previous = origin + DirectionAt(-horizontalFieldOfView * 0.5f) * range;
Gizmos.DrawLine(origin, previous);
for (int i = 1; i <= segments; i++)
{
float t = i / (float)segments;
float angle = Mathf.Lerp(-horizontalFieldOfView * 0.5f, horizontalFieldOfView * 0.5f, t);
Vector3 next = origin + DirectionAt(angle) * range;
Gizmos.DrawLine(previous, next);
previous = next;
}
Gizmos.DrawLine(origin, origin + DirectionAt(horizontalFieldOfView * 0.5f) * range);
if (target != null)
{
Vector3 sample = target.position + Vector3.up * targetSampleHeight;
Gizmos.DrawLine(origin, sample);
Gizmos.DrawWireSphere(LastSeenPosition, 0.18f);
}
}
private Vector3 DirectionAt(float angle)
{
return Quaternion.AngleAxis(angle, Vector3.up) * transform.forward;
}
}