using UnityEngine; using System.Collections.Generic; /// /// AIPerceptionSystem — Unified vision/detection system for AI. /// Uses Physics.OverlapSphereNonAlloc for zero-allocation detection. /// Populates the AIBlackboard with perception results. /// /// Call UpdatePerception() on the AI tick (not every frame). /// public class AIPerceptionSystem : MonoBehaviour { [Header("Vision Settings")] [SerializeField] private float detectionRadius = 18f; [SerializeField] private float fovAngle = 160f; // Forward cone [SerializeField] private float closeRangeRadius = 5f; // Always detect within this range (no FOV check) [Header("Bag Detection")] [SerializeField] private float bagDetectionRadius = 25f; [Header("Line of Sight")] [SerializeField] private bool requireLineOfSight = true; [SerializeField] private LayerMask obstructionLayers; [SerializeField] private float losHeightOffset = 1.2f; // Eye height [Header("Memory")] [SerializeField] private float memoryDuration = 5f; // Remember entities for this long after losing sight // ─── Detection Buffer (zero-alloc) ─────────────────────── private static readonly Collider[] _overlapBuffer = new Collider[32]; // ─── Threat Memory ─────────────────────────────────────── private readonly Dictionary _lastSeenTime = new Dictionary(16); private readonly Dictionary _lastKnownPositions = new Dictionary(16); // ─── Component Cache ───────────────────────────────────── private Transform _transform; private string _teamTag; private void Awake() { _transform = transform; } public void Initialize(string teamTag) { _teamTag = teamTag; } /// /// Update perception and populate blackboard. /// Call this on the AI decision tick, NOT every frame. /// public void UpdatePerception(AIBlackboard blackboard) { blackboard.ClearPerception(); Vector3 myPos = _transform.position; Vector3 eyePos = myPos + Vector3.up * losHeightOffset; // ─── Detect Characters ─────────────────────────────── var allMembers = TeamMember.AllMembers; for (int i = 0; i < allMembers.Count; i++) { TeamMember member = allMembers[i]; if (member == null || member.gameObject == gameObject) continue; Transform target = member.transform; float sqrDist = (target.position - myPos).sqrMagnitude; float dist = Mathf.Sqrt(sqrDist); // Range check if (dist > detectionRadius) continue; // FOV check (skip for close range) bool inFOV = dist <= closeRangeRadius || IsInFOV(target.position); // LOS check bool hasLOS = !requireLineOfSight || dist <= closeRangeRadius || CheckLineOfSight(eyePos, target.position + Vector3.up * losHeightOffset); bool isVisible = inFOV && hasLOS; // Update memory if (isVisible) { _lastSeenTime[target] = Time.time; _lastKnownPositions[target] = target.position; } // Check memory (even if not currently visible) bool isRemembered = false; float timeSinceLastSeen = float.MaxValue; Vector3 lastKnownPos = target.position; if (_lastSeenTime.TryGetValue(target, out float lastSeen)) { timeSinceLastSeen = Time.time - lastSeen; isRemembered = timeSinceLastSeen < memoryDuration; if (_lastKnownPositions.TryGetValue(target, out Vector3 lkp)) lastKnownPos = lkp; } if (!isVisible && !isRemembered) continue; // Build perceived entity HealthNew health = member.GetComponent(); CashCarrier carrier = member.GetComponent(); PerceivedEntity entity = new PerceivedEntity { transform = target, gameObject = member.gameObject, distance = dist, sqrDistance = sqrDist, healthPercent = health != null ? (health.currentHealth / health.maxHealth) : 1f, isCarryingBag = carrier != null && carrier.IsCarrying, carriedBagCount = carrier != null ? carrier.BagCount : 0, isVisible = isVisible, lastKnownPosition = lastKnownPos, timeSinceLastSeen = timeSinceLastSeen }; bool isEnemy = member.gameObject.tag != _teamTag; if (isEnemy) { blackboard.perceivedEnemies.Add(entity); blackboard.nearbyEnemyCount++; if (dist < blackboard.nearestEnemyDistance) blackboard.nearestEnemyDistance = dist; // Track nearest enemy carrier if (entity.isCarryingBag && carrier != null) { if (blackboard.nearestEnemyCarrier == null || dist < Vector3.Distance(myPos, blackboard.nearestEnemyCarrier.transform.position)) { blackboard.nearestEnemyCarrier = carrier; } } } else { blackboard.perceivedAllies.Add(entity); blackboard.nearbyAllyCount++; // Track nearest ally carrier (for escort) if (entity.isCarryingBag && carrier != null) { if (blackboard.nearestAllyCarrier == null || dist < Vector3.Distance(myPos, blackboard.nearestAllyCarrier.transform.position)) { blackboard.nearestAllyCarrier = carrier; } } } } // ─── Detect Cash Bags ───────────────────────────────── var allBags = EntityRegistry.All; for (int i = 0; i < allBags.Count; i++) { CashBag bag = allBags[i]; if (bag == null || bag.IsCarried) continue; float dist = Vector3.Distance(myPos, bag.transform.position); if (dist > bagDetectionRadius) continue; if (dist < blackboard.nearestBagDistance) blackboard.nearestBagDistance = dist; if (bag.IsAvailable) blackboard.nearbyAvailableBags.Add(bag); else if (bag.IsDropped) blackboard.nearbyDroppedBags.Add(bag); } } // ─── Vision Helpers ────────────────────────────────────── private bool IsInFOV(Vector3 targetPos) { Vector3 dirToTarget = (targetPos - _transform.position).normalized; dirToTarget.y = 0; // Horizontal only float angle = Vector3.Angle(_transform.forward, dirToTarget); return angle <= fovAngle * 0.5f; } private bool CheckLineOfSight(Vector3 from, Vector3 to) { Vector3 dir = to - from; float dist = dir.magnitude; if (dist < 0.1f) return true; return !Physics.Raycast(from, dir.normalized, dist, obstructionLayers); } // ─── Memory Management ─────────────────────────────────── /// Clear all threat memory (call on state reset). public void ClearMemory() { _lastSeenTime.Clear(); _lastKnownPositions.Clear(); } /// Get last known position of a target (for pursuit). public bool TryGetLastKnownPosition(Transform target, out Vector3 position) { return _lastKnownPositions.TryGetValue(target, out position); } // ─── Debug ─────────────────────────────────────────────── #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (_transform == null) _transform = transform; // Detection radius Gizmos.color = new Color(1f, 1f, 0f, 0.1f); Gizmos.DrawWireSphere(_transform.position, detectionRadius); // Close range Gizmos.color = new Color(1f, 0.5f, 0f, 0.15f); Gizmos.DrawWireSphere(_transform.position, closeRangeRadius); // FOV cone Gizmos.color = new Color(0f, 1f, 0f, 0.3f); Vector3 fovLeft = Quaternion.Euler(0, -fovAngle * 0.5f, 0) * _transform.forward * detectionRadius; Vector3 fovRight = Quaternion.Euler(0, fovAngle * 0.5f, 0) * _transform.forward * detectionRadius; Gizmos.DrawLine(_transform.position, _transform.position + fovLeft); Gizmos.DrawLine(_transform.position, _transform.position + fovRight); // Bag detection radius Gizmos.color = new Color(1f, 0.84f, 0f, 0.05f); Gizmos.DrawWireSphere(_transform.position, bagDetectionRadius); } #endif }