优化多平台设备输入系统映射宽假

This commit is contained in:
陈思海 2026-03-17 20:02:47 +08:00
parent 6aac14d3e5
commit f4ec668bfd
10 changed files with 1630 additions and 1261 deletions

View File

@ -1,18 +1,17 @@
using System; using System;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
public static class GlyphService public static class GlyphService
{ {
// 缓存的设备提示数组,避免内存分配 private static readonly string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" };
private static readonly string[] KeyboardHints = { "Keyboard", "Mouse" }; private static readonly string[] XboxGroupHints = { "xbox", "xinput", "gamepad", "controller" };
private static readonly string[] XboxHints = { "XInput", "Xbox", "Gamepad" }; private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" };
private static readonly string[] PlayStationHints = { "DualShock", "DualSense", "PlayStation", "Gamepad" }; private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" };
private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
/// <summary> private static InputGlyphDatabase _database;
/// 获取输入图标数据库实例
/// </summary>
public static InputGlyphDatabase Database public static InputGlyphDatabase Database
{ {
get get
@ -26,179 +25,285 @@ public static class GlyphService
} }
} }
private static InputGlyphDatabase _database; public static string GetBindingControlPath(
InputAction action,
string compositePartName = null,
/// <summary> InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
/// 获取输入操作的绑定控制路径
/// </summary>
/// <param name="action">输入操作</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <param name="deviceOverride">设备类型覆盖(可选)</param>
/// <returns>绑定控制路径</returns>
public static string GetBindingControlPath(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{ {
if (action == null) return string.Empty; return TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding)
var binding = GetBindingControl(action, compositePartName, deviceOverride); ? GetEffectivePath(binding)
return binding.hasOverrides ? binding.effectivePath : binding.path; : string.Empty;
} }
/// <summary> public static string GetBindingControlPath(
/// 尝试获取输入操作的 TextMeshPro 标签 InputActionReference actionReference,
/// </summary> string compositePartName = null,
/// <param name="reference">输入操作引用</param> InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
/// <param name="compositePartName">复合部分名称</param>
/// <param name="device">设备类型</param>
/// <param name="tag">输出的 TMP 标签</param>
/// <param name="displayFallback">显示回退文本</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetTMPTagForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null)
{ {
string path = GetBindingControlPath(reference, compositePartName, device); return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride);
return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db);
} }
/// <summary> public static bool TryGetTMPTagForActionPath(
/// 尝试获取输入操作的 UI Sprite InputAction action,
/// </summary> string compositePartName,
/// <param name="reference">输入操作引用</param> InputDeviceWatcher.InputDeviceCategory device,
/// <param name="compositePartName">复合部分名称</param> out string tag,
/// <param name="device">设备类型</param> out string displayFallback,
/// <param name="sprite">输出的 Sprite</param> InputGlyphDatabase db = null)
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetUISpriteForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null)
{ {
string path = GetBindingControlPath(reference, compositePartName, device); string controlPath = GetBindingControlPath(action, compositePartName, device);
return TryGetUISpriteForActionPath(path, device, out sprite, db); return TryGetTMPTagForActionPath(controlPath, device, out tag, out displayFallback, db);
} }
/// <summary> public static bool TryGetTMPTagForActionPath(
/// 根据控制路径尝试获取 TextMeshPro 标签 InputActionReference actionReference,
/// </summary> string compositePartName,
/// <param name="controlPath">控制路径</param> InputDeviceWatcher.InputDeviceCategory device,
/// <param name="device">设备类型</param> out string tag,
/// <param name="tag">输出的 TMP 标签</param> out string displayFallback,
/// <param name="displayFallback">显示回退文本</param> InputGlyphDatabase db = null)
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetTMPTagForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null)
{ {
tag = null; return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db);
displayFallback = null; }
db = db ?? Database;
public static bool TryGetUISpriteForActionPath(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
string controlPath = GetBindingControlPath(action, compositePartName, device);
return TryGetUISpriteForActionPath(controlPath, device, out sprite, db);
}
public static bool TryGetUISpriteForActionPath(
InputActionReference actionReference,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
return TryGetUISpriteForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out sprite, db);
}
public static bool TryGetTMPTagForActionPath(
string controlPath,
InputDeviceWatcher.InputDeviceCategory device,
out string tag,
out string displayFallback,
InputGlyphDatabase db = null)
{
displayFallback = GetDisplayNameFromControlPath(controlPath); displayFallback = GetDisplayNameFromControlPath(controlPath);
tag = null;
var sprite = db.FindSprite(controlPath, device) ?? db.FindSprite(controlPath, InputDeviceWatcher.InputDeviceCategory.Keyboard); if (!TryGetUISpriteForActionPath(controlPath, device, out Sprite sprite, db))
var spriteName = sprite == null ? string.Empty : sprite.name;
tag = $"<sprite name=\"{spriteName}\">";
return true;
}
/// <summary>
/// 根据控制路径尝试获取 UI Sprite
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <param name="sprite">输出的 Sprite</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetUISpriteForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null)
{ {
sprite = null;
db = db ?? Database;
if (string.IsNullOrEmpty(controlPath) || db == null) return false;
sprite = db.FindSprite(controlPath, device) ?? db.FindSprite(controlPath, InputDeviceWatcher.InputDeviceCategory.Keyboard);
return sprite != null;
}
/// <summary>
/// 获取输入操作的绑定控制
/// </summary>
static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{
if (action == null) return default;
var curCategory = deviceOverride ?? InputDeviceWatcher.CurrentCategory;
var hints = GetDeviceHintsForCategory(curCategory);
foreach (var b in action.bindings)
{
if (!string.IsNullOrEmpty(compositePartName))
{
if (!b.isPartOfComposite) continue;
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
}
// 替换 LINQ Any() 以避免委托分配
if (!string.IsNullOrEmpty(b.path) && ContainsAnyHint(b.path, hints)) return b;
if (!string.IsNullOrEmpty(b.effectivePath) && ContainsAnyHint(b.effectivePath, hints)) return b;
}
return default;
}
// 辅助方法,避免 LINQ Any() 的内存分配
/// <summary>
/// 检查路径是否包含任何提示字符串
/// </summary>
static bool ContainsAnyHint(string path, string[] hints)
{
for (int i = 0; i < hints.Length; i++)
{
if (path.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0)
return true;
}
return false; return false;
} }
/// <summary> tag = $"<sprite name=\"{sprite.name}\">";
/// 根据设备类型获取设备提示字符串数组 return true;
/// </summary>
static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat)
{
switch (cat)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardHints;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxHints;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationHints;
default:
return XboxHints;
}
} }
public static bool TryGetUISpriteForActionPath(
/// <summary> string controlPath,
/// 从输入操作获取显示名称 InputDeviceWatcher.InputDeviceCategory device,
/// </summary> out Sprite sprite,
/// <param name="action">输入操作</param> InputGlyphDatabase db = null)
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <param name="deviceOverride">设备类型覆盖</param>
/// <returns>显示名称</returns>
public static string GetDisplayNameFromInputAction(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory deviceOverride = InputDeviceWatcher.InputDeviceCategory.Keyboard)
{ {
if (action == null) return string.Empty; sprite = null;
var binding = GetBindingControl(action, compositePartName, deviceOverride); db ??= Database;
return binding.ToDisplayString(); return db != null && db.TryGetSprite(controlPath, device, out sprite);
}
public static string GetDisplayNameFromInputAction(
InputAction action,
string compositePartName = null,
InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{
if (!TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding))
{
return string.Empty;
}
string display = binding.ToDisplayString();
return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display;
} }
/// <summary>
/// 从控制路径获取显示名称
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <returns>显示名称</returns>
public static string GetDisplayNameFromControlPath(string controlPath) public static string GetDisplayNameFromControlPath(string controlPath)
{ {
if (string.IsNullOrEmpty(controlPath)) return string.Empty; if (string.IsNullOrWhiteSpace(controlPath))
var parts = controlPath.Split('/'); {
var last = parts[parts.Length - 1].Trim(TrimChars); return string.Empty;
}
string[] parts = controlPath.Split('/');
string last = parts[parts.Length - 1].Trim(TrimChars);
return last; return last;
} }
public static bool TryGetBindingControl(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory? deviceOverride,
out InputBinding binding)
{
binding = default;
if (action == null)
{
return false;
}
InputDeviceWatcher.InputDeviceCategory category = deviceOverride ?? InputDeviceWatcher.CurrentCategory;
int bestScore = int.MinValue;
bool requireCompositePart = !string.IsNullOrEmpty(compositePartName);
for (int i = 0; i < action.bindings.Count; i++)
{
InputBinding candidate = action.bindings[i];
if (candidate.isComposite)
{
continue;
}
if (requireCompositePart)
{
if (!candidate.isPartOfComposite || !string.Equals(candidate.name, compositePartName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
}
else if (candidate.isPartOfComposite)
{
continue;
}
string path = GetEffectivePath(candidate);
if (string.IsNullOrWhiteSpace(path))
{
continue;
}
int score = ScoreBinding(candidate, category);
if (score > bestScore)
{
bestScore = score;
binding = candidate;
}
}
return bestScore > int.MinValue;
}
private static int ScoreBinding(InputBinding binding, InputDeviceWatcher.InputDeviceCategory category)
{
int score = 0;
string path = GetEffectivePath(binding);
if (MatchesBindingGroups(binding.groups, category))
{
score += 100;
}
else if (!string.IsNullOrWhiteSpace(binding.groups))
{
score -= 20;
}
if (MatchesControlPath(path, category))
{
score += 60;
}
if (!binding.isPartOfComposite)
{
score += 5;
}
return score;
}
private static bool MatchesBindingGroups(string groups, InputDeviceWatcher.InputDeviceCategory category)
{
if (string.IsNullOrWhiteSpace(groups))
{
return false;
}
string[] hints = GetGroupHints(category);
string[] tokens = groups.Split(InputBinding.Separator);
for (int i = 0; i < tokens.Length; i++)
{
string token = tokens[i].Trim();
if (ContainsAny(token, hints))
{
return true;
}
}
return false;
}
private static bool MatchesControlPath(string path, InputDeviceWatcher.InputDeviceCategory category)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
switch (category)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return StartsWithDevice(path, "<Keyboard>") || StartsWithDevice(path, "<Mouse>");
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, XboxGroupHints);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, PlayStationGroupHints);
default:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, OtherGamepadGroupHints);
}
}
private static bool StartsWithDevice(string path, string deviceTag)
{
return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase);
}
private static string[] GetGroupHints(InputDeviceWatcher.InputDeviceCategory category)
{
switch (category)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardGroupHints;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxGroupHints;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationGroupHints;
default:
return OtherGamepadGroupHints;
}
}
private static bool ContainsAny(string source, string[] hints)
{
if (string.IsNullOrWhiteSpace(source) || hints == null)
{
return false;
}
for (int i = 0; i < hints.Length; i++)
{
if (source.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private static string GetEffectivePath(InputBinding binding)
{
return string.IsNullOrWhiteSpace(binding.effectivePath) ? binding.path : binding.effectivePath;
}
} }

View File

@ -1,5 +1,3 @@
// InputBindingManager.cs
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -8,12 +6,10 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using AlicizaX; using AlicizaX;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
public class InputBindingManager : MonoSingleton<InputBindingManager>
public class InputBindingManager : MonoSingleton<InputBindingManager> {
{
public const string NULL_BINDING = "__NULL__"; public const string NULL_BINDING = "__NULL__";
private const string KEYBOARD_DEVICE = "<Keyboard>"; private const string KEYBOARD_DEVICE = "<Keyboard>";
private const string MOUSE_DELTA = "<Mouse>/delta"; private const string MOUSE_DELTA = "<Mouse>/delta";
@ -39,6 +35,7 @@ using Cysharp.Threading.Tasks;
// 用于替代 Rx.NET Subjects 的事件 // 用于替代 Rx.NET Subjects 的事件
private event Action _onInputsInit; private event Action _onInputsInit;
public event Action OnInputsInit public event Action OnInputsInit
{ {
add add
@ -50,10 +47,7 @@ using Cysharp.Threading.Tasks;
value?.Invoke(); value?.Invoke();
} }
} }
remove remove { _onInputsInit -= value; }
{
_onInputsInit -= value;
}
} }
public event Action<bool, HashSet<RebindContext>> OnApply; public event Action<bool, HashSet<RebindContext>> OnApply;
@ -61,6 +55,7 @@ using Cysharp.Threading.Tasks;
public event Action OnRebindStart; public event Action OnRebindStart;
public event Action<bool, RebindContext> OnRebindEnd; public event Action<bool, RebindContext> OnRebindEnd;
public event Action<RebindContext, RebindContext> OnRebindConflict; public event Action<RebindContext, RebindContext> OnRebindConflict;
public static event Action BindingsChanged;
private bool isInputsInitialized = false; private bool isInputsInitialized = false;
@ -125,6 +120,7 @@ using Cysharp.Threading.Tasks;
{ {
actions.LoadBindingOverridesFromJson(json); actions.LoadBindingOverridesFromJson(json);
RefreshBindingPathsFromActions(); RefreshBindingPathsFromActions();
BindingsChanged?.Invoke();
if (debugMode) if (debugMode)
{ {
Debug.Log($"Loaded overrides from {SavePath}"); Debug.Log($"Loaded overrides from {SavePath}");
@ -159,6 +155,7 @@ using Cysharp.Threading.Tasks;
OnRebindStart = null; OnRebindStart = null;
OnRebindEnd = null; OnRebindEnd = null;
OnRebindConflict = null; OnRebindConflict = null;
BindingsChanged = null;
} }
private void BuildActionMap() private void BuildActionMap()
@ -455,6 +452,7 @@ using Cysharp.Threading.Tasks;
Instance.preparedRebinds.Clear(); Instance.preparedRebinds.Clear();
await Instance.WriteOverridesToDiskAsync(); await Instance.WriteOverridesToDiskAsync();
BindingsChanged?.Invoke();
Instance.OnApply?.Invoke(true, appliedContexts); Instance.OnApply?.Invoke(true, appliedContexts);
Instance.isApplyPending = false; Instance.isApplyPending = false;
if (Instance.debugMode) if (Instance.debugMode)
@ -674,6 +672,7 @@ using Cysharp.Threading.Tasks;
RefreshBindingPathsFromActions(); RefreshBindingPathsFromActions();
await WriteOverridesToDiskAsync(); await WriteOverridesToDiskAsync();
BindingsChanged?.Invoke();
if (debugMode) if (debugMode)
{ {
Debug.Log("Reset to default and saved."); Debug.Log("Reset to default and saved.");
@ -767,5 +766,4 @@ using Cysharp.Threading.Tasks;
} }
private static InputBindingManager _instance; private static InputBindingManager _instance;
} }

