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);
}
}
}