using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace BaseGames.Dialogue { /// /// 世界空间交互提示控制器(架构 14_NarrativeModule §2 升级版)。 /// 挂在每个 InteractableNPC 子节点(Prefab 实例),默认隐藏。 /// /// 功能: /// • 自动订阅父级 InteractableNPC 的进/出范围事件,免手动调用 Show/Hide /// • TMP_Text 实时显示 InteractPrompt(如"接受任务"/"提交任务"),随任务状态动态刷新 /// • 根据当前活跃输入设备自动切换按键图标(键盘/手柄) /// • 支持淡入/淡出动画 /// 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(); 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(兼容旧调用 / 脚本手动控制)──────────────────────────── /// 手动显示提示。通常由 InteractableNPC 自动调用,无需手动触发。 public void Show() { if (_npc != null) _label.text = _npc.InteractPrompt; SetVisible(true, immediate: false); UpdateIcon(); } /// 手动隐藏提示。通常由 InteractableNPC 自动调用,无需手动触发。 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; } } } }