View File

@ -1,9 +1,10 @@
using System; using System;
using System.Text.RegularExpressions; #if UNITY_EDITOR
using AlicizaX;
using UnityEditor; using UnityEditor;
#endif
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
public static class InputDeviceWatcher public static class InputDeviceWatcher
{ {
@ -15,35 +16,108 @@ public static class InputDeviceWatcher
Other Other
} }
static readonly float DebounceWindow = 1f; public readonly struct DeviceContext : IEquatable<DeviceContext>
public static InputDeviceCategory CurrentCategory = InputDeviceCategory.Keyboard; {
public static string CurrentDeviceName = ""; public readonly InputDeviceCategory Category;
public readonly int DeviceId;
public readonly int VendorId;
public readonly int ProductId;
public readonly string DeviceName;
public readonly string Layout;
public DeviceContext(
InputDeviceCategory category,
int deviceId,
int vendorId,
int productId,
string deviceName,
string layout)
{
Category = category;
DeviceId = deviceId;
VendorId = vendorId;
ProductId = productId;
DeviceName = deviceName ?? string.Empty;
Layout = layout ?? string.Empty;
}
public bool Equals(DeviceContext other)
{
return Category == other.Category
&& DeviceId == other.DeviceId
&& VendorId == other.VendorId
&& ProductId == other.ProductId
&& string.Equals(DeviceName, other.DeviceName, StringComparison.Ordinal)
&& string.Equals(Layout, other.Layout, StringComparison.Ordinal);
}
public override bool Equals(object obj)
{
return obj is DeviceContext other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = (int)Category;
hashCode = (hashCode * 397) ^ DeviceId;
hashCode = (hashCode * 397) ^ VendorId;
hashCode = (hashCode * 397) ^ ProductId;
hashCode = (hashCode * 397) ^ (DeviceName != null ? DeviceName.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (Layout != null ? Layout.GetHashCode() : 0);
return hashCode;
}
}
}
[Serializable]
private struct DeviceCapabilityInfo
{
public int vendorId;
public int productId;
}
private const float SameCategoryDebounceWindow = 0.15f;
private const float AxisActivationThreshold = 0.5f;
private const float StickActivationThreshold = 0.25f;
private const string DefaultKeyboardDeviceName = "Keyboard&Mouse";
public static InputDeviceCategory CurrentCategory { get; private set; } = InputDeviceCategory.Keyboard;
public static string CurrentDeviceName { get; private set; } = DefaultKeyboardDeviceName;
public static int CurrentDeviceId { get; private set; } = -1;
public static int CurrentVendorId { get; private set; }
public static int CurrentProductId { get; private set; }
public static DeviceContext CurrentContext { get; private set; } = CreateDefaultContext();
private static InputAction _anyInputAction; private static InputAction _anyInputAction;
private static int _lastDeviceId = -1; private static float _lastSwitchTime = -Mathf.Infinity;
private static float _lastInputTime = -Mathf.Infinity; private static DeviceContext _lastEmittedContext = CreateDefaultContext();
private static bool _initialized;
private static InputDeviceCategory _lastEmittedCategory = InputDeviceCategory.Keyboard;
public static event Action<InputDeviceCategory> OnDeviceChanged; public static event Action<InputDeviceCategory> OnDeviceChanged;
public static event Action<DeviceContext> OnDeviceContextChanged;
private static bool initialized = false;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void Initialize() public static void Initialize()
{ {
if (initialized) return; if (_initialized)
initialized = true; {
return;
}
CurrentCategory = InputDeviceCategory.Keyboard; _initialized = true;
CurrentDeviceName = ""; ApplyContext(CreateDefaultContext(), false);
_lastEmittedCategory = CurrentCategory; // 初始化同步 _lastEmittedContext = CurrentContext;
_anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough); _anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough);
_anyInputAction.AddBinding("<Keyboard>/anyKey"); _anyInputAction.AddBinding("<Keyboard>/anyKey");
_anyInputAction.AddBinding("<Mouse>/leftButton");
_anyInputAction.AddBinding("<Mouse>/rightButton");
_anyInputAction.AddBinding("<Mouse>/middleButton");
_anyInputAction.AddBinding("<Mouse>/scroll");
_anyInputAction.AddBinding("<Gamepad>/*"); _anyInputAction.AddBinding("<Gamepad>/*");
_anyInputAction.AddBinding("<Joystick>/*"); _anyInputAction.AddBinding("<Joystick>/*");
_anyInputAction.performed += OnAnyInputPerformed; _anyInputAction.performed += OnAnyInputPerformed;
_anyInputAction.Enable(); _anyInputAction.Enable();
@ -54,142 +128,311 @@ public static class InputDeviceWatcher
} }
#if UNITY_EDITOR #if UNITY_EDITOR
static void OnPlayModeStateChanged(PlayModeStateChange state) private static void OnPlayModeStateChanged(PlayModeStateChange state)
{ {
if (state == PlayModeStateChange.ExitingPlayMode) if (state == PlayModeStateChange.ExitingPlayMode)
{ {
Dispose(); Dispose();
}
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
} }
}
#endif #endif
public static void Dispose() public static void Dispose()
{ {
if (!initialized) return; if (!_initialized)
CurrentCategory = InputDeviceCategory.Keyboard; {
return;
}
if (_anyInputAction != null)
{
_anyInputAction.performed -= OnAnyInputPerformed; _anyInputAction.performed -= OnAnyInputPerformed;
_anyInputAction.Disable(); _anyInputAction.Disable();
_anyInputAction.Dispose(); _anyInputAction.Dispose();
_anyInputAction = null;
}
InputSystem.onDeviceChange -= OnDeviceChange; InputSystem.onDeviceChange -= OnDeviceChange;
ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext;
_lastSwitchTime = -Mathf.Infinity;
OnDeviceChanged = null; OnDeviceChanged = null;
initialized = false; OnDeviceContextChanged = null;
_initialized = false;
_lastEmittedCategory = InputDeviceCategory.Keyboard;
} }
private static void OnAnyInputPerformed(InputAction.CallbackContext ctx) private static void OnAnyInputPerformed(InputAction.CallbackContext context)
{ {
if (ctx.control == null || ctx.control.device == null) return; InputControl control = context.control;
if (!IsRelevantControl(control))
{
return;
}
var device = ctx.control.device; DeviceContext deviceContext = BuildContext(control.device);
if (deviceContext.DeviceId == CurrentDeviceId)
{
return;
}
if (!IsRelevantDevice(device)) return;
int curId = device.deviceId;
float now = Time.realtimeSinceStartup; float now = Time.realtimeSinceStartup;
if (deviceContext.Category == CurrentCategory && now - _lastSwitchTime < SameCategoryDebounceWindow)
{
return;
}
if (curId == _lastDeviceId) return; _lastSwitchTime = now;
if (DebounceWindow > 0f && (now - _lastInputTime) < DebounceWindow) return; SetCurrentContext(deviceContext);
_lastInputTime = now;
_lastDeviceId = curId;
CurrentCategory = DetermineCategoryFromDevice(device);
CurrentDeviceName = device.displayName ?? $"Device_{curId}";
EmitChange();
} }
private static void OnDeviceChange(InputDevice device, InputDeviceChange change) private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
{ {
if (change == InputDeviceChange.Removed || change == InputDeviceChange.Disconnected) if (device == null)
{ {
if (device.deviceId == _lastDeviceId) return;
{
_lastDeviceId = -1;
_lastInputTime = -Mathf.Infinity;
CurrentDeviceName = "";
CurrentCategory = InputDeviceCategory.Keyboard;
EmitChange();
} }
switch (change)
{
case InputDeviceChange.Removed:
case InputDeviceChange.Disconnected:
if (device.deviceId == CurrentDeviceId)
{
PromoteFallbackDevice(device.deviceId);
}
break;
case InputDeviceChange.Reconnected:
case InputDeviceChange.Added:
if (CurrentDeviceId < 0 && IsRelevantDevice(device))
{
SetCurrentContext(BuildContext(device));
}
break;
} }
} }
// ------------------ 分类逻辑 -------------------- private static void PromoteFallbackDevice(int removedDeviceId)
{
for (int i = InputSystem.devices.Count - 1; i >= 0; i--)
{
InputDevice device = InputSystem.devices[i];
if (device == null || device.deviceId == removedDeviceId || !device.added || !IsRelevantDevice(device))
{
continue;
}
SetCurrentContext(BuildContext(device));
return;
}
SetCurrentContext(CreateDefaultContext());
}
private static void SetCurrentContext(DeviceContext context)
{
bool categoryChanged = CurrentCategory != context.Category;
ApplyContext(context, true);
if (!_lastEmittedContext.Equals(context))
{
OnDeviceContextChanged?.Invoke(context);
if (categoryChanged)
{
OnDeviceChanged?.Invoke(context.Category);
}
_lastEmittedContext = context;
}
}
private static void ApplyContext(DeviceContext context, bool log)
{
CurrentContext = context;
CurrentCategory = context.Category;
CurrentDeviceId = context.DeviceId;
CurrentVendorId = context.VendorId;
CurrentProductId = context.ProductId;
CurrentDeviceName = context.DeviceName;
#if UNITY_EDITOR
if (log)
{
AlicizaX.Log.Info($"Input device -> {CurrentCategory} name={CurrentDeviceName} vid=0x{CurrentVendorId:X} pid=0x{CurrentProductId:X} id={CurrentDeviceId}");
}
#endif
}
private static DeviceContext BuildContext(InputDevice device)
{
if (device == null)
{
return CreateDefaultContext();
}
TryParseVendorProductIds(device.description.capabilities, out int vendorId, out int productId);
string deviceName = string.IsNullOrWhiteSpace(device.displayName) ? device.name : device.displayName;
return new DeviceContext(
DetermineCategoryFromDevice(device),
device.deviceId,
vendorId,
productId,
deviceName,
device.layout);
}
private static DeviceContext CreateDefaultContext()
{
return new DeviceContext(InputDeviceCategory.Keyboard, -1, 0, 0, DefaultKeyboardDeviceName, Keyboard.current != null ? Keyboard.current.layout : string.Empty);
}
private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device) private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device)
{ {
if (device == null) return InputDeviceCategory.Keyboard; if (device == null)
// 重要:鼠标不再被视为键盘类(避免鼠标触发时回退到 Keyboard {
if (device is Keyboard) return InputDeviceCategory.Keyboard; return InputDeviceCategory.Keyboard;
if (device is Mouse) return InputDeviceCategory.Other; // 明确忽略鼠标
if (IsGamepadLike(device)) return GetGamepadCategory(device);
string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower();
if (combined.Contains("xbox") || combined.Contains("xinput")) return InputDeviceCategory.Xbox;
if (combined.Contains("dualshock") || combined.Contains("dualsense") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation;
return InputDeviceCategory.Other;
} }
private static bool IsGamepadLike(InputDevice device) if (device is Keyboard || device is Mouse)
{ {
if (device is Gamepad) return true; return InputDeviceCategory.Keyboard;
if (device is Joystick) return true; }
var layout = (device.layout ?? "").ToLower(); if (IsGamepadLike(device))
// 这里保留 controller/gamepad/joystick 的识别,但忽略 mouse/touch 等 {
if (layout.Contains("mouse") || layout.Contains("touch") || layout.Contains("pen")) return false; return GetGamepadCategory(device);
return layout.Contains("gamepad") || layout.Contains("controller") || layout.Contains("joystick"); }
string combined = CombineDeviceDescription(device);
if (ContainsIgnoreCase(combined, "xbox") || ContainsIgnoreCase(combined, "xinput"))
{
return InputDeviceCategory.Xbox;
}
if (ContainsIgnoreCase(combined, "dualshock")
|| ContainsIgnoreCase(combined, "dualsense")
|| ContainsIgnoreCase(combined, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
return InputDeviceCategory.Other;
} }
private static bool IsRelevantDevice(InputDevice device) private static bool IsRelevantDevice(InputDevice device)
{ {
if (device == null) return false; return device is Keyboard || device is Mouse || IsGamepadLike(device);
if (device is Keyboard) return true; }
if (IsGamepadLike(device)) return true;
private static bool IsRelevantControl(InputControl control)
{
if (control == null || control.device == null || !IsRelevantDevice(control.device) || control.synthetic)
{
return false; return false;
} }
switch (control)
{
case ButtonControl button:
return button.IsPressed();
case StickControl stick:
return stick.ReadValue().sqrMagnitude >= StickActivationThreshold;
case Vector2Control vector2:
return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold;
case AxisControl axis:
return Mathf.Abs(axis.ReadValue()) >= AxisActivationThreshold;
default:
return !control.noisy;
}
}
private static bool IsGamepadLike(InputDevice device)
{
if (device is Gamepad || device is Joystick)
{
return true;
}
string layout = device.layout ?? string.Empty;
if (ContainsIgnoreCase(layout, "Mouse")
|| ContainsIgnoreCase(layout, "Touch")
|| ContainsIgnoreCase(layout, "Pen"))
{
return false;
}
return ContainsIgnoreCase(layout, "Gamepad")
|| ContainsIgnoreCase(layout, "Controller")
|| ContainsIgnoreCase(layout, "Joystick");
}
private static InputDeviceCategory GetGamepadCategory(InputDevice device) private static InputDeviceCategory GetGamepadCategory(InputDevice device)
{ {
if (device == null) return InputDeviceCategory.Other; if (device == null)
var iface = (device.description.interfaceName ?? "").ToLower();
if (iface.Contains("xinput")) return InputDeviceCategory.Xbox;
if (TryParseVidPidFromCapabilities(device.description.capabilities, out int vendorId, out int _))
{ {
if (vendorId == 0x045E || vendorId == 1118) return InputDeviceCategory.Xbox; return InputDeviceCategory.Other;
if (vendorId == 0x054C || vendorId == 1356) return InputDeviceCategory.PlayStation;
} }
string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower(); string interfaceName = device.description.interfaceName ?? string.Empty;
if (combined.Contains("xbox")) return InputDeviceCategory.Xbox; if (ContainsIgnoreCase(interfaceName, "xinput"))
if (combined.Contains("dualshock") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation; {
return InputDeviceCategory.Xbox;
}
if (TryParseVendorProductIds(device.description.capabilities, out int vendorId, out _))
{
if (vendorId == 0x045E || vendorId == 1118)
{
return InputDeviceCategory.Xbox;
}
if (vendorId == 0x054C || vendorId == 1356)
{
return InputDeviceCategory.PlayStation;
}
}
string combined = CombineDeviceDescription(device);
if (ContainsIgnoreCase(combined, "xbox"))
{
return InputDeviceCategory.Xbox;
}
if (ContainsIgnoreCase(combined, "dualshock")
|| ContainsIgnoreCase(combined, "dualsense")
|| ContainsIgnoreCase(combined, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
return InputDeviceCategory.Other; return InputDeviceCategory.Other;
} }
// ------------------ VID/PID 解析 -------------------- private static string CombineDeviceDescription(InputDevice device)
private static bool TryParseVidPidFromCapabilities(string capabilities, out int vendorId, out int productId) {
return string.Concat(
device.description.interfaceName, " ",
device.layout, " ",
device.description.product, " ",
device.description.manufacturer, " ",
device.displayName);
}
private static bool TryParseVendorProductIds(string capabilities, out int vendorId, out int productId)
{ {
vendorId = 0; vendorId = 0;
productId = 0; productId = 0;
if (string.IsNullOrEmpty(capabilities)) return false; if (string.IsNullOrWhiteSpace(capabilities))
{
return false;
}
try try
{ {
var decVendor = Regex.Match(capabilities, "\"vendorId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase); DeviceCapabilityInfo info = JsonUtility.FromJson<DeviceCapabilityInfo>(capabilities);
var decProduct = Regex.Match(capabilities, "\"productId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase); vendorId = info.vendorId;
productId = info.productId;
if (decVendor.Success) int.TryParse(decVendor.Groups[1].Value, out vendorId);
if (decProduct.Success) int.TryParse(decProduct.Groups[1].Value, out productId);
return vendorId != 0 || productId != 0; return vendorId != 0 || productId != 0;
} }
catch catch
@ -198,45 +441,8 @@ public static class InputDeviceWatcher
} }
} }
private static void EmitChange() private static bool ContainsIgnoreCase(string source, string value)
{ {
if (CurrentCategory == _lastEmittedCategory) return !string.IsNullOrEmpty(source) && source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
{
return;
}
int vid = GetVendorId();
int pid = GetProductId();
#if UNITY_EDITOR
Log.Info($"输入设备变更 -> {CurrentCategory} 触发设备: {CurrentDeviceName} vid=0x{vid:X} pid=0x{pid:X}");
#endif
OnDeviceChanged?.Invoke(CurrentCategory);
_lastEmittedCategory = CurrentCategory;
}
private static int GetVendorId()
{
foreach (var d in InputSystem.devices)
{
if ((d.displayName ?? "") == CurrentDeviceName &&
TryParseVidPidFromCapabilities(d.description.capabilities, out int v, out int _))
return v;
}
return 0;
}
private static int GetProductId()
{
foreach (var d in InputSystem.devices)
{
if ((d.displayName ?? "") == CurrentDeviceName &&
TryParseVidPidFromCapabilities(d.description.capabilities, out int _, out int p))
return p;
}
return 0;
} }
} }

View File

@ -0,0 +1,33 @@
using UnityEngine;
public abstract class InputGlyphBehaviourBase : MonoBehaviour
{
protected InputDeviceWatcher.InputDeviceCategory CurrentCategory { get; private set; }
protected virtual void OnEnable()
{
CurrentCategory = InputDeviceWatcher.CurrentCategory;
InputDeviceWatcher.OnDeviceContextChanged += HandleDeviceContextChanged;
InputBindingManager.BindingsChanged += HandleBindingsChanged;
RefreshGlyph();
}
protected virtual void OnDisable()
{
InputDeviceWatcher.OnDeviceContextChanged -= HandleDeviceContextChanged;
InputBindingManager.BindingsChanged -= HandleBindingsChanged;
}
private void HandleDeviceContextChanged(InputDeviceWatcher.DeviceContext context)
{
CurrentCategory = context.Category;
RefreshGlyph();
}
private void HandleBindingsChanged()
{
RefreshGlyph();
}
protected abstract void RefreshGlyph();
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a16393b81b47f1844a49d492284be475
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,10 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.U2D;
[Serializable] [Serializable]
public sealed class GlyphEntry public sealed class GlyphEntry
@ -17,205 +14,265 @@ public sealed class GlyphEntry
public sealed class DeviceGlyphTable public sealed class DeviceGlyphTable
{ {
public string deviceName; public string deviceName;
public Texture2D spriteSheetTexture; public Texture2D spriteSheetTexture;
public Sprite platformIcons; public Sprite platformIcons;
public List<GlyphEntry> entries = new List<GlyphEntry>(); public List<GlyphEntry> entries = new List<GlyphEntry>();
} }
[CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "InputGlyphs/InputGlyphDatabase", order = 400)] [CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "InputGlyphs/InputGlyphDatabase", order = 400)]
public sealed class InputGlyphDatabase : ScriptableObject public sealed class InputGlyphDatabase : ScriptableObject
{ {
private const string DEVICE_KEYBOARD = "Keyboard"; private const string DeviceKeyboard = "Keyboard";
private const string DEVICE_XBOX = "Xbox"; private const string DeviceXbox = "Xbox";
private const string DEVICE_PLAYSTATION = "PlayStation"; private const string DevicePlayStation = "PlayStation";
private const string DeviceOther = "Other";
private static readonly InputDeviceWatcher.InputDeviceCategory[] KeyboardLookupOrder = { InputDeviceWatcher.InputDeviceCategory.Keyboard };
private static readonly InputDeviceWatcher.InputDeviceCategory[] XboxLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
private static readonly InputDeviceWatcher.InputDeviceCategory[] PlayStationLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.PlayStation,
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
private static readonly InputDeviceWatcher.InputDeviceCategory[] OtherLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>(); public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>();
// 当 FindEntryByControlPath 传空 path 时返回的占位 sprite
public Sprite placeholderSprite; public Sprite placeholderSprite;
// 用于更快查找的缓存
private Dictionary<string, DeviceGlyphTable> _tableCache; private Dictionary<string, DeviceGlyphTable> _tableCache;
private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache; private Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>> _pathLookup;
/// <summary>
/// 启用时构建缓存
/// </summary>
private void OnEnable() private void OnEnable()
{ {
BuildCache(); BuildCache();
} }
/// <summary> #if UNITY_EDITOR
/// 构建表和精灵的查找缓存 private void OnValidate()
/// </summary>
private void BuildCache()
{
if (_tableCache == null)
{
_tableCache = new Dictionary<string, DeviceGlyphTable>(tables.Count);
}
else
{
_tableCache.Clear();
}
if (_spriteCache == null)
{
_spriteCache = new Dictionary<(string, InputDeviceWatcher.InputDeviceCategory), Sprite>();
}
else
{
_spriteCache.Clear();
}
for (int i = 0; i < tables.Count; i++)
{
var table = tables[i];
if (table != null && !string.IsNullOrEmpty(table.deviceName))
{
_tableCache[table.deviceName.ToLowerInvariant()] = table;
}
}
}
/// <summary>
/// 根据设备名称获取设备图标表
/// </summary>
/// <param name="deviceName">设备名称</param>
/// <returns>设备图标表</returns>
public DeviceGlyphTable GetTable(string deviceName)
{
if (string.IsNullOrEmpty(deviceName)) return null;
if (tables == null) return null;
// 确保缓存已构建
if (_tableCache == null || _tableCache.Count == 0)
{ {
BuildCache(); BuildCache();
} }
#endif
// 使用缓存进行 O(1) 查找 public DeviceGlyphTable GetTable(string deviceName)
if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table))
{ {
if (string.IsNullOrWhiteSpace(deviceName) || tables == null)
{
return null;
}
EnsureCache();
_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out DeviceGlyphTable table);
return table; return table;
} }
return null;
}
/// <summary>
/// 获取平台图标
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>平台图标 Sprite</returns>
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{
var table = GetTable(device);
if (table == null) return null;
return table.platformIcons;
}
/// <summary>
/// 根据设备类型获取设备图标表
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>设备图标表</returns>
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
{ {
// 使用常量避免字符串分配
string name;
switch (device) switch (device)
{ {
case InputDeviceWatcher.InputDeviceCategory.Keyboard: case InputDeviceWatcher.InputDeviceCategory.Keyboard:
name = DEVICE_KEYBOARD; return GetTable(DeviceKeyboard);
break;
case InputDeviceWatcher.InputDeviceCategory.Xbox: case InputDeviceWatcher.InputDeviceCategory.Xbox:
name = DEVICE_XBOX; return GetTable(DeviceXbox);
break;
case InputDeviceWatcher.InputDeviceCategory.PlayStation: case InputDeviceWatcher.InputDeviceCategory.PlayStation:
name = DEVICE_PLAYSTATION; return GetTable(DevicePlayStation);
break;
default: default:
name = DEVICE_XBOX; return GetTable(DeviceOther) ?? GetTable(DeviceXbox);
break; }
} }
return GetTable(name); public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{
DeviceGlyphTable table = GetTable(device);
return table != null ? table.platformIcons : null;
}
public bool TryGetSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite)
{
EnsureCache();
string key = NormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(key))
{
sprite = placeholderSprite;
return sprite != null;
}
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
for (int i = 0; i < lookupOrder.Length; i++)
{
InputDeviceWatcher.InputDeviceCategory category = lookupOrder[i];
if (_pathLookup.TryGetValue(category, out Dictionary<string, Sprite> map) && map.TryGetValue(key, out sprite) && sprite != null)
{
return true;
}
}
sprite = placeholderSprite;
return sprite != null;
} }
/// <summary>
/// 根据控制路径和设备类型查找 Sprite
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <returns>找到的 Sprite未找到则返回占位 Sprite</returns>
public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{ {
if (string.IsNullOrEmpty(controlPath)) return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite;
{
return placeholderSprite;
} }
// 首先检查缓存
var cacheKey = (controlPath, device);
if (_spriteCache != null && _spriteCache.TryGetValue(cacheKey, out var cachedSprite))
{
return cachedSprite ?? placeholderSprite;
}
var entry = FindEntryByControlPath(controlPath, device);
var sprite = entry?.Sprite ?? placeholderSprite;
// 缓存结果(包括 null 结果以避免重复查找)
if (_spriteCache != null)
{
_spriteCache[cacheKey] = sprite;
}
return sprite;
}
/// <summary>
/// 根据控制路径和设备类型查找图标条目
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <returns>找到的图标条目,未找到则返回 null</returns>
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device) public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{ {
var t = GetTable(device); if (!TryGetSprite(controlPath, device, out Sprite sprite) || sprite == null)
if (t != null && t.entries != null)
{ {
for (int i = 0; i < t.entries.Count; ++i) return null;
{
var e = t.entries[i];
if (e == null) continue;
if (e.action == null) continue;
var bindings = e.action.bindings;
int bindingCount = bindings.Count;
if (bindingCount <= 0) continue;
for (int j = 0; j < bindingCount; j++)
{
var b = bindings[j];
if (!string.IsNullOrEmpty(b.path) && string.Equals(b.path, controlPath, StringComparison.OrdinalIgnoreCase))
{
return e;
} }
if (!string.IsNullOrEmpty(b.effectivePath) && string.Equals(b.effectivePath, controlPath, StringComparison.OrdinalIgnoreCase)) InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
for (int i = 0; i < lookupOrder.Length; i++)
{ {
return e; DeviceGlyphTable table = GetTable(lookupOrder[i]);
if (table == null || table.entries == null)
{
continue;
} }
for (int j = 0; j < table.entries.Count; j++)
{
GlyphEntry entry = table.entries[j];
if (entry != null && entry.Sprite == sprite)
{
return entry;
} }
} }
} }
return null; return null;
} }
private void EnsureCache()
{
if (_tableCache == null || _pathLookup == null)
{
BuildCache();
}
}
private void BuildCache()
{
_tableCache ??= new Dictionary<string, DeviceGlyphTable>(StringComparer.OrdinalIgnoreCase);
_tableCache.Clear();
_pathLookup ??= new Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>>();
_pathLookup.Clear();
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Keyboard);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Xbox);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.PlayStation);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Other);
for (int i = 0; i < tables.Count; i++)
{
DeviceGlyphTable table = tables[i];
if (table == null || string.IsNullOrWhiteSpace(table.deviceName))
{
continue;
}
_tableCache[table.deviceName.ToLowerInvariant()] = table;
InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName);
Dictionary<string, Sprite> map = _pathLookup[category];
RegisterEntries(table, map);
}
}
private void InitializeLookup(InputDeviceWatcher.InputDeviceCategory category)
{
_pathLookup[category] = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
}
private void RegisterEntries(DeviceGlyphTable table, Dictionary<string, Sprite> map)
{
if (table.entries == null)
{
return;
}
for (int i = 0; i < table.entries.Count; i++)
{
GlyphEntry entry = table.entries[i];
if (entry == null || entry.Sprite == null || entry.action == null)
{
continue;
}
for (int j = 0; j < entry.action.bindings.Count; j++)
{
RegisterBinding(map, entry.action.bindings[j].path, entry.Sprite);
RegisterBinding(map, entry.action.bindings[j].effectivePath, entry.Sprite);
}
}
}
private void RegisterBinding(Dictionary<string, Sprite> map, string controlPath, Sprite sprite)
{
string key = NormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(key) || map.ContainsKey(key))
{
return;
}
map[key] = sprite;
}
private static string NormalizeControlPath(string controlPath)
{
return string.IsNullOrWhiteSpace(controlPath)
? string.Empty
: controlPath.Trim().ToLowerInvariant();
}
private static InputDeviceWatcher.InputDeviceCategory ParseCategory(string deviceName)
{
if (string.IsNullOrWhiteSpace(deviceName))
{
return InputDeviceWatcher.InputDeviceCategory.Other;
}
if (deviceName.Equals(DeviceKeyboard, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.Keyboard;
}
if (deviceName.Equals(DeviceXbox, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.Xbox;
}
if (deviceName.Equals(DevicePlayStation, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.PlayStation;
}
return InputDeviceWatcher.InputDeviceCategory.Other;
}
private static InputDeviceWatcher.InputDeviceCategory[] GetLookupOrder(InputDeviceWatcher.InputDeviceCategory device)
{
switch (device)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardLookupOrder;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxLookupOrder;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationLookupOrder;
default:
return OtherLookupOrder;
}
}
} }

