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