using System; using UnityEngine; namespace OmiLAXR.Components.Gaze { public class GazeDetector : MonoBehaviour { [Header("Raycast Settings")] [Tooltip("Welche Layer sollen für den Gaze-Raycast berücksichtigt werden?")] public LayerMask layersToInclude = ~0; [Tooltip("Maximale Raycast-Distanz in Metern.")] public float rayDistance = 10.0f; [Tooltip("Wie viele Treffer sollen maximal in den Buffer geschrieben werden?")] [SerializeField] private int maxHits = 8; [Tooltip("Wie sollen Trigger-Collider behandelt werden?")] [SerializeField] private QueryTriggerInteraction triggerMode = QueryTriggerInteraction.Ignore; [Tooltip("Kleiner Vorwärts-Offset (Meter), um Self-Hits zu vermeiden (z.B. 0.01).")] [SerializeField] private float originForwardOffset = 0.0f; public GazeHit LastHit { get; private set; } public event GazeHitHandler OnEnter; public event GazeHitHandler OnLeave; public event GazeHitHandler OnUpdate; private Ray _cachedRay; private RaycastHit[] _hits; [field: SerializeField, ReadOnly] public Component OwnerComponent { get; private set; } private void Awake() { maxHits = Mathf.Max(1, maxHits); _hits = new RaycastHit[maxHits]; } // Bequeme Overloads public GazeHit PerformRaycast(bool updateState = false) => PerformRaycast(layersToInclude, rayDistance, updateState); public GazeHit PerformRaycast(float rayDis = 10.0f, bool updateState = false) => PerformRaycast(layersToInclude, rayDis, updateState); /// /// Führt einen garbage-freien Raycast in Blickrichtung aus und wählt deterministisch den nächstgelegenen Treffer. /// Optional aktualisiert die Methode internen Zustand und feuert Events. /// public GazeHit PerformRaycast(LayerMask layers, float rayDis = 10.0f, bool updateState = false) { // Blickstrahl setzen (ohne Allokation) var gazeDirection = transform.forward; var gazeOrigin = transform.position; if (originForwardOffset > 0f) gazeOrigin += gazeDirection * originForwardOffset; _cachedRay.origin = gazeOrigin; _cachedRay.direction = gazeDirection; // Einmaliger NonAlloc-Raycast in den wiederverwendbaren Buffer var hitCount = Physics.RaycastNonAlloc(_cachedRay, _hits, rayDis, layers, triggerMode); if (hitCount > 0) { // Aus dem bereits gefüllten Buffer den nächsten Treffer bestimmen var closestHit = GetClosestHitFromBuffer(hitCount); var gazeHit = new GazeHit(this, closestHit, gazeDirection, gazeOrigin); if (updateState) UpdateStateWith(gazeHit); return gazeHit; } if (LastHit != null && updateState) TriggerLeave(); return null; } /// /// Wählt den nächstgelegenen Treffer aus dem vorhandenen _hits-Buffer (Indexbereich [0, hitCount)). /// private RaycastHit GetClosestHitFromBuffer(int hitCount) { var closest = _hits[0]; for (var i = 1; i < hitCount; i++) { // NaN-Schutz ist i.d.R. nicht nötig, kann aber bei fehlerhaften Collidern helfen if (_hits[i].distance < closest.distance) closest = _hits[i]; } return closest; } private void UpdateStateWith(GazeHit gazeHit) { // WICHTIG: Collider statt GameObject vergleichen (Objekte können mehrere Collider haben) var newCol = gazeHit.RayHit.collider; var lastCol = LastHit?.RayHit.collider; var isNew = LastHit == null || newCol != lastCol; if (isNew) { if (LastHit != null) TriggerLeave(); LastHit = gazeHit; OnEnter?.Invoke(gazeHit); } LastHit = gazeHit; OnUpdate?.Invoke(gazeHit); } private void TriggerLeave() { if (LastHit == null) return; OnLeave?.Invoke(LastHit); LastHit = null; } public void AssignOwner(Component component) => OwnerComponent = component; public void AssignOwner() where T : Component { var comp = gameObject.GetComponent(); OwnerComponent = comp; } public T GetOwner() where T : Component => (T)OwnerComponent; } }