View File

@ -1,72 +1,62 @@
using UnityEngine; using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.UI;
public sealed class InputGlyphImage : MonoBehaviour public sealed class InputGlyphImage : MonoBehaviour
public sealed class InputGlyphImage : InputGlyphBehaviourBase
{ {
[SerializeField] private InputActionReference actionReference; [SerializeField] private InputActionReference actionReference;
[SerializeField] private Image targetImage; [SerializeField] private Image targetImage;
[SerializeField] private bool hideIfMissing = false; [SerializeField] private bool hideIfMissing = false;
[SerializeField] private GameObject hideTargetObject; [SerializeField] private GameObject hideTargetObject;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite; private Sprite _cachedSprite;
/// <summary> protected override void OnEnable()
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
{ {
if (targetImage == null) targetImage = GetComponent<Image>(); if (targetImage == null)
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; {
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard; targetImage = GetComponent<Image>();
UpdatePrompt();
} }
/// <summary> base.OnEnable();
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
} }
/// <summary> protected override void RefreshGlyph()
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{ {
if (_cachedCategory != cat) if (actionReference == null || actionReference.action == null || targetImage == null)
{ {
_cachedCategory = cat; if (targetImage != null && _cachedSprite != null)
UpdatePrompt(); {
} _cachedSprite = null;
targetImage.sprite = null;
} }
/// <summary> ApplyVisibility(false);
/// 更新输入提示图标,并根据配置控制目标对象的显示/隐藏 return;
/// </summary> }
void UpdatePrompt()
{
if (actionReference == null || actionReference.action == null || targetImage == null) return;
// 使用缓存的设备类型,避免重复查询 CurrentCategory bool hasSprite = GlyphService.TryGetUISpriteForActionPath(actionReference, string.Empty, CurrentCategory, out Sprite sprite);
if (GlyphService.TryGetUISpriteForActionPath(actionReference, "", _cachedCategory, out Sprite sprite))
{
if (_cachedSprite != sprite) if (_cachedSprite != sprite)
{ {
_cachedSprite = sprite; _cachedSprite = sprite;
targetImage.sprite = sprite; targetImage.sprite = sprite;
} }
ApplyVisibility(hasSprite && sprite != null);
} }
if (hideTargetObject != null) private void ApplyVisibility(bool hasSprite)
{ {
bool shouldBeActive = sprite != null && !hideIfMissing; if (hideTargetObject == null)
{
return;
}
bool shouldBeActive = !hideIfMissing || hasSprite;
if (hideTargetObject.activeSelf != shouldBeActive) if (hideTargetObject.activeSelf != shouldBeActive)
{ {
hideTargetObject.SetActive(shouldBeActive); hideTargetObject.SetActive(shouldBeActive);
} }
} }
}
} }

