Files
zeling_v2/Assets/_Game/Scripts/Dialogue/InteractionPromptController.cs
2026-05-25 11:54:37 +08:00

167 lines
7.0 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.InputSystem;
using UnityEngine.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("键盘/鼠标设备激活时使用的按键图标 Sprite。")]
[SerializeField] private Sprite _keyboardIcon;
[Tooltip("手柄设备激活时使用的按键图标 Sprite。")]
[SerializeField] private Sprite _gamepadIcon;
[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;
// ── 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;
}
}
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) return;
bool isGamepad = Gamepad.current != null && Gamepad.current.enabled;
_icon.sprite = isGamepad ? _gamepadIcon : _keyboardIcon;
}
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; }
}
}
}