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.
This commit is contained in:
@@ -30,9 +30,7 @@ namespace BaseGames.UI.HUD
|
||||
[Header("Form")]
|
||||
[SerializeField] private Image[] _formIcons;
|
||||
|
||||
[Header("Interact Prompt")]
|
||||
[Tooltip("独立 Widget 组件负责渲染图标+文本,HUDController 仅保留引用供编辑器配置检查")]
|
||||
[SerializeField] private InteractPromptWidget _interactPromptWidget;
|
||||
// 交互提示已改为「每个交互物自带的世界空间提示」(WorldInteractPrompt),不再由 HUD 承载。
|
||||
|
||||
[Header("Event Channels - Subscribe")]
|
||||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// 交互提示 Widget。
|
||||
///
|
||||
/// 职责:
|
||||
/// • 订阅 InteractPromptEventChannelSO 显示/隐藏提示
|
||||
/// • 显示按键图标(Image)+ 动作文本(TMP_Text)
|
||||
/// • 监听 IInputIconService.OnIconSetChanged,在设备切换或改键后自动刷新图标
|
||||
///
|
||||
/// 布置方式:放在 HUD Canvas 下,引用对应的事件频道 SO 资产。
|
||||
/// 不依赖 HUDController,可独立使用。
|
||||
/// </summary>
|
||||
public sealed class InteractPromptWidget : MonoBehaviour
|
||||
{
|
||||
[Header("UI 引用")]
|
||||
[SerializeField] private Image _keyIcon;
|
||||
[SerializeField] private TMP_Text _labelText;
|
||||
[Tooltip("整个提示根节点,控制显示/隐藏")]
|
||||
[SerializeField] private GameObject _root;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private InteractPromptEventChannelSO _onShowPrompt;
|
||||
[SerializeField] private VoidEventChannelSO _onHidePrompt;
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
private IInputIconService _iconService;
|
||||
private string _currentActionName;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// ServiceLocator 可能在此组件 OnEnable 时尚未注册(执行顺序问题),
|
||||
// 延迟到 ShowPrompt 首次调用时再获取,确保服务可用
|
||||
_onShowPrompt?.Subscribe(ShowPrompt).AddTo(_subs);
|
||||
_onHidePrompt?.Subscribe(HidePrompt).AddTo(_subs);
|
||||
|
||||
HidePrompt();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
UnsubscribeFromIconService();
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
private void ShowPrompt(InteractPromptEvent evt)
|
||||
{
|
||||
_currentActionName = evt.ActionName;
|
||||
|
||||
// 延迟绑定:首次显示时获取服务(确保 ServiceLocator 已初始化)
|
||||
if (_iconService == null)
|
||||
{
|
||||
_iconService = ServiceLocator.GetOrDefault<IInputIconService>();
|
||||
if (_iconService != null)
|
||||
_iconService.OnIconSetChanged += RefreshIcon;
|
||||
}
|
||||
|
||||
if (_labelText != null)
|
||||
_labelText.text = evt.LabelText;
|
||||
|
||||
RefreshIcon();
|
||||
|
||||
if (_root != null)
|
||||
_root.SetActive(true);
|
||||
else
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void HidePrompt()
|
||||
{
|
||||
_currentActionName = null;
|
||||
|
||||
if (_root != null)
|
||||
_root.SetActive(false);
|
||||
else
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Icon Refresh ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>设备切换或改键后刷新图标。由 IInputIconService.OnIconSetChanged 调用。</summary>
|
||||
private void RefreshIcon()
|
||||
{
|
||||
if (_keyIcon == null || string.IsNullOrEmpty(_currentActionName)) return;
|
||||
|
||||
var sprite = _iconService?.GetActionIcon(_currentActionName);
|
||||
if (sprite != null)
|
||||
{
|
||||
_keyIcon.sprite = sprite;
|
||||
_keyIcon.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 找不到图标时隐藏图标格,避免显示错误占位图
|
||||
_keyIcon.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromIconService()
|
||||
{
|
||||
if (_iconService != null)
|
||||
{
|
||||
_iconService.OnIconSetChanged -= RefreshIcon;
|
||||
_iconService = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Assets/_Game/Scripts/UI/World.meta
Normal file
8
Assets/_Game/Scripts/UI/World.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e5e071d3ce62d74284bcd89d5bbe6d4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
141
Assets/_Game/Scripts/UI/World/WorldInteractPrompt.cs
Normal file
141
Assets/_Game/Scripts/UI/World/WorldInteractPrompt.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Interaction;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 世界空间交互提示(按键图标 + 文字),挂在可交互物的子节点上,跟随物体显示。
|
||||
///
|
||||
/// 职责:
|
||||
/// • 实现 <see cref="IInteractPromptView"/>,由玩家身上的交互探测器在进入/离开最近可交互物时驱动显隐。
|
||||
/// • 显示按键图标(Image)+ 动作文本(TMP_Text);图标走 <see cref="IInputIconService"/>,
|
||||
/// 随设备切换 / 改键自动刷新(与全局一致)。
|
||||
/// • 通过 <see cref="CanvasGroup"/> 做淡入 / 淡出。
|
||||
///
|
||||
/// 布置方式:作为可交互物(门 / 存档点 / 商店 / 传送点…)的子节点,
|
||||
/// 用世界空间 Canvas 承载。气泡相对物体的位置 = 本节点的 Transform,
|
||||
/// 直接在场景里拖动本子节点即可单独微调每个物体的提示锚点。
|
||||
/// </summary>
|
||||
public sealed class WorldInteractPrompt : MonoBehaviour, IInteractPromptView
|
||||
{
|
||||
[Header("UI 引用")]
|
||||
[Tooltip("淡入淡出用 CanvasGroup(一般挂在本节点的 Canvas 上)。")]
|
||||
[SerializeField] private CanvasGroup _canvasGroup;
|
||||
[Tooltip("按键图标 Image。由 IInputIconService 按当前设备填充。")]
|
||||
[SerializeField] private Image _keyIcon;
|
||||
[Tooltip("提示文字 TMP_Text。显示 IInteractable.InteractPrompt 的当前值。")]
|
||||
[SerializeField] private TMP_Text _labelText;
|
||||
|
||||
[Header("按键动作")]
|
||||
[Tooltip("用于查询按键图标的输入动作名(与 InputReader 的 Action 对应)。")]
|
||||
[SerializeField] private string _actionName = "Interact";
|
||||
|
||||
[Header("淡入淡出")]
|
||||
[Tooltip("淡入持续时间(秒)。0 = 立即显示。")]
|
||||
[SerializeField] [Min(0f)] private float _fadeInDuration = 0.12f;
|
||||
[Tooltip("淡出持续时间(秒)。0 = 立即隐藏。")]
|
||||
[SerializeField] [Min(0f)] private float _fadeOutDuration = 0.08f;
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
private IInputIconService _iconService;
|
||||
private bool _visible;
|
||||
private float _alpha;
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_alpha = 0f;
|
||||
_visible = false;
|
||||
ApplyAlpha(0f);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnsubscribeFromIconService();
|
||||
_visible = false;
|
||||
_alpha = 0f;
|
||||
ApplyAlpha(0f);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 完全隐藏且无需继续淡出时,跳过
|
||||
if (!_visible && _alpha <= 0f) return;
|
||||
|
||||
float target = _visible ? 1f : 0f;
|
||||
if (!Mathf.Approximately(_alpha, target))
|
||||
{
|
||||
float dur = _visible ? _fadeInDuration : _fadeOutDuration;
|
||||
float speed = dur > 0f ? Time.unscaledDeltaTime / dur : 1f;
|
||||
_alpha = Mathf.MoveTowards(_alpha, target, speed);
|
||||
ApplyAlpha(_alpha);
|
||||
}
|
||||
}
|
||||
|
||||
// ── IInteractPromptView ───────────────────────────────────────────────
|
||||
|
||||
public void Show(string label)
|
||||
{
|
||||
if (_labelText != null)
|
||||
_labelText.text = label;
|
||||
|
||||
// 延迟绑定:首次显示时获取服务(确保 ServiceLocator 已初始化)
|
||||
if (_iconService == null)
|
||||
{
|
||||
_iconService = ServiceLocator.GetOrDefault<IInputIconService>();
|
||||
if (_iconService != null)
|
||||
_iconService.OnIconSetChanged += RefreshIcon;
|
||||
}
|
||||
|
||||
RefreshIcon();
|
||||
_visible = true;
|
||||
}
|
||||
|
||||
public void Hide() => _visible = false;
|
||||
|
||||
// ── Icon ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>设备切换或改键后刷新图标。由 IInputIconService.OnIconSetChanged 调用。</summary>
|
||||
private void RefreshIcon()
|
||||
{
|
||||
if (_keyIcon == null || string.IsNullOrEmpty(_actionName)) return;
|
||||
|
||||
var sprite = _iconService?.GetActionIcon(_actionName);
|
||||
if (sprite != null)
|
||||
{
|
||||
_keyIcon.sprite = sprite;
|
||||
_keyIcon.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keyIcon.enabled = false; // 找不到图标时隐藏图标格,避免错误占位图
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromIconService()
|
||||
{
|
||||
if (_iconService != null)
|
||||
{
|
||||
_iconService.OnIconSetChanged -= RefreshIcon;
|
||||
_iconService = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 辅助 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void ApplyAlpha(float a)
|
||||
{
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = a;
|
||||
// 提示纯展示,不接收交互;同时彻底关闭射线避免遮挡
|
||||
_canvasGroup.interactable = false;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85bdb69d66e546f49b6c89941beda368
|
||||
guid: 845b4e4cb1d22f54e8b0910d937650a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
Reference in New Issue
Block a user