- 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.
183 lines
7.7 KiB
C#
183 lines
7.7 KiB
C#
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using BaseGames.Core;
|
||
using BaseGames.UI;
|
||
|
||
namespace BaseGames.Dialogue
|
||
{
|
||
/// <summary>
|
||
/// 世界空间交互提示控制器(架构 14_NarrativeModule §2 升级版)。
|
||
/// 挂在每个 InteractableNPC 子节点(Prefab 实例),默认隐藏。
|
||
///
|
||
/// 功能:
|
||
/// • 自动订阅父级 InteractableNPC 的进/出范围事件,免手动调用 Show/Hide
|
||
/// • TMP_Text 实时显示 InteractPrompt(如"接受任务"/"提交任务"),随任务状态动态刷新
|
||
/// • 根据当前活跃输入设备自动切换按键图标(键盘/手柄)
|
||
/// • 支持淡入/淡出动画
|
||
/// </summary>
|
||
public class InteractionPromptController : MonoBehaviour
|
||
{
|
||
[Header("UI 引用")]
|
||
[Tooltip("整个提示根节点(包含图标和文字),控制显示/隐藏。")]
|
||
[SerializeField] private GameObject _promptRoot;
|
||
[Tooltip("按键图标 Image 组件(可选)。有输入设备时显示对应图标。")]
|
||
[SerializeField] private Image _icon;
|
||
[Tooltip("提示文字 TMP_Text 组件(可选)。自动显示 InteractableNPC.InteractPrompt 的当前值。")]
|
||
[SerializeField] private TMP_Text _label;
|
||
|
||
[Header("按键图标")]
|
||
[Tooltip("用于查询按键图标的输入动作名(与 InputReader 的 Action 对应)。\n" +
|
||
"图标走 IInputIconService,随设备切换 / 改键自动刷新。")]
|
||
[SerializeField] private string _actionName = "Interact";
|
||
|
||
[Header("位置与动画")]
|
||
[Tooltip("相对于本组件 transform 的世界空间偏移。调整此值可控制气泡与 NPC 的相对位置。")]
|
||
[SerializeField] private Vector3 _offset = new Vector3(0f, 1.8f, 0f);
|
||
[Tooltip("是否随相机方向 Billboard 朝向(世界空间 Canvas 推荐开启)。")]
|
||
[SerializeField] private bool _billboard = true;
|
||
[Tooltip("淡入持续时间(秒)。0 = 立即显示,无动画。")]
|
||
[SerializeField] [Min(0f)] private float _fadeInDuration = 0.12f;
|
||
[Tooltip("淡出持续时间(秒)。0 = 立即隐藏,无动画。")]
|
||
[SerializeField] [Min(0f)] private float _fadeOutDuration = 0.08f;
|
||
|
||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||
private InteractableNPC _npc;
|
||
private bool _visible;
|
||
private float _alpha;
|
||
private Camera _cam;
|
||
private IInputIconService _iconService;
|
||
|
||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||
|
||
private void Awake()
|
||
{
|
||
// 自动连接父级 InteractableNPC 事件(无需手动调用 Show/Hide)
|
||
_npc = GetComponentInParent<InteractableNPC>();
|
||
if (_npc != null)
|
||
{
|
||
_npc.PlayerEnteredRange += OnPlayerEntered;
|
||
_npc.PlayerExitedRange += OnPlayerExited;
|
||
}
|
||
|
||
SetVisible(false, immediate: true);
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_npc != null)
|
||
{
|
||
_npc.PlayerEnteredRange -= OnPlayerEntered;
|
||
_npc.PlayerExitedRange -= OnPlayerExited;
|
||
}
|
||
if (_iconService != null)
|
||
{
|
||
_iconService.OnIconSetChanged -= UpdateIcon;
|
||
_iconService = null;
|
||
}
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// 完全隐藏且不需要淡出时,跳过所有计算
|
||
if (!_visible && _alpha <= 0f) return;
|
||
|
||
// 位置偏移(世界空间气泡)
|
||
if (_offset != Vector3.zero)
|
||
transform.position = (_npc != null ? _npc.transform.position : transform.parent.position) + _offset;
|
||
|
||
// Billboard
|
||
if (_billboard && _visible)
|
||
{
|
||
if (_cam == null) _cam = Camera.main;
|
||
if (_cam != null)
|
||
transform.forward = _cam.transform.forward;
|
||
}
|
||
|
||
// 淡入/淡出
|
||
if (_promptRoot == null) return;
|
||
if (_visible && _alpha < 1f)
|
||
{
|
||
float speed = _fadeInDuration > 0f ? Time.deltaTime / _fadeInDuration : 1f;
|
||
_alpha = Mathf.MoveTowards(_alpha, 1f, speed);
|
||
ApplyAlpha(_alpha);
|
||
}
|
||
else if (!_visible && _alpha > 0f)
|
||
{
|
||
float speed = _fadeOutDuration > 0f ? Time.deltaTime / _fadeOutDuration : 1f;
|
||
_alpha = Mathf.MoveTowards(_alpha, 0f, speed);
|
||
ApplyAlpha(_alpha);
|
||
if (_alpha <= 0f) _promptRoot.SetActive(false);
|
||
}
|
||
}
|
||
|
||
// ── 公开 API(兼容旧调用 / 脚本手动控制)────────────────────────────
|
||
|
||
/// <summary>手动显示提示。通常由 InteractableNPC 自动调用,无需手动触发。</summary>
|
||
public void Show()
|
||
{
|
||
if (_npc != null) _label.text = _npc.InteractPrompt;
|
||
SetVisible(true, immediate: false);
|
||
UpdateIcon();
|
||
}
|
||
|
||
/// <summary>手动隐藏提示。通常由 InteractableNPC 自动调用,无需手动触发。</summary>
|
||
public void Hide() => SetVisible(false, immediate: false);
|
||
|
||
// ── 私有辅助 ─────────────────────────────────────────────────────────
|
||
|
||
private void OnPlayerEntered(Transform player)
|
||
{
|
||
// 刷新文字(每次进入都读取最新 InteractPrompt,确保任务状态变化后文字正确)
|
||
if (_label != null && _npc != null)
|
||
_label.text = _npc.InteractPrompt;
|
||
SetVisible(true, immediate: false);
|
||
UpdateIcon();
|
||
}
|
||
|
||
private void OnPlayerExited() => SetVisible(false, immediate: false);
|
||
|
||
private void SetVisible(bool show, bool immediate)
|
||
{
|
||
_visible = show;
|
||
if (immediate)
|
||
{
|
||
_alpha = show ? 1f : 0f;
|
||
if (_promptRoot != null)
|
||
{
|
||
_promptRoot.SetActive(show);
|
||
ApplyAlpha(_alpha);
|
||
}
|
||
}
|
||
else if (show && _promptRoot != null)
|
||
{
|
||
_promptRoot.SetActive(true); // 淡出由 Update 结束时 SetActive(false)
|
||
}
|
||
}
|
||
|
||
private void UpdateIcon()
|
||
{
|
||
if (_icon == null || string.IsNullOrEmpty(_actionName)) return;
|
||
|
||
// 延迟绑定:首次需要时获取服务(确保 ServiceLocator 已初始化),并订阅设备/改键刷新
|
||
if (_iconService == null)
|
||
{
|
||
_iconService = ServiceLocator.GetOrDefault<IInputIconService>();
|
||
if (_iconService != null)
|
||
_iconService.OnIconSetChanged += UpdateIcon;
|
||
}
|
||
|
||
var sprite = _iconService?.GetActionIcon(_actionName);
|
||
if (sprite != null) { _icon.sprite = sprite; _icon.enabled = true; }
|
||
else { _icon.enabled = false; }
|
||
}
|
||
|
||
private void ApplyAlpha(float a)
|
||
{
|
||
if (_icon != null) { var c = _icon.color; c.a = a; _icon.color = c; }
|
||
if (_label != null) { var c = _label.color; c.a = a; _label.color = c; }
|
||
}
|
||
}
|
||
}
|
||
|