using UnityEngine; /// /// Finds nearby enemies (tagged "Enemy") that satisfy the assist constraints. /// Designed for zero-allocation queries during combat assists. /// [DisallowMultipleComponent] public sealed class EnemyTargetDetector : MonoBehaviour { private const float MinAssistRadius = 0.5f; [Header("Assist Window")] [Tooltip("Maximum distance at which the assist can consider enemies.")] [SerializeField] private float assistRadius = 4.0f; [Tooltip("Maximum field of view in degrees around the forward vector where assist is allowed.")] [SerializeField, Range(30f, 180f)] private float assistFOVDegrees = 130f; [Tooltip("Optional hard cap on the delta angle to avoid 180° turns.")] [SerializeField, Range(45f, 180f)] private float maxAssistAngle = 150f; [Tooltip("Layer mask used for enemy overlap queries.")] [SerializeField] private LayerMask enemyLayers = ~0; [Tooltip("Maximum number of enemy candidates that will be considered per query.")] [SerializeField, Min(1)] private int maxCandidates = 12; [Header("Line Of Sight")] [Tooltip("If enabled, a raycast must reach the target without hitting the obstruction layers.")] [SerializeField] private bool requireLineOfSight = false; [SerializeField] private LayerMask obstructionLayers = ~0; [SerializeField, Tooltip("Height offset for line of sight ray origin.")] private float losHeightOffset = 1.6f; [SerializeField, Tooltip("Height offset for line of sight ray destination.")] private float targetHeightOffset = 1.2f; private Collider[] candidateBuffer; private Transform hardLockOverride; private void Awake() { maxCandidates = Mathf.Max(1, maxCandidates); candidateBuffer = new Collider[maxCandidates]; if (assistRadius < MinAssistRadius) { assistRadius = MinAssistRadius; } } /// /// Optionally sets a manually chosen target that should be prioritised over soft candidates. /// public void SetHardLockTarget(Transform target) { hardLockOverride = target; } /// /// Attempts to find the best enemy target within the configured assist window. /// public bool TryFindBestTarget(out Transform target, out Vector3 targetPosition) { target = null; targetPosition = Vector3.zero; if (!enabled) return false; if (hardLockOverride != null && IsTargetValid(hardLockOverride, out var hardLockPos)) { target = hardLockOverride; targetPosition = hardLockPos; return true; } var origin = transform.position; var forward = transform.forward; forward.y = 0f; forward.Normalize(); int hits = Physics.OverlapSphereNonAlloc(origin, assistRadius, candidateBuffer, enemyLayers, QueryTriggerInteraction.Collide); if (hits <= 0) return false; float bestScore = float.MaxValue; Transform bestTarget = null; Vector3 bestPosition = Vector3.zero; for (int i = 0; i < hits; i++) { Collider candidate = candidateBuffer[i]; if (candidate == null) continue; Transform candidateTransform = candidate.transform; if (!candidateTransform.CompareTag("Enemy")) continue; if (!IsTargetValid(candidateTransform, out var candidatePosition)) continue; Vector3 toTarget = candidatePosition - origin; toTarget.y = 0f; float sqrDist = toTarget.sqrMagnitude; if (sqrDist < 0.0001f) continue; Vector3 direction = toTarget / Mathf.Sqrt(sqrDist); float angle = Vector3.Angle(forward, direction); if (angle > assistFOVDegrees) continue; if (angle > maxAssistAngle) continue; if (requireLineOfSight && !HasLineOfSight(origin, candidatePosition)) continue; // Score by angle first, then distance. float score = angle * 10f + sqrDist; if (score < bestScore) { bestScore = score; bestTarget = candidateTransform; bestPosition = candidatePosition; } } if (bestTarget == null) return false; target = bestTarget; targetPosition = bestPosition; return true; } private bool IsTargetValid(Transform candidate, out Vector3 targetPosition) { targetPosition = candidate.position; if (candidate == null) return false; // Verify active in hierarchy (common alive check surrogate). if (!candidate.gameObject.activeInHierarchy) return false; return true; } private bool HasLineOfSight(Vector3 origin, Vector3 targetPosition) { Vector3 rayOrigin = origin + Vector3.up * losHeightOffset; Vector3 rayTarget = targetPosition + Vector3.up * targetHeightOffset; Vector3 delta = rayTarget - rayOrigin; float distance = delta.magnitude; if (distance <= 0.001f) return true; Vector3 direction = delta / distance; if (Physics.Raycast(rayOrigin, direction, distance, obstructionLayers, QueryTriggerInteraction.Ignore)) { return false; } return true; } #region Inspector API public float AssistRadius { get => assistRadius; set => assistRadius = Mathf.Max(MinAssistRadius, value); } public float AssistFOVDegrees { get => assistFOVDegrees; set => assistFOVDegrees = Mathf.Clamp(value, 30f, 180f); } public float MaxAssistAngle { get => maxAssistAngle; set => maxAssistAngle = Mathf.Clamp(value, 45f, 180f); } public LayerMask EnemyLayers { get => enemyLayers; set => enemyLayers = value; } public bool RequireLineOfSight { get => requireLineOfSight; set => requireLineOfSight = value; } public LayerMask ObstructionLayers { get => obstructionLayers; set => obstructionLayers = value; } #endregion }