Files
zeling_v2/Assets/_Game/Scripts/World/InteractableDetector.cs
Joywayer a1f54b68e6 Refactor interaction prompt system to use world space prompts
- Removed the InteractPromptWidget from HUD and its references in HUDController.
- Introduced IInteractPromptView interface for world space interaction prompts.
- Implemented WorldInteractPrompt class to manage display of interaction prompts in world space.
- Updated InteractableDetector to handle showing/hiding of world space prompts based on player proximity to interactable objects.
- Created a new prefab for UI_WorldInteractPrompt to facilitate the new interaction prompt system.
2026-06-10 14:14:08 +08:00

129 lines
4.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using BaseGames.Core.Interaction;
using BaseGames.Input;
using UnityEngine;
namespace BaseGames.World
{
/// <summary>
/// 挂在 Player 上,检测附近可交互物,并驱动其世界空间提示(<see cref="IInteractPromptView"/>)显隐。
/// 通过 InputReaderSO.InteractEvent 绑定交互输入。
/// <para>
/// 提示表现是「每个交互物自带」的世界空间子节点(跟随物体、可单独配样式),
/// 本组件只负责「检测最近可交互物」并通知它显示/隐藏,不持有任何 UI。
/// </para>
/// </summary>
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<Collider2D, IInteractable> _componentCache = new();
// IInteractable → 其世界空间提示视图缓存(可能为 null表示该物体未配置提示
private readonly Dictionary<IInteractable, IInteractPromptView> _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<IInteractable>();
_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;
}
/// <summary>解析交互物的世界空间提示视图(在其子节点上查找一次并缓存;无则记 null。</summary>
private IInteractPromptView ResolvePrompt(IInteractable interactable)
{
if (interactable == null) return null;
if (_promptCache.TryGetValue(interactable, out var view)) return view;
view = (interactable as Component)?.GetComponentInChildren<IInteractPromptView>(true);
_promptCache[interactable] = view;
return view;
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, _detectRadius);
}
}
}