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++)
{
CashMag 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
}