View File

@ -1,75 +1,53 @@
using System; using AlicizaX;
using System.Linq;
using AlicizaX;
using UnityEngine;
using TMPro; using TMPro;
using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
[RequireComponent(typeof(TextMeshProUGUI))] [RequireComponent(typeof(TextMeshProUGUI))]
public sealed class InputGlyphText : MonoBehaviour public sealed class InputGlyphText : InputGlyphBehaviourBase
{ {
[SerializeField] private InputActionReference actionReference; [SerializeField] private InputActionReference actionReference;
private TMP_Text textField;
private string _oldText; private TMP_Text _textField;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory; private string _templateText;
private string _cachedFormattedText; private string _cachedFormattedText;
/// <summary> protected override void OnEnable()
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
{ {
if (textField == null) textField = GetComponent<TMP_Text>(); if (_textField == null)
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; {
_oldText = textField.text; _textField = GetComponent<TMP_Text>();
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt();
} }
/// <summary> if (string.IsNullOrEmpty(_templateText) && _textField != null)
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{ {
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; _templateText = _textField.text;
} }
/// <summary> base.OnEnable();
/// 设备类型变更时的回调,更新文本显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
{
_cachedCategory = cat;
UpdatePrompt();
}
} }
/// <summary> protected override void RefreshGlyph()
/// 更新文本中的输入提示标签,使用 TextMeshPro 的 sprite 标签或文本回退
/// </summary>
void UpdatePrompt()
{ {
if (actionReference == null || actionReference.action == null || textField == null) return; if (actionReference == null || actionReference.action == null || _textField == null)
// 使用缓存的设备类型,避免重复查询 CurrentCategory
if (GlyphService.TryGetTMPTagForActionPath(actionReference, "", _cachedCategory, out string tag, out string displayFallback))
{ {
string formattedText = Utility.Text.Format(_oldText, tag);
if (_cachedFormattedText != formattedText)
{
_cachedFormattedText = formattedText;
textField.text = formattedText;
}
return; return;
} }
string fallbackText = Utility.Text.Format(_oldText, displayFallback); string formattedText;
if (_cachedFormattedText != fallbackText) if (GlyphService.TryGetTMPTagForActionPath(actionReference, string.Empty, CurrentCategory, out string tag, out string displayFallback))
{ {
_cachedFormattedText = fallbackText; formattedText = Utility.Text.Format(_templateText, tag);
textField.text = fallbackText; }
else
{
formattedText = Utility.Text.Format(_templateText, displayFallback);
}
if (_cachedFormattedText != formattedText)
{
_cachedFormattedText = formattedText;
_textField.text = formattedText;
} }
} }
} }

