chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
241
Assets/Scripts/AI/Core/AIPerceptionSystem.cs
Normal file
241
Assets/Scripts/AI/Core/AIPerceptionSystem.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user