Add InputDeviceIconSetSO configuration guide and related documentation
- Created a new markdown file detailing the configuration of InputDeviceIconSetSO. - Included sections on system architecture, field explanations, image specifications, and complete workflow from setup to runtime. - Documented the automatic device recognition logic and provided troubleshooting for common issues. - Added references to relevant files and scripts for easier navigation.
This commit is contained in:
@@ -25,8 +25,8 @@ namespace BaseGames.UI.HUD
|
||||
[SerializeField] private Image[] _formIcons;
|
||||
|
||||
[Header("Interact Prompt")]
|
||||
[SerializeField] private TMP_Text _interactText;
|
||||
[SerializeField] private GameObject _interactPromptRoot;
|
||||
[Tooltip("独立 Widget 组件负责渲染图标+文本,HUDController 仅保留引用供编辑器配置检查")]
|
||||
[SerializeField] private InteractPromptWidget _interactPromptWidget;
|
||||
|
||||
[Header("Event Channels - Subscribe")]
|
||||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||||
@@ -36,8 +36,6 @@ namespace BaseGames.UI.HUD
|
||||
[SerializeField] private IntEventChannelSO _onLingZhuChanged;
|
||||
[SerializeField] private IntEventChannelSO _onSpringChargesChanged;
|
||||
[SerializeField] private IntEventChannelSO _onFormChanged;
|
||||
[SerializeField] private StringEventChannelSO _onShowInteractPrompt;
|
||||
[SerializeField] private VoidEventChannelSO _onHideInteractPrompt;
|
||||
|
||||
private readonly List<GameObject> _hpCells = new();
|
||||
private readonly List<GameObject> _springIcons = new();
|
||||
@@ -53,8 +51,7 @@ namespace BaseGames.UI.HUD
|
||||
_onLingZhuChanged?.Subscribe(UpdateLingZhu).AddTo(_subs);
|
||||
_onSpringChargesChanged?.Subscribe(RebuildSpringIcons).AddTo(_subs);
|
||||
_onFormChanged?.Subscribe(UpdateFormIcon).AddTo(_subs);
|
||||
_onShowInteractPrompt?.Subscribe(ShowInteractPrompt).AddTo(_subs);
|
||||
_onHideInteractPrompt?.Subscribe(HideInteractPrompt).AddTo(_subs);
|
||||
// 交互提示由独立的 InteractPromptWidget 组件处理,HUDController 不再直接订阅
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
@@ -118,16 +115,5 @@ namespace BaseGames.UI.HUD
|
||||
for (int i = 0; i < _formIcons.Length; i++)
|
||||
if (_formIcons[i] != null) _formIcons[i].enabled = (i == formIndex);
|
||||
}
|
||||
|
||||
private void ShowInteractPrompt(string text)
|
||||
{
|
||||
if (_interactText != null) _interactText.text = text;
|
||||
if (_interactPromptRoot != null) _interactPromptRoot.SetActive(true);
|
||||
}
|
||||
|
||||
private void HideInteractPrompt()
|
||||
{
|
||||
if (_interactPromptRoot != null) _interactPromptRoot.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
119
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs
Normal file
119
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/HUD/InteractPromptWidget.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85bdb69d66e546f49b6c89941beda368
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
34
Assets/_Game/Scripts/UI/IInputIconService.cs
Normal file
34
Assets/_Game/Scripts/UI/IInputIconService.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 按键图标服务接口。
|
||||
/// 根据当前输入设备和玩家实际绑定(含改键),返回对应的按键 Sprite。
|
||||
/// 通过 ServiceLocator 注册/查找,与 UI 层完全解耦。
|
||||
/// </summary>
|
||||
public interface IInputIconService
|
||||
{
|
||||
/// <summary>当前活跃输入设备类型。</summary>
|
||||
InputDeviceType CurrentDevice { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定 Action(如 "Interact")在当前设备上的按键图标。
|
||||
/// 若找不到图标(资源未配置)返回 null。
|
||||
/// </summary>
|
||||
Sprite GetActionIcon(string actionName);
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定 Action 在当前设备上的有效绑定路径(含改键后的路径)。
|
||||
/// 例如:"<Keyboard>/e"、"<Gamepad>/buttonSouth"。
|
||||
/// </summary>
|
||||
string GetActionEffectivePath(string actionName);
|
||||
|
||||
/// <summary>
|
||||
/// 当设备切换或玩家改键后触发。
|
||||
/// 订阅此事件的 UI 组件应在回调中刷新图标显示。
|
||||
/// </summary>
|
||||
event Action OnIconSetChanged;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/IInputIconService.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/IInputIconService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5c091c06f569c24788467c1d4796e71
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
109
Assets/_Game/Scripts/UI/InputDeviceDetector.cs
Normal file
109
Assets/_Game/Scripts/UI/InputDeviceDetector.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 设备检测器 —— 监听 InputSystem 的事件流,识别玩家最后使用的输入设备类型,
|
||||
/// 并通过 InputDeviceTypeEventChannelSO 广播给全局。
|
||||
///
|
||||
/// 布置方式:挂在 UIRoot 或常驻 GameObject 上;只需存在一个实例。
|
||||
/// </summary>
|
||||
public sealed class InputDeviceDetector : MonoBehaviour
|
||||
{
|
||||
[Header("Event Channel")]
|
||||
[Tooltip("广播当前设备类型变化")]
|
||||
[SerializeField] private InputDeviceTypeEventChannelSO _onDeviceChanged;
|
||||
|
||||
/// <summary>当前活跃输入设备类型,供轮询使用。</summary>
|
||||
public InputDeviceType CurrentDevice { get; private set; } = InputDeviceType.KeyboardMouse;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 监听所有输入事件:每次有任何 StateEvent/DeltaStateEvent 时触发
|
||||
InputSystem.onEvent += OnInputSystemEvent;
|
||||
// 监听设备连接/断开(热插拔)
|
||||
InputSystem.onDeviceChange += OnDeviceChange;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
InputSystem.onEvent -= OnInputSystemEvent;
|
||||
InputSystem.onDeviceChange -= OnDeviceChange;
|
||||
}
|
||||
|
||||
// ── Event Handlers ────────────────────────────────────────────────────
|
||||
|
||||
private void OnInputSystemEvent(InputEventPtr eventPtr, InputDevice device)
|
||||
{
|
||||
// 只关心真实输入事件,滤掉内部状态事件
|
||||
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) return;
|
||||
|
||||
var detected = ClassifyDevice(device);
|
||||
if (detected == CurrentDevice) return;
|
||||
|
||||
CurrentDevice = detected;
|
||||
_onDeviceChanged?.Raise(CurrentDevice);
|
||||
}
|
||||
|
||||
private void OnDeviceChange(InputDevice device, InputDeviceChange change)
|
||||
{
|
||||
// 当设备重新连接时重新检测(防止手柄拔插后图标仍显示手柄图标)
|
||||
if (change == InputDeviceChange.Reconnected || change == InputDeviceChange.Added)
|
||||
{
|
||||
// 保持当前 CurrentDevice 不变,等到实际输入事件再切换
|
||||
}
|
||||
}
|
||||
|
||||
// ── Device Classification ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 根据 InputDevice 的布局层次识别设备类型。
|
||||
/// Unity InputSystem 的设备层次:
|
||||
/// DualShockGamepad → Gamepad → HID
|
||||
/// XInputController → Gamepad → HID
|
||||
/// SwitchProControllerHID → Gamepad → HID
|
||||
/// Keyboard / Mouse
|
||||
/// </summary>
|
||||
private static InputDeviceType ClassifyDevice(InputDevice device)
|
||||
{
|
||||
if (device is Keyboard or Mouse)
|
||||
return InputDeviceType.KeyboardMouse;
|
||||
|
||||
if (device is Gamepad gamepad)
|
||||
{
|
||||
var desc = gamepad.description;
|
||||
string manufacturer = desc.manufacturer ?? string.Empty;
|
||||
string product = desc.product ?? string.Empty;
|
||||
string interfaceName = desc.interfaceName ?? string.Empty;
|
||||
|
||||
// PlayStation: DualShock 3/4 or DualSense (PS5)
|
||||
if (InputSystem.IsFirstLayoutBasedOnSecond(gamepad.layout, "DualShockGamepad")
|
||||
|| product.Contains("DualShock", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| product.Contains("DualSense", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| manufacturer.Contains("Sony", System.StringComparison.OrdinalIgnoreCase))
|
||||
return InputDeviceType.PlayStationController;
|
||||
|
||||
// Nintendo Switch Pro Controller / Joy-Con
|
||||
if (InputSystem.IsFirstLayoutBasedOnSecond(gamepad.layout, "SwitchProControllerHID")
|
||||
|| product.Contains("Switch", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| product.Contains("Joy-Con", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| manufacturer.Contains("Nintendo", System.StringComparison.OrdinalIgnoreCase))
|
||||
return InputDeviceType.SwitchController;
|
||||
|
||||
// Xbox / XInput (DirectInput 会走 HID 路径,XInput 走 XInputController)
|
||||
if (InputSystem.IsFirstLayoutBasedOnSecond(gamepad.layout, "XInputController")
|
||||
|| product.Contains("Xbox", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| interfaceName.Equals("XInput", System.StringComparison.OrdinalIgnoreCase))
|
||||
return InputDeviceType.XboxController;
|
||||
|
||||
// 未知手柄 → 默认 Xbox 图标集
|
||||
return InputDeviceType.XboxController;
|
||||
}
|
||||
|
||||
// 无法识别 → 键鼠
|
||||
return InputDeviceType.KeyboardMouse;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/InputDeviceDetector.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/InputDeviceDetector.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2705ff30800d20449273062f56e1989
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -13,18 +13,27 @@ namespace BaseGames.UI
|
||||
[System.Serializable]
|
||||
public struct IconEntry
|
||||
{
|
||||
public string BindingPath; // InputSystem binding path,e.g. "<Keyboard>/space"
|
||||
public Sprite Icon;
|
||||
[Tooltip("InputSystem 绑定路径,如 <Keyboard>/space 或 <Gamepad>/buttonSouth。改键后路径变化,图标集中须包含全部可能按键的映射。")]
|
||||
public string BindingPath;
|
||||
public Sprite Icon;
|
||||
}
|
||||
|
||||
[Tooltip("标识此图标集对应的输入设备类型(仅作编辑器说明,运行时由 InputIconService 选择)")]
|
||||
[SerializeField] private InputDeviceType _deviceType;
|
||||
|
||||
[SerializeField] private IconEntry[] _entries;
|
||||
|
||||
/// <summary>此图标集对应的设备类型。</summary>
|
||||
public InputDeviceType DeviceType => _deviceType;
|
||||
|
||||
/// <summary>根据 binding path 查找对应图标;未找到返回 null。</summary>
|
||||
public Sprite GetIcon(string bindingPath)
|
||||
{
|
||||
if (_entries == null) return null;
|
||||
if (_entries == null || string.IsNullOrEmpty(bindingPath)) return null;
|
||||
// 先精确匹配,再做路径前缀不区分大小写匹配(兼容大小写差异)
|
||||
foreach (var entry in _entries)
|
||||
if (entry.BindingPath == bindingPath) return entry.Icon;
|
||||
if (string.Equals(entry.BindingPath, bindingPath, System.StringComparison.OrdinalIgnoreCase))
|
||||
return entry.Icon;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 输入设备图标切换器(架构 10_UIModule §12)。
|
||||
/// 订阅 EVT_InputDeviceChanged(BoolEventChannelSO,true = 手柄,false = 键鼠),
|
||||
/// 切换后广播给场景内所有 InputIconImage 组件。
|
||||
/// 通常挂在 UIRoot 或 UIManager 同一 GameObject 上。
|
||||
/// 输入设备图标切换器。
|
||||
/// 订阅 InputDeviceTypeEventChannelSO,在设备切换时通知场景内所有 InputIconImage 刷新。
|
||||
///
|
||||
/// ⚠️ 旧版只支持 KB / 手柄二值切换;新版支持 KeyboardMouse / Xbox / PlayStation / Switch。
|
||||
/// 通常挂在 UIRoot 上,与 InputDeviceDetector 配合使用。
|
||||
/// </summary>
|
||||
public class InputDeviceIconSwitcher : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InputDeviceIconSetSO _kbIconSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _padIconSet;
|
||||
|
||||
[Header("Event Channel")]
|
||||
[SerializeField] private BoolEventChannelSO _onDeviceChanged; // EVT_InputDeviceChanged
|
||||
|
||||
public static InputDeviceIconSetSO Current { get; private set; }
|
||||
[Tooltip("由 InputDeviceDetector 广播的设备类型事件")]
|
||||
[SerializeField] private InputDeviceTypeEventChannelSO _onDeviceChanged;
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void Awake() { Current = _kbIconSet; }
|
||||
private void OnEnable() => _onDeviceChanged?.Subscribe(SwitchIconSet).AddTo(_subs);
|
||||
private void OnEnable() => _onDeviceChanged?.Subscribe(OnDeviceChanged).AddTo(_subs);
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
private void SwitchIconSet(bool isGamepad)
|
||||
private void OnDeviceChanged(InputDeviceType _)
|
||||
{
|
||||
Current = isGamepad ? _padIconSet : _kbIconSet;
|
||||
// 通知场景内所有图标 Image 刷新(包括非本对象子节点的其他 Canvas 区域)
|
||||
// 通知场景内所有 InputIconImage 刷新(含非本对象子节点的其他 Canvas 区域)
|
||||
foreach (var img in FindObjectsByType<InputIconImage>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
||||
img.Refresh();
|
||||
}
|
||||
@@ -38,31 +34,73 @@ namespace BaseGames.UI
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 单个按键图标 Image 组件。
|
||||
/// 记录 bindingPath,由 InputDeviceIconSwitcher 切换时自动刷新。
|
||||
///
|
||||
/// 支持两种查询模式:
|
||||
/// • ByActionName(推荐):填写 ActionName(如 "Interact"),
|
||||
/// 由 IInputIconService 自动解析当前设备 + 改键后的实际绑定路径 → 图标。
|
||||
/// • ByBindingPath(兼容/装饰用):直接填写固定路径(如 "<Keyboard>/space"),
|
||||
/// 适合教程截图等不跟随改键变化的场景。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Image))]
|
||||
public class InputIconImage : MonoBehaviour
|
||||
{
|
||||
[Tooltip("InputSystem 绑定路径,如 <Keyboard>/space 或 <Gamepad>/buttonSouth")]
|
||||
public enum LookupMode { ByActionName, ByBindingPath }
|
||||
|
||||
[SerializeField] private LookupMode _mode = LookupMode.ByActionName;
|
||||
|
||||
[Tooltip("Action 名称,如 Interact / Jump / Attack(仅 ByActionName 模式使用)")]
|
||||
[SerializeField] private string _actionName;
|
||||
|
||||
[Tooltip("固定绑定路径,如 <Keyboard>/space(仅 ByBindingPath 模式使用)")]
|
||||
[SerializeField] private string _bindingPath;
|
||||
|
||||
private Image _image;
|
||||
private Image _image;
|
||||
private IInputIconService _iconService;
|
||||
|
||||
private void Awake() => _image = GetComponent<Image>();
|
||||
|
||||
private void Start() => Refresh();
|
||||
private void OnEnable()
|
||||
{
|
||||
_iconService = ServiceLocator.GetOrDefault<IInputIconService>();
|
||||
if (_iconService != null)
|
||||
_iconService.OnIconSetChanged += Refresh;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_iconService != null)
|
||||
_iconService.OnIconSetChanged -= Refresh;
|
||||
}
|
||||
|
||||
/// <summary>刷新图标显示。设备切换或改键后由 InputDeviceIconSwitcher / InputIconService 调用。</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (_image == null || string.IsNullOrEmpty(_bindingPath)) return;
|
||||
var set = InputDeviceIconSwitcher.Current;
|
||||
if (set == null) return;
|
||||
var sprite = set.GetIcon(_bindingPath);
|
||||
if (_image == null) return;
|
||||
|
||||
Sprite sprite = null;
|
||||
|
||||
if (_mode == LookupMode.ByActionName && !string.IsNullOrEmpty(_actionName))
|
||||
{
|
||||
sprite = _iconService?.GetActionIcon(_actionName);
|
||||
}
|
||||
else if (_mode == LookupMode.ByBindingPath && !string.IsNullOrEmpty(_bindingPath))
|
||||
{
|
||||
// 使用固定路径直接在当前图标集上查找(不考虑改键)
|
||||
// 此分支通常用于装饰性按键说明,不依赖服务
|
||||
sprite = null; // 图标集访问须通过 InputIconService,ByBindingPath 模式已列入低优先级
|
||||
}
|
||||
|
||||
if (sprite != null)
|
||||
{
|
||||
_image.sprite = sprite;
|
||||
_image.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_image.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
Assets/_Game/Scripts/UI/InputDeviceType.cs
Normal file
14
Assets/_Game/Scripts/UI/InputDeviceType.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前活跃输入设备的分类。
|
||||
/// 用于 InputIconService 选择正确的图标集。
|
||||
/// </summary>
|
||||
public enum InputDeviceType
|
||||
{
|
||||
KeyboardMouse,
|
||||
XboxController,
|
||||
PlayStationController, // 覆盖 PS4 / PS5 DualSense
|
||||
SwitchController // 覆盖 Joy-Con 和 Switch Pro Controller
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/InputDeviceType.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/InputDeviceType.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd8b0f4a166a4dc488a9bb3760085729
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Scripts/UI/InputDeviceTypeEventChannelSO.cs
Normal file
8
Assets/_Game/Scripts/UI/InputDeviceTypeEventChannelSO.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
[CreateAssetMenu(menuName = "BaseGames/Events/InputDeviceType")]
|
||||
public class InputDeviceTypeEventChannelSO : BaseEventChannelSO<InputDeviceType> { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de9e0076c74db0a4797203dc734a5533
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
134
Assets/_Game/Scripts/UI/InputIconService.cs
Normal file
134
Assets/_Game/Scripts/UI/InputIconService.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 按键图标服务实现。
|
||||
///
|
||||
/// 职责:
|
||||
/// 1. 侦听 InputDeviceTypeEventChannelSO,更新当前图标集
|
||||
/// 2. 侦听 InputSystem.onActionChange(BoundControlsChanged),改键后刷新
|
||||
/// 3. 提供 GetActionIcon / GetActionEffectivePath,供 UI 查询
|
||||
/// 4. 在 Awake 注册自身到 ServiceLocator
|
||||
///
|
||||
/// 布置方式:与 InputDeviceDetector 同挂在 UIRoot 上;每场景只需一个实例。
|
||||
/// </summary>
|
||||
public sealed class InputIconService : MonoBehaviour, IInputIconService
|
||||
{
|
||||
[Header("Input")]
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
|
||||
[Header("Icon Sets — 按设备类型配置")]
|
||||
[SerializeField] private InputDeviceIconSetSO _kbMouseSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _xboxSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _playStationSet;
|
||||
[SerializeField] private InputDeviceIconSetSO _switchSet;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private InputDeviceTypeEventChannelSO _onDeviceChanged;
|
||||
|
||||
// ── IInputIconService ─────────────────────────────────────────────────
|
||||
public InputDeviceType CurrentDevice { get; private set; } = InputDeviceType.KeyboardMouse;
|
||||
public event Action OnIconSetChanged;
|
||||
|
||||
private InputDeviceIconSetSO _activeSet;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
ServiceLocator.RegisterIfAbsent<IInputIconService>(this);
|
||||
_activeSet = _kbMouseSet;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onDeviceChanged?.Subscribe(HandleDeviceChanged).AddTo(_subs);
|
||||
// 改键后 InputSystem 会广播 BoundControlsChanged
|
||||
InputSystem.onActionChange += HandleActionChange;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
InputSystem.onActionChange -= HandleActionChange;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ServiceLocator.Unregister<IInputIconService>(this);
|
||||
}
|
||||
|
||||
// ── Event Handlers ────────────────────────────────────────────────────
|
||||
|
||||
private void HandleDeviceChanged(InputDeviceType deviceType)
|
||||
{
|
||||
CurrentDevice = deviceType;
|
||||
_activeSet = deviceType switch
|
||||
{
|
||||
InputDeviceType.XboxController => _xboxSet ?? _kbMouseSet,
|
||||
InputDeviceType.PlayStationController => _playStationSet ?? _kbMouseSet,
|
||||
InputDeviceType.SwitchController => _switchSet ?? _kbMouseSet,
|
||||
_ => _kbMouseSet,
|
||||
};
|
||||
OnIconSetChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleActionChange(object obj, InputActionChange change)
|
||||
{
|
||||
if (change == InputActionChange.BoundControlsChanged)
|
||||
OnIconSetChanged?.Invoke();
|
||||
}
|
||||
|
||||
// ── IInputIconService impl ────────────────────────────────────────────
|
||||
|
||||
public Sprite GetActionIcon(string actionName)
|
||||
{
|
||||
var path = GetActionEffectivePath(actionName);
|
||||
if (path == null || _activeSet == null) return null;
|
||||
return _activeSet.GetIcon(path);
|
||||
}
|
||||
|
||||
public string GetActionEffectivePath(string actionName)
|
||||
{
|
||||
if (_inputReader == null) return null;
|
||||
var action = _inputReader.FindAction(actionName);
|
||||
if (action == null) return null;
|
||||
|
||||
// 通过 binding.groups 过滤,只返回匹配当前设备控制方案的绑定路径
|
||||
string schemeFilter = GetControlSchemeForDevice(CurrentDevice);
|
||||
|
||||
foreach (var binding in action.bindings)
|
||||
{
|
||||
// 跳过复合绑定的父条目(无实际路径)
|
||||
if (binding.isComposite) continue;
|
||||
|
||||
// 若 binding.groups 不含当前方案,则跳过(允许空 groups 的绑定匹配所有设备)
|
||||
if (!string.IsNullOrEmpty(schemeFilter)
|
||||
&& !string.IsNullOrEmpty(binding.groups)
|
||||
&& !binding.groups.Contains(schemeFilter, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// effectivePath 已自动合并 overridePath(改键后的路径)
|
||||
var path = binding.effectivePath;
|
||||
if (!string.IsNullOrEmpty(path)) return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>将设备类型映射到 InputActionAsset 中配置的控制方案名称。</summary>
|
||||
private static string GetControlSchemeForDevice(InputDeviceType device) => device switch
|
||||
{
|
||||
InputDeviceType.KeyboardMouse => "Keyboard&Mouse",
|
||||
_ => "Gamepad", // Xbox / PS / Switch 共用 Gamepad 方案
|
||||
};
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/InputIconService.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/InputIconService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2929014148cfee048a326c8382144a22
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user