View File

@ -1,21 +1,16 @@
using System; using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.UI;
[RequireComponent(typeof(UXButton))] [RequireComponent(typeof(UXButton))]
public sealed class InputGlyphUXButton : MonoBehaviour public sealed class InputGlyphUXButton : InputGlyphBehaviourBase
{ {
[SerializeField] private UXButton button; [SerializeField] private UXButton button;
[SerializeField] private Image targetImage; [SerializeField] private Image targetImage;
private InputActionReference _actionReference;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite; private Sprite _cachedSprite;
#if UNITY_EDITOR #if UNITY_EDITOR
/// <summary>
/// 编辑器验证,自动获取 UXButton 组件
/// </summary>
private void OnValidate() private void OnValidate()
{ {
if (button == null) if (button == null)
@ -25,54 +20,45 @@ public sealed class InputGlyphUXButton : MonoBehaviour
} }
#endif #endif
/// <summary> protected override void OnEnable()
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
{ {
if (button == null) button = GetComponent<UXButton>(); if (button == null)
if (targetImage == null) targetImage = GetComponent<Image>(); {
_actionReference = button.HotKeyRefrence; button = GetComponent<UXButton>();
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt();
} }
/// <summary> if (targetImage == null)
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{ {
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; targetImage = GetComponent<Image>();
} }
/// <summary> base.OnEnable();
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
{
_cachedCategory = cat;
UpdatePrompt();
}
} }
/// <summary> protected override void RefreshGlyph()
/// 更新按钮的输入提示图标
/// </summary>
void UpdatePrompt()
{ {
if (_actionReference == null || _actionReference.action == null || targetImage == null) return; InputActionReference actionReference = button != null ? button.HotKeyRefrence : null;
if (actionReference == null || actionReference.action == null || targetImage == null)
{
if (targetImage != null && _cachedSprite != null)
{
_cachedSprite = null;
targetImage.sprite = null;
}
// 使用缓存的设备类型,避免重复查询 CurrentCategory return;
if (GlyphService.TryGetUISpriteForActionPath(_actionReference, "", _cachedCategory, out Sprite sprite)) }
bool hasSprite = GlyphService.TryGetUISpriteForActionPath(actionReference, string.Empty, CurrentCategory, out Sprite sprite);
if (!hasSprite)
{ {
sprite = null;
}
if (_cachedSprite != sprite) if (_cachedSprite != sprite)
{ {
_cachedSprite = sprite; _cachedSprite = sprite;
targetImage.sprite = sprite; targetImage.sprite = sprite;
} }
} }
}
} }

