Files
zeling_v2/Assets/_Game/Scripts/Dialogue/InteractionPromptController.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

183 lines
7.7 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 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; }
}
}
}