using System.Collections.Generic; using BaseGames.Core.Interaction; using BaseGames.Input; using UnityEngine; namespace BaseGames.World { /// /// 挂在 Player 上,检测附近可交互物,并驱动其世界空间提示()显隐。 /// 通过 InputReaderSO.InteractEvent 绑定交互输入。 /// /// 提示表现是「每个交互物自带」的世界空间子节点(跟随物体、可单独配样式), /// 本组件只负责「检测最近可交互物」并通知它显示/隐藏,不持有任何 UI。 /// /// public class InteractableDetector : MonoBehaviour { [SerializeField] private float _detectRadius = 1.5f; [SerializeField] private LayerMask _interactableLayer; [SerializeField] private InputReaderSO _inputReader; private IInteractable _nearest; private IInteractable _previousNearest; // 预分配检测缓冲区,避免 OverlapCircleAll 每帧 GC 分配 private readonly Collider2D[] _overlapBuffer = new Collider2D[16]; // Collider → IInteractable 缓存,避免 FindNearest 每帧重复 GetComponentInParent private readonly Dictionary _componentCache = new(); // IInteractable → 其世界空间提示视图缓存(可能为 null,表示该物体未配置提示) private readonly Dictionary _promptCache = new(); private void OnEnable() { _inputReader.InteractEvent += TryInteract; } private void OnDisable() { _inputReader.InteractEvent -= TryInteract; // 离场前隐藏当前提示,防止跨场景残留 ResolvePrompt(_previousNearest)?.Hide(); _componentCache.Clear(); // 清理缓存,防止跨场景持有旧引用 _promptCache.Clear(); _previousNearest = null; _nearest = null; } private void Update() { int count = Physics2D.OverlapCircleNonAlloc( transform.position, _detectRadius, _overlapBuffer, _interactableLayer); _nearest = FindNearest(_overlapBuffer, count); if (_nearest != _previousNearest) { if (_previousNearest != null) { _previousNearest.OnPlayerExitRange(); ResolvePrompt(_previousNearest)?.Hide(); } if (_nearest != null) { _nearest.OnPlayerEnterRange(transform); ResolvePrompt(_nearest)?.Show(_nearest.InteractPrompt); } _previousNearest = _nearest; } } private void TryInteract() { if (_nearest != null && _nearest.CanInteract) _nearest.Interact(transform); } private IInteractable FindNearest(Collider2D[] hits, int count) { IInteractable best = null; float bestSqrDist = float.MaxValue; for (int i = 0; i < count; i++) { var col = hits[i]; if (col == null) continue; // 查缓存,未命中时才调用 GetComponentInParent(避免每帧反射开销) if (!_componentCache.TryGetValue(col, out var interactable)) { interactable = col.GetComponentInParent(); _componentCache[col] = interactable; } if (interactable == null || !interactable.CanInteract) continue; // 用 sqrMagnitude 比较距离,省去 Distance 的 sqrt 开销 float sqrDist = ((Vector2)transform.position - (Vector2)col.transform.position).sqrMagnitude; if (sqrDist < bestSqrDist) { bestSqrDist = sqrDist; best = interactable; } } return best; } /// 解析交互物的世界空间提示视图(在其子节点上查找一次并缓存;无则记 null)。 private IInteractPromptView ResolvePrompt(IInteractable interactable) { if (interactable == null) return null; if (_promptCache.TryGetValue(interactable, out var view)) return view; view = (interactable as Component)?.GetComponentInChildren(true); _promptCache[interactable] = view; return view; } private void OnDrawGizmosSelected() { Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(transform.position, _detectRadius); } } }