View File

@ -27,7 +27,8 @@ public class TestRebindScript : MonoBehaviour
private void Start() private void Start()
{ {
if (btn != null) btn.onClick.AddListener(OnBtnClicked); if (btn != null) btn.onClick.AddListener(OnBtnClicked);
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; InputDeviceWatcher.OnDeviceContextChanged += OnDeviceContextChanged;
InputBindingManager.BindingsChanged += OnBindingsChanged;
UpdateBindingText(); UpdateBindingText();
if (InputBindingManager.Instance != null) if (InputBindingManager.Instance != null)
@ -46,7 +47,8 @@ public class TestRebindScript : MonoBehaviour
private void OnDisable() private void OnDisable()
{ {
if (btn != null) btn.onClick.RemoveListener(OnBtnClicked); if (btn != null) btn.onClick.RemoveListener(OnBtnClicked);
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; InputDeviceWatcher.OnDeviceContextChanged -= OnDeviceContextChanged;
InputBindingManager.BindingsChanged -= OnBindingsChanged;
if (InputBindingManager.Instance != null) if (InputBindingManager.Instance != null)
{ {
@ -115,7 +117,12 @@ public class TestRebindScript : MonoBehaviour
/// <summary> /// <summary>
/// 设备变更的回调 /// 设备变更的回调
/// </summary> /// </summary>
private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _) private void OnDeviceContextChanged(InputDeviceWatcher.DeviceContext _)
{
UpdateBindingText();
}
private void OnBindingsChanged()
{ {
UpdateBindingText(); UpdateBindingText();
} }
@ -207,6 +214,7 @@ public class TestRebindScript : MonoBehaviour
private void UpdateBindingText() private void UpdateBindingText()
{ {
var action = GetAction(); var action = GetAction();
var deviceCat = InputDeviceWatcher.CurrentCategory;
if (action == null) if (action == null)
{ {
bindKeyText.text = "<no action>"; bindKeyText.text = "<no action>";
@ -215,15 +223,12 @@ public class TestRebindScript : MonoBehaviour
} }
bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName); bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName, deviceCat);
try try
{ {
var deviceCat = InputDeviceWatcher.CurrentCategory; if (GlyphService.TryGetUISpriteForActionPath(action, compositePartName, deviceCat, out Sprite sprite))
InputActionReference refr=default;
// string controlPath = GlyphService.GetBindingControlPath(action, compositePartName, deviceCat);
if ( GlyphService.TryGetUISpriteForActionPath(action,compositePartName, deviceCat, out Sprite sprite))
{ {
if (targetImage != null) targetImage.sprite = sprite; if (targetImage != null) targetImage.sprite = sprite;
} }