242 lines
9.4 KiB
C#
242 lines
9.4 KiB
C#
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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<Transform, float> _lastSeenTime = new Dictionary<Transform, float>(16);
|
|
private readonly Dictionary<Transform, Vector3> _lastKnownPositions = new Dictionary<Transform, Vector3>(16);
|
|
|
|
// ─── Component Cache ─────────────────────────────────────
|
|
private Transform _transform;
|
|
private string _teamTag;
|
|
|
|
private void Awake()
|
|
{
|
|
_transform = transform;
|
|
}
|
|
|
|
public void Initialize(string teamTag)
|
|
{
|
|
_teamTag = teamTag;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update perception and populate blackboard.
|
|
/// Call this on the AI decision tick, NOT every frame.
|
|
/// </summary>
|
|
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<HealthNew>();
|
|
CashCarrier carrier = member.GetComponent<CashCarrier>();
|
|
|
|
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<CashBag>.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 ───────────────────────────────────
|
|
|
|
/// <summary>Clear all threat memory (call on state reset).</summary>
|
|
public void ClearMemory()
|
|
{
|
|
_lastSeenTime.Clear();
|
|
_lastKnownPositions.Clear();
|
|
}
|
|
|
|
/// <summary>Get last known position of a target (for pursuit).</summary>
|
|
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
|
|
}
|