From f4ec668bfd38b30ec412de3eb4d7234a749e72c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Tue, 17 Mar 2026 20:02:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=9A=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E8=BE=93=E5=85=A5=E7=B3=BB=E7=BB=9F=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E5=AE=BD=E5=81=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomeModule/InputGlyph/GlyphService.cs | 371 +++-- .../InputGlyph/InputBindingManager.cs | 1364 ++++++++--------- .../InputGlyph/InputDeviceWatcher.cs | 478 ++++-- .../InputGlyph/InputGlyphBehaviourBase.cs | 33 + .../InputGlyphBehaviourBase.cs.meta | 11 + .../InputGlyph/InputGlyphDatabase.cs | 355 +++-- .../InputGlyph/InputGlyphImage.cs | 92 +- .../InputGlyph/InputGlyphText.cs | 84 +- .../InputGlyph/InputGlyphUXButton.cs | 82 +- .../InputGlyph/TestRebindScript.cs | 21 +- 10 files changed, 1630 insertions(+), 1261 deletions(-) create mode 100644 Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs create mode 100644 Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs.meta diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs index 3d0afb7..d542fbb 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs @@ -1,18 +1,17 @@ -using System; +using System; using UnityEngine; using UnityEngine.InputSystem; public static class GlyphService { - // 缓存的设备提示数组,避免内存分配 - private static readonly string[] KeyboardHints = { "Keyboard", "Mouse" }; - private static readonly string[] XboxHints = { "XInput", "Xbox", "Gamepad" }; - private static readonly string[] PlayStationHints = { "DualShock", "DualSense", "PlayStation", "Gamepad" }; + private static readonly string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" }; + private static readonly string[] XboxGroupHints = { "xbox", "xinput", "gamepad", "controller" }; + private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" }; + private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" }; private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; - /// - /// 获取输入图标数据库实例 - /// + private static InputGlyphDatabase _database; + public static InputGlyphDatabase Database { get @@ -26,179 +25,285 @@ public static class GlyphService } } - private static InputGlyphDatabase _database; - - - /// - /// 获取输入操作的绑定控制路径 - /// - /// 输入操作 - /// 复合部分名称(可选) - /// 设备类型覆盖(可选) - /// 绑定控制路径 - public static string GetBindingControlPath(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) + public static string GetBindingControlPath( + InputAction action, + string compositePartName = null, + InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) { - if (action == null) return string.Empty; - var binding = GetBindingControl(action, compositePartName, deviceOverride); - return binding.hasOverrides ? binding.effectivePath : binding.path; + return TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding) + ? GetEffectivePath(binding) + : string.Empty; } - /// - /// 尝试获取输入操作的 TextMeshPro 标签 - /// - /// 输入操作引用 - /// 复合部分名称 - /// 设备类型 - /// 输出的 TMP 标签 - /// 显示回退文本 - /// 数据库实例(可选) - /// 是否成功获取 - public static bool TryGetTMPTagForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) + public static string GetBindingControlPath( + InputActionReference actionReference, + string compositePartName = null, + InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) { - string path = GetBindingControlPath(reference, compositePartName, device); - return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db); + return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride); } - /// - /// 尝试获取输入操作的 UI Sprite - /// - /// 输入操作引用 - /// 复合部分名称 - /// 设备类型 - /// 输出的 Sprite - /// 数据库实例(可选) - /// 是否成功获取 - public static bool TryGetUISpriteForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) + public static bool TryGetTMPTagForActionPath( + InputAction action, + string compositePartName, + InputDeviceWatcher.InputDeviceCategory device, + out string tag, + out string displayFallback, + InputGlyphDatabase db = null) { - string path = GetBindingControlPath(reference, compositePartName, device); - return TryGetUISpriteForActionPath(path, device, out sprite, db); + string controlPath = GetBindingControlPath(action, compositePartName, device); + return TryGetTMPTagForActionPath(controlPath, device, out tag, out displayFallback, db); } - /// - /// 根据控制路径尝试获取 TextMeshPro 标签 - /// - /// 控制路径 - /// 设备类型 - /// 输出的 TMP 标签 - /// 显示回退文本 - /// 数据库实例(可选) - /// 是否成功获取 - public static bool TryGetTMPTagForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) + public static bool TryGetTMPTagForActionPath( + InputActionReference actionReference, + string compositePartName, + InputDeviceWatcher.InputDeviceCategory device, + out string tag, + out string displayFallback, + InputGlyphDatabase db = null) { - tag = null; - displayFallback = null; - db = db ?? Database; + return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db); + } + 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); + tag = null; - var sprite = db.FindSprite(controlPath, device) ?? db.FindSprite(controlPath, InputDeviceWatcher.InputDeviceCategory.Keyboard); + if (!TryGetUISpriteForActionPath(controlPath, device, out Sprite sprite, db)) + { + return false; + } - var spriteName = sprite == null ? string.Empty : sprite.name; - tag = $""; + tag = $""; return true; } - /// - /// 根据控制路径尝试获取 UI Sprite - /// - /// 控制路径 - /// 设备类型 - /// 输出的 Sprite - /// 数据库实例(可选) - /// 是否成功获取 - public static bool TryGetUISpriteForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) + 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; + db ??= Database; + return db != null && db.TryGetSprite(controlPath, device, out sprite); } - - /// - /// 获取输入操作的绑定控制 - /// - static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) + public static string GetDisplayNameFromInputAction( + 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 (!TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding)) { - if (!string.IsNullOrEmpty(compositePartName)) + return string.Empty; + } + + string display = binding.ToDisplayString(); + return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display; + } + + public static string GetDisplayNameFromControlPath(string controlPath) + { + if (string.IsNullOrWhiteSpace(controlPath)) + { + return string.Empty; + } + + string[] parts = controlPath.Split('/'); + string last = parts[parts.Length - 1].Trim(TrimChars); + 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) { - if (!b.isPartOfComposite) continue; - if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; + 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; + 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 default; + return bestScore > int.MinValue; } - // 辅助方法,避免 LINQ Any() 的内存分配 - /// - /// 检查路径是否包含任何提示字符串 - /// - static bool ContainsAnyHint(string path, string[] hints) + private static int ScoreBinding(InputBinding binding, InputDeviceWatcher.InputDeviceCategory category) { - for (int i = 0; i < hints.Length; i++) + int score = 0; + string path = GetEffectivePath(binding); + + if (MatchesBindingGroups(binding.groups, category)) { - if (path.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0) - return true; + 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; } - /// - /// 根据设备类型获取设备提示字符串数组 - /// - static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat) + private static bool MatchesControlPath(string path, InputDeviceWatcher.InputDeviceCategory category) { - switch (cat) + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + switch (category) { case InputDeviceWatcher.InputDeviceCategory.Keyboard: - return KeyboardHints; + return StartsWithDevice(path, "") || StartsWithDevice(path, ""); case InputDeviceWatcher.InputDeviceCategory.Xbox: - return XboxHints; + return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, XboxGroupHints); case InputDeviceWatcher.InputDeviceCategory.PlayStation: - return PlayStationHints; + return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, PlayStationGroupHints); default: - return XboxHints; + return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, OtherGamepadGroupHints); } } - - /// - /// 从输入操作获取显示名称 - /// - /// 输入操作 - /// 复合部分名称(可选) - /// 设备类型覆盖 - /// 显示名称 - public static string GetDisplayNameFromInputAction(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory deviceOverride = InputDeviceWatcher.InputDeviceCategory.Keyboard) + private static bool StartsWithDevice(string path, string deviceTag) { - if (action == null) return string.Empty; - var binding = GetBindingControl(action, compositePartName, deviceOverride); - return binding.ToDisplayString(); + return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase); } - /// - /// 从控制路径获取显示名称 - /// - /// 控制路径 - /// 显示名称 - public static string GetDisplayNameFromControlPath(string controlPath) + private static string[] GetGroupHints(InputDeviceWatcher.InputDeviceCategory category) { - if (string.IsNullOrEmpty(controlPath)) return string.Empty; - var parts = controlPath.Split('/'); - var last = parts[parts.Length - 1].Trim(TrimChars); - return last; + 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; } } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs index 87c63bd..e02a02f 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs @@ -1,5 +1,3 @@ -// InputBindingManager.cs - using System; using System.IO; using System.Linq; @@ -8,764 +6,764 @@ using System.Threading.Tasks; using UnityEngine; using UnityEngine.InputSystem; using AlicizaX; - using Cysharp.Threading.Tasks; +public class InputBindingManager : MonoSingleton +{ + public const string NULL_BINDING = "__NULL__"; + private const string KEYBOARD_DEVICE = ""; + private const string MOUSE_DELTA = "/delta"; + private const string MOUSE_SCROLL = "/scroll"; + private const string MOUSE_SCROLL_X = "/scroll/x"; + private const string MOUSE_SCROLL_Y = "/scroll/y"; + private const string KEYBOARD_ESCAPE = "/escape"; - public class InputBindingManager : MonoSingleton + [Tooltip("InputActionAsset to manage")] + public InputActionAsset actions; + + public string fileName = "input_bindings.json"; + public bool debugMode = false; + + public Dictionary actionMap = new Dictionary(); + public HashSet preparedRebinds = new HashSet(); + + internal InputActionRebindingExtensions.RebindingOperation rebindOperation; + private bool isApplyPending = false; + private string defaultBindingsJson = string.Empty; + private string cachedSavePath; + private Dictionary actionLookup = new Dictionary(); + + // 用于替代 Rx.NET Subjects 的事件 + private event Action _onInputsInit; + + public event Action OnInputsInit { - public const string NULL_BINDING = "__NULL__"; - private const string KEYBOARD_DEVICE = ""; - private const string MOUSE_DELTA = "/delta"; - private const string MOUSE_SCROLL = "/scroll"; - private const string MOUSE_SCROLL_X = "/scroll/x"; - private const string MOUSE_SCROLL_Y = "/scroll/y"; - private const string KEYBOARD_ESCAPE = "/escape"; - - [Tooltip("InputActionAsset to manage")] - public InputActionAsset actions; - - public string fileName = "input_bindings.json"; - public bool debugMode = false; - - public Dictionary actionMap = new Dictionary(); - public HashSet preparedRebinds = new HashSet(); - - internal InputActionRebindingExtensions.RebindingOperation rebindOperation; - private bool isApplyPending = false; - private string defaultBindingsJson = string.Empty; - private string cachedSavePath; - private Dictionary actionLookup = new Dictionary(); - - // 用于替代 Rx.NET Subjects 的事件 - private event Action _onInputsInit; - public event Action OnInputsInit + add { - add + _onInputsInit += value; + // 重放行为:如果已经初始化,立即调用 + if (isInputsInitialized) { - _onInputsInit += value; - // 重放行为:如果已经初始化,立即调用 - if (isInputsInitialized) - { - value?.Invoke(); - } - } - remove - { - _onInputsInit -= value; + value?.Invoke(); } } + remove { _onInputsInit -= value; } + } - public event Action> OnApply; - public event Action OnRebindPrepare; - public event Action OnRebindStart; - public event Action OnRebindEnd; - public event Action OnRebindConflict; + public event Action> OnApply; + public event Action OnRebindPrepare; + public event Action OnRebindStart; + public event Action OnRebindEnd; + public event Action OnRebindConflict; + public static event Action BindingsChanged; - private bool isInputsInitialized = false; + private bool isInputsInitialized = false; - public string SavePath + public string SavePath + { + get { - get - { - if (!string.IsNullOrEmpty(cachedSavePath)) - return cachedSavePath; + if (!string.IsNullOrEmpty(cachedSavePath)) + return cachedSavePath; #if UNITY_EDITOR - string folder = Application.dataPath; + string folder = Application.dataPath; #else string folder = Application.persistentDataPath; #endif - cachedSavePath = Path.Combine(folder, fileName); - return cachedSavePath; - } + cachedSavePath = Path.Combine(folder, fileName); + return cachedSavePath; + } + } + + private void EnsureSaveDirectoryExists() + { + var directory = Path.GetDirectoryName(SavePath); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + } + + protected override void Awake() + { + if (_instance != null && _instance != this) + { + Destroy(gameObject); + return; } - private void EnsureSaveDirectoryExists() + _instance = this; + + if (actions == null) { - var directory = Path.GetDirectoryName(SavePath); - if (!Directory.Exists(directory)) - Directory.CreateDirectory(directory); + Debug.LogError("InputBindingManager: InputActionAsset not assigned."); + return; } - protected override void Awake() + BuildActionMap(); + + try { - if (_instance != null && _instance != this) - { - Destroy(gameObject); - return; - } - - _instance = this; - - if (actions == null) - { - Debug.LogError("InputBindingManager: InputActionAsset not assigned."); - return; - } - - BuildActionMap(); + defaultBindingsJson = actions.SaveBindingOverridesAsJson(); + } + catch (Exception ex) + { + Debug.LogWarning($"[InputBindingManager] Failed to save default bindings: {ex.Message}"); + defaultBindingsJson = string.Empty; + } + if (File.Exists(SavePath)) + { try { - defaultBindingsJson = actions.SaveBindingOverridesAsJson(); + var json = File.ReadAllText(SavePath); + if (!string.IsNullOrEmpty(json)) + { + actions.LoadBindingOverridesFromJson(json); + RefreshBindingPathsFromActions(); + BindingsChanged?.Invoke(); + if (debugMode) + { + Debug.Log($"Loaded overrides from {SavePath}"); + } + } } catch (Exception ex) { - Debug.LogWarning($"[InputBindingManager] Failed to save default bindings: {ex.Message}"); - defaultBindingsJson = string.Empty; + Debug.LogError("Failed to load overrides: " + ex); } - - if (File.Exists(SavePath)) - { - try - { - var json = File.ReadAllText(SavePath); - if (!string.IsNullOrEmpty(json)) - { - actions.LoadBindingOverridesFromJson(json); - RefreshBindingPathsFromActions(); - if (debugMode) - { - Debug.Log($"Loaded overrides from {SavePath}"); - } - } - } - catch (Exception ex) - { - Debug.LogError("Failed to load overrides: " + ex); - } - } - - isInputsInitialized = true; - _onInputsInit?.Invoke(); - actions.Enable(); } - protected override void OnDestroy() + isInputsInitialized = true; + _onInputsInit?.Invoke(); + actions.Enable(); + } + + protected override void OnDestroy() + { + if (_instance == this) { - if (_instance == this) - { - _instance = null; - } - - rebindOperation?.Dispose(); - rebindOperation = null; - - // 清除所有事件处理器 - _onInputsInit = null; - OnApply = null; - OnRebindPrepare = null; - OnRebindStart = null; - OnRebindEnd = null; - OnRebindConflict = null; + _instance = null; } - private void BuildActionMap() + rebindOperation?.Dispose(); + rebindOperation = null; + + // 清除所有事件处理器 + _onInputsInit = null; + OnApply = null; + OnRebindPrepare = null; + OnRebindStart = null; + OnRebindEnd = null; + OnRebindConflict = null; + BindingsChanged = null; + } + + private void BuildActionMap() + { + // 预分配已知容量以避免调整大小 + int mapCount = actions.actionMaps.Count; + actionMap.Clear(); + actionLookup.Clear(); + + // 估算总操作数以便更好地分配内存 + int estimatedActionCount = 0; + foreach (var map in actions.actionMaps) { - // 预分配已知容量以避免调整大小 - int mapCount = actions.actionMaps.Count; - actionMap.Clear(); - actionLookup.Clear(); - - // 估算总操作数以便更好地分配内存 - int estimatedActionCount = 0; - foreach (var map in actions.actionMaps) - { - estimatedActionCount += map.actions.Count; - } - - // 确保容量以避免重新哈希 - if (actionMap.Count == 0) - { - actionMap = new Dictionary(mapCount); - actionLookup = new Dictionary(estimatedActionCount); - } - - foreach (var map in actions.actionMaps) - { - var actionMapObj = new ActionMap(map); - actionMap.Add(map.name, actionMapObj); - - // 构建查找字典以实现 O(1) 操作访问 - foreach (var actionPair in actionMapObj.actions) - { - actionLookup[actionPair.Key] = (actionMapObj, actionPair.Value); - } - } + estimatedActionCount += map.actions.Count; } - private void RefreshBindingPathsFromActions() + // 确保容量以避免重新哈希 + if (actionMap.Count == 0) { - foreach (var mapPair in actionMap.Values) - { - foreach (var actionPair in mapPair.actions.Values) - { - var a = actionPair; - foreach (var bpair in a.bindings) - { - bpair.Value.bindingPath.EffectivePath = a.action.bindings[bpair.Key].effectivePath; - } - } - } + actionMap = new Dictionary(mapCount); + actionLookup = new Dictionary(estimatedActionCount); } - public sealed class ActionMap + foreach (var map in actions.actionMaps) { - public string name; - public Dictionary actions; + var actionMapObj = new ActionMap(map); + actionMap.Add(map.name, actionMapObj); - public ActionMap(InputActionMap map) + // 构建查找字典以实现 O(1) 操作访问 + foreach (var actionPair in actionMapObj.actions) { - name = map.name; - int actionCount = map.actions.Count; - actions = new Dictionary(actionCount); - foreach (var action in map.actions) - { - actions.Add(action.name, new Action(action)); - } - } - - public sealed class Action - { - public InputAction action; - public Dictionary bindings; - - public Action(InputAction action) - { - this.action = action; - int count = action.bindings.Count; - bindings = new Dictionary(count); - - for (int i = 0; i < count; i++) - { - if (action.bindings[i].isComposite) - { - int first = i + 1; - int last = first; - while (last < count && action.bindings[last].isPartOfComposite) last++; - for (int p = first; p < last; p++) - AddBinding(action.bindings[p], p); - i = last - 1; - } - else - { - AddBinding(action.bindings[i], i); - } - } - - void AddBinding(InputBinding binding, int bindingIndex) - { - bindings.Add(bindingIndex, new Binding( - binding.name, - action.name, - binding.name, - bindingIndex, - binding.groups?.Split(InputBinding.Separator) ?? Array.Empty(), - new BindingPath(binding.path, binding.overridePath), - binding - )); - } - } - - public readonly struct Binding - { - public readonly string name; - public readonly string parentAction; - public readonly string compositePart; - public readonly int bindingIndex; - public readonly string[] group; - public readonly BindingPath bindingPath; - public readonly InputBinding inputBinding; - - public Binding(string name, string parentAction, string compositePart, int bindingIndex, - string[] group, BindingPath bindingPath, InputBinding inputBinding) - { - this.name = name; - this.parentAction = parentAction; - this.compositePart = compositePart; - this.bindingIndex = bindingIndex; - this.group = group; - this.bindingPath = bindingPath; - this.inputBinding = inputBinding; - } - } + actionLookup[actionPair.Key] = (actionMapObj, actionPair.Value); } } + } - public sealed class BindingPath + private void RefreshBindingPathsFromActions() + { + foreach (var mapPair in actionMap.Values) { - public string bindingPath; - public string overridePath; - private event Action onEffectivePathChanged; - - public BindingPath(string bindingPath, string overridePath) + foreach (var actionPair in mapPair.actions.Values) { - this.bindingPath = bindingPath; - this.overridePath = overridePath; - } - - public string EffectivePath - { - get => !string.IsNullOrEmpty(overridePath) ? overridePath : bindingPath; - set + var a = actionPair; + foreach (var bpair in a.bindings) { - overridePath = (value == bindingPath) ? string.Empty : value; - onEffectivePathChanged?.Invoke(EffectivePath); + bpair.Value.bindingPath.EffectivePath = a.action.bindings[bpair.Key].effectivePath; } } + } + } - public void SubscribeToEffectivePathChanged(Action callback) - { - onEffectivePathChanged += callback; - } + public sealed class ActionMap + { + public string name; + public Dictionary actions; - public void UnsubscribeFromEffectivePathChanged(Action callback) + public ActionMap(InputActionMap map) + { + name = map.name; + int actionCount = map.actions.Count; + actions = new Dictionary(actionCount); + foreach (var action in map.actions) { - onEffectivePathChanged -= callback; - } - - public void Dispose() - { - onEffectivePathChanged = null; + actions.Add(action.name, new Action(action)); } } - public sealed class RebindContext + public sealed class Action { public InputAction action; - public int bindingIndex; - public string overridePath; - private string cachedToString; + public Dictionary bindings; - public RebindContext(InputAction action, int bindingIndex, string overridePath) + public Action(InputAction action) { this.action = action; - this.bindingIndex = bindingIndex; - this.overridePath = overridePath; - } + int count = action.bindings.Count; + bindings = new Dictionary(count); - public override bool Equals(object obj) - { - if (obj is not RebindContext other) return false; - if (action == null || other.action == null) return false; - return action.name == other.action.name && bindingIndex == other.bindingIndex; - } - - public override int GetHashCode() => (action?.name ?? string.Empty, bindingIndex).GetHashCode(); - - public override string ToString() - { - if (cachedToString == null && action != null) + for (int i = 0; i < count; i++) { - cachedToString = $"{action.name}:{bindingIndex}"; - } - - return cachedToString ?? ""; - } - } - - /* ---------------- Public API ---------------- */ - - /// - /// 根据操作名称获取输入操作 - /// - /// 操作名称 - /// 输入操作,未找到则返回 null - public static InputAction Action(string actionName) - { - var instance = Instance; - if (instance == null) return null; - - if (instance.actionLookup.TryGetValue(actionName, out var result)) - { - return result.action.action; - } - - Debug.LogError($"[InputBindingManager] Could not find action '{actionName}'"); - return null; - } - - /// - /// 开始重新绑定指定的输入操作 - /// - /// 操作名称 - /// 复合部分名称(可选) - public static void StartRebind(string actionName, string compositePartName = null) - { - var action = Action(actionName); - if (action == null) return; - - // 自动决定 bindingIndex 和 deviceMatch - int bindingIndex = Instance.FindBestBindingIndexForKeyboard(action, compositePartName); - if (bindingIndex < 0) - { - Debug.LogError($"[InputBindingManager] No suitable binding found for action '{actionName}' (part={compositePartName ?? ""})"); - return; - } - - Instance.actions.Disable(); - Instance.PerformInteractiveRebinding(action, bindingIndex, KEYBOARD_DEVICE, true); - Instance.OnRebindStart?.Invoke(); - if (Instance.debugMode) - { - Debug.Log("[InputBindingManager] Rebind started"); - } - } - - /// - /// 取消当前的重新绑定操作 - /// - public static void CancelRebind() => Instance.rebindOperation?.Cancel(); - - /// - /// 确认并应用准备好的重新绑定 - /// - /// 是否清除冲突 - /// 是否成功应用 - public static async UniTask ConfirmApply(bool clearConflicts = true) - { - if (!Instance.isApplyPending) return false; - - try - { - // 在清除之前创建准备好的重绑定的副本 - var appliedContexts = new HashSet(Instance.preparedRebinds); - - foreach (var ctx in Instance.preparedRebinds) - { - if (!string.IsNullOrEmpty(ctx.overridePath)) + if (action.bindings[i].isComposite) { - if (ctx.overridePath == NULL_BINDING) - { - ctx.action.RemoveBindingOverride(ctx.bindingIndex); - } - else - { - ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath); - } - } - - var bp = GetBindingPath(ctx.action.name, ctx.bindingIndex); - if (bp != null) - { - bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath; - } - } - - Instance.preparedRebinds.Clear(); - await Instance.WriteOverridesToDiskAsync(); - Instance.OnApply?.Invoke(true, appliedContexts); - Instance.isApplyPending = false; - if (Instance.debugMode) - { - Debug.Log("[InputBindingManager] Apply confirmed and saved."); - } - - return true; - } - catch (Exception ex) - { - Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex); - Instance.OnApply?.Invoke(false, new HashSet()); - return false; - } - } - - /// - /// 丢弃准备好的重新绑定 - /// - public static void DiscardPrepared() - { - if (!Instance.isApplyPending) return; - - // 在清除之前创建准备好的重绑定的副本(用于事件通知) - var discardedContexts = new HashSet(Instance.preparedRebinds); - - Instance.preparedRebinds.Clear(); - Instance.isApplyPending = false; - Instance.OnApply?.Invoke(false, discardedContexts); - if (Instance.debugMode) - { - Debug.Log("[InputBindingManager] Prepared rebinds discarded."); - } - } - - private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true) - { - var op = action.PerformInteractiveRebinding(bindingIndex); - - if (!string.IsNullOrEmpty(deviceMatchPath)) - { - op = op.WithControlsHavingToMatchPath(deviceMatchPath); - } - - if (excludeMouseMovementAndScroll) - { - op = op.WithControlsExcluding(MOUSE_DELTA) - .WithControlsExcluding(MOUSE_SCROLL) - .WithControlsExcluding(MOUSE_SCROLL_X) - .WithControlsExcluding(MOUSE_SCROLL_Y); - } - - rebindOperation = op - .OnApplyBinding((o, path) => - { - if (AnyPreparedRebind(path, action, bindingIndex, out var existing)) - { - PrepareRebind(new RebindContext(action, bindingIndex, path)); - PrepareRebind(new RebindContext(existing.action, existing.bindingIndex, NULL_BINDING)); - OnRebindConflict?.Invoke(new RebindContext(action, bindingIndex, path), existing); - } - else if (AnyBindingPath(path, action, bindingIndex, out var dup)) - { - PrepareRebind(new RebindContext(action, bindingIndex, path)); - PrepareRebind(new RebindContext(dup.action, dup.bindingIndex, NULL_BINDING)); - OnRebindConflict?.Invoke(new RebindContext(action, bindingIndex, path), new RebindContext(dup.action, dup.bindingIndex, dup.action.bindings[dup.bindingIndex].path)); + int first = i + 1; + int last = first; + while (last < count && action.bindings[last].isPartOfComposite) last++; + for (int p = first; p < last; p++) + AddBinding(action.bindings[p], p); + i = last - 1; } else { - PrepareRebind(new RebindContext(action, bindingIndex, path)); - } - }) - .OnComplete(opc => - { - if (debugMode) - { - Debug.Log("[InputBindingManager] Rebind completed"); - } - - actions.Enable(); - OnRebindEnd?.Invoke(true, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)); - CleanRebindOperation(); - }) - .OnCancel(opc => - { - if (debugMode) - { - Debug.Log("[InputBindingManager] Rebind cancelled"); - } - - actions.Enable(); - OnRebindEnd?.Invoke(false, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)); - CleanRebindOperation(); - }) - .WithCancelingThrough(KEYBOARD_ESCAPE) - .Start(); - } - - private void CleanRebindOperation() - { - rebindOperation?.Dispose(); - rebindOperation = null; - } - - private bool AnyPreparedRebind(string bindingPath, InputAction currentAction, int currentIndex, out RebindContext duplicate) - { - foreach (var ctx in preparedRebinds) - { - if (ctx.overridePath == bindingPath && (ctx.action != currentAction || (ctx.action == currentAction && ctx.bindingIndex != currentIndex))) - { - duplicate = ctx; - return true; - } - } - - duplicate = null; - return false; - } - - private bool AnyBindingPath(string bindingPath, InputAction currentAction, int currentIndex, out (InputAction action, int bindingIndex) duplicate) - { - foreach (var map in actionMap.Values) - { - foreach (var actionPair in map.actions.Values) - { - bool isSameAction = actionPair.action == currentAction; - - foreach (var bindingPair in actionPair.bindings) - { - // Skip if it's the same action and same binding index - if (isSameAction && bindingPair.Key == currentIndex) - continue; - - if (bindingPair.Value.bindingPath.EffectivePath == bindingPath) - { - duplicate = (actionPair.action, bindingPair.Key); - return true; - } - } - } - } - - duplicate = default; - return false; - } - - private void PrepareRebind(RebindContext context) - { - // 如果存在相同操作/绑定的现有重绑定,则移除 - preparedRebinds.Remove(context); - - if (string.IsNullOrEmpty(context.overridePath)) - { - var bp = GetBindingPath(context.action.name, context.bindingIndex); - if (bp != null) context.overridePath = bp.bindingPath; - } - - var bindingPath = GetBindingPath(context.action.name, context.bindingIndex); - if (bindingPath == null) return; - - if (bindingPath.EffectivePath != context.overridePath) - { - preparedRebinds.Add(context); - isApplyPending = true; - OnRebindPrepare?.Invoke(context); - if (debugMode) - { - Debug.Log($"Prepared rebind: {context} -> {context.overridePath}"); - } - } - } - - private async UniTask WriteOverridesToDiskAsync() - { - try - { - var json = actions.SaveBindingOverridesAsJson(); - EnsureSaveDirectoryExists(); - using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json); - if (debugMode) - { - Debug.Log($"Overrides saved to {SavePath}"); - } - } - catch (Exception ex) - { - Debug.LogError("Failed to save overrides: " + ex); - throw; - } - } - - /// - /// 重置所有绑定到默认值 - /// - public async UniTask ResetToDefaultAsync() - { - try - { - if (!string.IsNullOrEmpty(defaultBindingsJson)) - { - actions.LoadBindingOverridesFromJson(defaultBindingsJson); - } - else - { - foreach (var map in actionMap.Values) - { - foreach (var a in map.actions.Values) - { - for (int b = 0; b < a.action.bindings.Count; b++) - { - a.action.RemoveBindingOverride(b); - } - } + AddBinding(action.bindings[i], i); } } - RefreshBindingPathsFromActions(); - await WriteOverridesToDiskAsync(); - if (debugMode) + void AddBinding(InputBinding binding, int bindingIndex) { - Debug.Log("Reset to default and saved."); + bindings.Add(bindingIndex, new Binding( + binding.name, + action.name, + binding.name, + bindingIndex, + binding.groups?.Split(InputBinding.Separator) ?? Array.Empty(), + new BindingPath(binding.path, binding.overridePath), + binding + )); } } - catch (Exception ex) + + public readonly struct Binding { - Debug.LogError("Failed to reset defaults: " + ex); + public readonly string name; + public readonly string parentAction; + public readonly string compositePart; + public readonly int bindingIndex; + public readonly string[] group; + public readonly BindingPath bindingPath; + public readonly InputBinding inputBinding; + + public Binding(string name, string parentAction, string compositePart, int bindingIndex, + string[] group, BindingPath bindingPath, InputBinding inputBinding) + { + this.name = name; + this.parentAction = parentAction; + this.compositePart = compositePart; + this.bindingIndex = bindingIndex; + this.group = group; + this.bindingPath = bindingPath; + this.inputBinding = inputBinding; + } } } - - /// - /// 获取指定操作的绑定路径 - /// - /// 操作名称 - /// 绑定索引 - /// 绑定路径,未找到则返回 null - public static BindingPath GetBindingPath(string actionName, int bindingIndex = 0) - { - var instance = Instance; - if (instance == null) return null; - - if (instance.actionLookup.TryGetValue(actionName, out var result)) - { - if (result.action.bindings.TryGetValue(bindingIndex, out var binding)) - { - return binding.bindingPath; - } - } - - return null; - } - - - // 为键盘选择最佳绑定索引;如果 compositePartName != null 则查找部分 - /// - /// 为键盘查找最佳的绑定索引 - /// - /// 输入操作 - /// 复合部分名称(可选) - /// 绑定索引,未找到则返回 -1 - public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null) - { - if (action == null) return -1; - - int fallbackPart = -1; - int fallbackNonComposite = -1; - bool searchingForCompositePart = !string.IsNullOrEmpty(compositePartName); - - for (int i = 0; i < action.bindings.Count; i++) - { - var b = action.bindings[i]; - - // 如果搜索特定的复合部分,跳过不匹配的绑定 - if (searchingForCompositePart) - { - if (!b.isPartOfComposite) continue; - if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; - } - - // 检查此绑定是否用于键盘 - bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) || - (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)); - - if (b.isPartOfComposite) - { - if (fallbackPart == -1) fallbackPart = i; - if (isKeyboardBinding) return i; - } - else - { - if (fallbackNonComposite == -1) fallbackNonComposite = i; - if (isKeyboardBinding) return i; - } - } - - return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart; - } - - public static InputBindingManager Instance - { - get - { - if (_instance == null) - { - _instance = FindObjectOfType(); - } - - return _instance; - } - } - - private static InputBindingManager _instance; } + public sealed class BindingPath + { + public string bindingPath; + public string overridePath; + private event Action onEffectivePathChanged; + + public BindingPath(string bindingPath, string overridePath) + { + this.bindingPath = bindingPath; + this.overridePath = overridePath; + } + + public string EffectivePath + { + get => !string.IsNullOrEmpty(overridePath) ? overridePath : bindingPath; + set + { + overridePath = (value == bindingPath) ? string.Empty : value; + onEffectivePathChanged?.Invoke(EffectivePath); + } + } + + public void SubscribeToEffectivePathChanged(Action callback) + { + onEffectivePathChanged += callback; + } + + public void UnsubscribeFromEffectivePathChanged(Action callback) + { + onEffectivePathChanged -= callback; + } + + public void Dispose() + { + onEffectivePathChanged = null; + } + } + + public sealed class RebindContext + { + public InputAction action; + public int bindingIndex; + public string overridePath; + private string cachedToString; + + public RebindContext(InputAction action, int bindingIndex, string overridePath) + { + this.action = action; + this.bindingIndex = bindingIndex; + this.overridePath = overridePath; + } + + public override bool Equals(object obj) + { + if (obj is not RebindContext other) return false; + if (action == null || other.action == null) return false; + return action.name == other.action.name && bindingIndex == other.bindingIndex; + } + + public override int GetHashCode() => (action?.name ?? string.Empty, bindingIndex).GetHashCode(); + + public override string ToString() + { + if (cachedToString == null && action != null) + { + cachedToString = $"{action.name}:{bindingIndex}"; + } + + return cachedToString ?? ""; + } + } + + /* ---------------- Public API ---------------- */ + + /// + /// 根据操作名称获取输入操作 + /// + /// 操作名称 + /// 输入操作,未找到则返回 null + public static InputAction Action(string actionName) + { + var instance = Instance; + if (instance == null) return null; + + if (instance.actionLookup.TryGetValue(actionName, out var result)) + { + return result.action.action; + } + + Debug.LogError($"[InputBindingManager] Could not find action '{actionName}'"); + return null; + } + + /// + /// 开始重新绑定指定的输入操作 + /// + /// 操作名称 + /// 复合部分名称(可选) + public static void StartRebind(string actionName, string compositePartName = null) + { + var action = Action(actionName); + if (action == null) return; + + // 自动决定 bindingIndex 和 deviceMatch + int bindingIndex = Instance.FindBestBindingIndexForKeyboard(action, compositePartName); + if (bindingIndex < 0) + { + Debug.LogError($"[InputBindingManager] No suitable binding found for action '{actionName}' (part={compositePartName ?? ""})"); + return; + } + + Instance.actions.Disable(); + Instance.PerformInteractiveRebinding(action, bindingIndex, KEYBOARD_DEVICE, true); + Instance.OnRebindStart?.Invoke(); + if (Instance.debugMode) + { + Debug.Log("[InputBindingManager] Rebind started"); + } + } + + /// + /// 取消当前的重新绑定操作 + /// + public static void CancelRebind() => Instance.rebindOperation?.Cancel(); + + /// + /// 确认并应用准备好的重新绑定 + /// + /// 是否清除冲突 + /// 是否成功应用 + public static async UniTask ConfirmApply(bool clearConflicts = true) + { + if (!Instance.isApplyPending) return false; + + try + { + // 在清除之前创建准备好的重绑定的副本 + var appliedContexts = new HashSet(Instance.preparedRebinds); + + foreach (var ctx in Instance.preparedRebinds) + { + if (!string.IsNullOrEmpty(ctx.overridePath)) + { + if (ctx.overridePath == NULL_BINDING) + { + ctx.action.RemoveBindingOverride(ctx.bindingIndex); + } + else + { + ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath); + } + } + + var bp = GetBindingPath(ctx.action.name, ctx.bindingIndex); + if (bp != null) + { + bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath; + } + } + + Instance.preparedRebinds.Clear(); + await Instance.WriteOverridesToDiskAsync(); + BindingsChanged?.Invoke(); + Instance.OnApply?.Invoke(true, appliedContexts); + Instance.isApplyPending = false; + if (Instance.debugMode) + { + Debug.Log("[InputBindingManager] Apply confirmed and saved."); + } + + return true; + } + catch (Exception ex) + { + Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex); + Instance.OnApply?.Invoke(false, new HashSet()); + return false; + } + } + + /// + /// 丢弃准备好的重新绑定 + /// + public static void DiscardPrepared() + { + if (!Instance.isApplyPending) return; + + // 在清除之前创建准备好的重绑定的副本(用于事件通知) + var discardedContexts = new HashSet(Instance.preparedRebinds); + + Instance.preparedRebinds.Clear(); + Instance.isApplyPending = false; + Instance.OnApply?.Invoke(false, discardedContexts); + if (Instance.debugMode) + { + Debug.Log("[InputBindingManager] Prepared rebinds discarded."); + } + } + + private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true) + { + var op = action.PerformInteractiveRebinding(bindingIndex); + + if (!string.IsNullOrEmpty(deviceMatchPath)) + { + op = op.WithControlsHavingToMatchPath(deviceMatchPath); + } + + if (excludeMouseMovementAndScroll) + { + op = op.WithControlsExcluding(MOUSE_DELTA) + .WithControlsExcluding(MOUSE_SCROLL) + .WithControlsExcluding(MOUSE_SCROLL_X) + .WithControlsExcluding(MOUSE_SCROLL_Y); + } + + rebindOperation = op + .OnApplyBinding((o, path) => + { + if (AnyPreparedRebind(path, action, bindingIndex, out var existing)) + { + PrepareRebind(new RebindContext(action, bindingIndex, path)); + PrepareRebind(new RebindContext(existing.action, existing.bindingIndex, NULL_BINDING)); + OnRebindConflict?.Invoke(new RebindContext(action, bindingIndex, path), existing); + } + else if (AnyBindingPath(path, action, bindingIndex, out var dup)) + { + PrepareRebind(new RebindContext(action, bindingIndex, path)); + PrepareRebind(new RebindContext(dup.action, dup.bindingIndex, NULL_BINDING)); + OnRebindConflict?.Invoke(new RebindContext(action, bindingIndex, path), new RebindContext(dup.action, dup.bindingIndex, dup.action.bindings[dup.bindingIndex].path)); + } + else + { + PrepareRebind(new RebindContext(action, bindingIndex, path)); + } + }) + .OnComplete(opc => + { + if (debugMode) + { + Debug.Log("[InputBindingManager] Rebind completed"); + } + + actions.Enable(); + OnRebindEnd?.Invoke(true, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)); + CleanRebindOperation(); + }) + .OnCancel(opc => + { + if (debugMode) + { + Debug.Log("[InputBindingManager] Rebind cancelled"); + } + + actions.Enable(); + OnRebindEnd?.Invoke(false, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)); + CleanRebindOperation(); + }) + .WithCancelingThrough(KEYBOARD_ESCAPE) + .Start(); + } + + private void CleanRebindOperation() + { + rebindOperation?.Dispose(); + rebindOperation = null; + } + + private bool AnyPreparedRebind(string bindingPath, InputAction currentAction, int currentIndex, out RebindContext duplicate) + { + foreach (var ctx in preparedRebinds) + { + if (ctx.overridePath == bindingPath && (ctx.action != currentAction || (ctx.action == currentAction && ctx.bindingIndex != currentIndex))) + { + duplicate = ctx; + return true; + } + } + + duplicate = null; + return false; + } + + private bool AnyBindingPath(string bindingPath, InputAction currentAction, int currentIndex, out (InputAction action, int bindingIndex) duplicate) + { + foreach (var map in actionMap.Values) + { + foreach (var actionPair in map.actions.Values) + { + bool isSameAction = actionPair.action == currentAction; + + foreach (var bindingPair in actionPair.bindings) + { + // Skip if it's the same action and same binding index + if (isSameAction && bindingPair.Key == currentIndex) + continue; + + if (bindingPair.Value.bindingPath.EffectivePath == bindingPath) + { + duplicate = (actionPair.action, bindingPair.Key); + return true; + } + } + } + } + + duplicate = default; + return false; + } + + private void PrepareRebind(RebindContext context) + { + // 如果存在相同操作/绑定的现有重绑定,则移除 + preparedRebinds.Remove(context); + + if (string.IsNullOrEmpty(context.overridePath)) + { + var bp = GetBindingPath(context.action.name, context.bindingIndex); + if (bp != null) context.overridePath = bp.bindingPath; + } + + var bindingPath = GetBindingPath(context.action.name, context.bindingIndex); + if (bindingPath == null) return; + + if (bindingPath.EffectivePath != context.overridePath) + { + preparedRebinds.Add(context); + isApplyPending = true; + OnRebindPrepare?.Invoke(context); + if (debugMode) + { + Debug.Log($"Prepared rebind: {context} -> {context.overridePath}"); + } + } + } + + private async UniTask WriteOverridesToDiskAsync() + { + try + { + var json = actions.SaveBindingOverridesAsJson(); + EnsureSaveDirectoryExists(); + using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json); + if (debugMode) + { + Debug.Log($"Overrides saved to {SavePath}"); + } + } + catch (Exception ex) + { + Debug.LogError("Failed to save overrides: " + ex); + throw; + } + } + + /// + /// 重置所有绑定到默认值 + /// + public async UniTask ResetToDefaultAsync() + { + try + { + if (!string.IsNullOrEmpty(defaultBindingsJson)) + { + actions.LoadBindingOverridesFromJson(defaultBindingsJson); + } + else + { + foreach (var map in actionMap.Values) + { + foreach (var a in map.actions.Values) + { + for (int b = 0; b < a.action.bindings.Count; b++) + { + a.action.RemoveBindingOverride(b); + } + } + } + } + + RefreshBindingPathsFromActions(); + await WriteOverridesToDiskAsync(); + BindingsChanged?.Invoke(); + if (debugMode) + { + Debug.Log("Reset to default and saved."); + } + } + catch (Exception ex) + { + Debug.LogError("Failed to reset defaults: " + ex); + } + } + + /// + /// 获取指定操作的绑定路径 + /// + /// 操作名称 + /// 绑定索引 + /// 绑定路径,未找到则返回 null + public static BindingPath GetBindingPath(string actionName, int bindingIndex = 0) + { + var instance = Instance; + if (instance == null) return null; + + if (instance.actionLookup.TryGetValue(actionName, out var result)) + { + if (result.action.bindings.TryGetValue(bindingIndex, out var binding)) + { + return binding.bindingPath; + } + } + + return null; + } + + + // 为键盘选择最佳绑定索引;如果 compositePartName != null 则查找部分 + /// + /// 为键盘查找最佳的绑定索引 + /// + /// 输入操作 + /// 复合部分名称(可选) + /// 绑定索引,未找到则返回 -1 + public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null) + { + if (action == null) return -1; + + int fallbackPart = -1; + int fallbackNonComposite = -1; + bool searchingForCompositePart = !string.IsNullOrEmpty(compositePartName); + + for (int i = 0; i < action.bindings.Count; i++) + { + var b = action.bindings[i]; + + // 如果搜索特定的复合部分,跳过不匹配的绑定 + if (searchingForCompositePart) + { + if (!b.isPartOfComposite) continue; + if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; + } + + // 检查此绑定是否用于键盘 + bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) || + (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)); + + if (b.isPartOfComposite) + { + if (fallbackPart == -1) fallbackPart = i; + if (isKeyboardBinding) return i; + } + else + { + if (fallbackNonComposite == -1) fallbackNonComposite = i; + if (isKeyboardBinding) return i; + } + } + + return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart; + } + + public static InputBindingManager Instance + { + get + { + if (_instance == null) + { + _instance = FindObjectOfType(); + } + + return _instance; + } + } + + private static InputBindingManager _instance; +} diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs index b184865..9d54659 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs @@ -1,9 +1,10 @@ -using System; -using System.Text.RegularExpressions; -using AlicizaX; +using System; +#if UNITY_EDITOR using UnityEditor; +#endif using UnityEngine; using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Controls; public static class InputDeviceWatcher { @@ -15,35 +16,108 @@ public static class InputDeviceWatcher Other } - static readonly float DebounceWindow = 1f; - public static InputDeviceCategory CurrentCategory = InputDeviceCategory.Keyboard; - public static string CurrentDeviceName = ""; + public readonly struct DeviceContext : IEquatable + { + 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 int _lastDeviceId = -1; - private static float _lastInputTime = -Mathf.Infinity; - - private static InputDeviceCategory _lastEmittedCategory = InputDeviceCategory.Keyboard; + private static float _lastSwitchTime = -Mathf.Infinity; + private static DeviceContext _lastEmittedContext = CreateDefaultContext(); + private static bool _initialized; public static event Action OnDeviceChanged; - - private static bool initialized = false; + public static event Action OnDeviceContextChanged; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] public static void Initialize() { - if (initialized) return; - initialized = true; + if (_initialized) + { + return; + } - CurrentCategory = InputDeviceCategory.Keyboard; - CurrentDeviceName = ""; - _lastEmittedCategory = CurrentCategory; // 初始化同步 + _initialized = true; + ApplyContext(CreateDefaultContext(), false); + _lastEmittedContext = CurrentContext; _anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough); _anyInputAction.AddBinding("/anyKey"); + _anyInputAction.AddBinding("/leftButton"); + _anyInputAction.AddBinding("/rightButton"); + _anyInputAction.AddBinding("/middleButton"); + _anyInputAction.AddBinding("/scroll"); _anyInputAction.AddBinding("/*"); _anyInputAction.AddBinding("/*"); - _anyInputAction.performed += OnAnyInputPerformed; _anyInputAction.Enable(); @@ -54,142 +128,311 @@ public static class InputDeviceWatcher } #if UNITY_EDITOR - static void OnPlayModeStateChanged(PlayModeStateChange state) + private static void OnPlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.ExitingPlayMode) { Dispose(); + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } - - EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } #endif public static void Dispose() { - if (!initialized) return; - CurrentCategory = InputDeviceCategory.Keyboard; - _anyInputAction.performed -= OnAnyInputPerformed; - _anyInputAction.Disable(); - _anyInputAction.Dispose(); + if (!_initialized) + { + return; + } + + if (_anyInputAction != null) + { + _anyInputAction.performed -= OnAnyInputPerformed; + _anyInputAction.Disable(); + _anyInputAction.Dispose(); + _anyInputAction = null; + } InputSystem.onDeviceChange -= OnDeviceChange; + ApplyContext(CreateDefaultContext(), false); + _lastEmittedContext = CurrentContext; + _lastSwitchTime = -Mathf.Infinity; OnDeviceChanged = null; - initialized = false; - - _lastEmittedCategory = InputDeviceCategory.Keyboard; + OnDeviceContextChanged = null; + _initialized = false; } - 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; + if (deviceContext.Category == CurrentCategory && now - _lastSwitchTime < SameCategoryDebounceWindow) + { + return; + } - if (curId == _lastDeviceId) return; - if (DebounceWindow > 0f && (now - _lastInputTime) < DebounceWindow) return; - - _lastInputTime = now; - _lastDeviceId = curId; - - CurrentCategory = DetermineCategoryFromDevice(device); - CurrentDeviceName = device.displayName ?? $"Device_{curId}"; - - EmitChange(); + _lastSwitchTime = now; + SetCurrentContext(deviceContext); } private static void OnDeviceChange(InputDevice device, InputDeviceChange change) { - if (change == InputDeviceChange.Removed || change == InputDeviceChange.Disconnected) + if (device == null) { - if (device.deviceId == _lastDeviceId) - { - _lastDeviceId = -1; - _lastInputTime = -Mathf.Infinity; - CurrentDeviceName = ""; - CurrentCategory = InputDeviceCategory.Keyboard; - EmitChange(); - } + return; + } + + 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 InputDeviceCategory DetermineCategoryFromDevice(InputDevice device) + private static void PromoteFallbackDevice(int removedDeviceId) { - if (device == null) return InputDeviceCategory.Keyboard; - // 重要:鼠标不再被视为键盘类(避免鼠标触发时回退到 Keyboard) - if (device is Keyboard) return InputDeviceCategory.Keyboard; - if (device is Mouse) return InputDeviceCategory.Other; // 明确忽略鼠标 - if (IsGamepadLike(device)) return GetGamepadCategory(device); + 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; + } - string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower(); + SetCurrentContext(BuildContext(device)); + return; + } - 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; + SetCurrentContext(CreateDefaultContext()); } - private static bool IsGamepadLike(InputDevice device) + private static void SetCurrentContext(DeviceContext context) { - if (device is Gamepad) return true; - if (device is Joystick) return true; + bool categoryChanged = CurrentCategory != context.Category; + ApplyContext(context, true); - var layout = (device.layout ?? "").ToLower(); - // 这里保留 controller/gamepad/joystick 的识别,但忽略 mouse/touch 等 - if (layout.Contains("mouse") || layout.Contains("touch") || layout.Contains("pen")) return false; - return layout.Contains("gamepad") || layout.Contains("controller") || layout.Contains("joystick"); + 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) + { + if (device == null) + { + return InputDeviceCategory.Keyboard; + } + + if (device is Keyboard || device is Mouse) + { + return InputDeviceCategory.Keyboard; + } + + if (IsGamepadLike(device)) + { + return GetGamepadCategory(device); + } + + 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) { - if (device == null) return false; - if (device is Keyboard) return true; - if (IsGamepadLike(device)) return true; - return false; + return device is Keyboard || device is Mouse || IsGamepadLike(device); + } + + private static bool IsRelevantControl(InputControl control) + { + if (control == null || control.device == null || !IsRelevantDevice(control.device) || control.synthetic) + { + 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) { - if (device == null) return InputDeviceCategory.Other; - - var iface = (device.description.interfaceName ?? "").ToLower(); - if (iface.Contains("xinput")) return InputDeviceCategory.Xbox; - - if (TryParseVidPidFromCapabilities(device.description.capabilities, out int vendorId, out int _)) + if (device == null) { - if (vendorId == 0x045E || vendorId == 1118) return InputDeviceCategory.Xbox; - if (vendorId == 0x054C || vendorId == 1356) return InputDeviceCategory.PlayStation; + return InputDeviceCategory.Other; } - string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower(); - if (combined.Contains("xbox")) return InputDeviceCategory.Xbox; - if (combined.Contains("dualshock") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation; + string interfaceName = device.description.interfaceName ?? string.Empty; + if (ContainsIgnoreCase(interfaceName, "xinput")) + { + 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; } - // ------------------ VID/PID 解析 -------------------- - private static bool TryParseVidPidFromCapabilities(string capabilities, out int vendorId, out int productId) + private static string CombineDeviceDescription(InputDevice device) + { + 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; productId = 0; - if (string.IsNullOrEmpty(capabilities)) return false; + if (string.IsNullOrWhiteSpace(capabilities)) + { + return false; + } try { - var decVendor = Regex.Match(capabilities, "\"vendorId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase); - var decProduct = Regex.Match(capabilities, "\"productId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase); - - if (decVendor.Success) int.TryParse(decVendor.Groups[1].Value, out vendorId); - if (decProduct.Success) int.TryParse(decProduct.Groups[1].Value, out productId); - + DeviceCapabilityInfo info = JsonUtility.FromJson(capabilities); + vendorId = info.vendorId; + productId = info.productId; return vendorId != 0 || productId != 0; } 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; - } - - 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; + return !string.IsNullOrEmpty(source) && source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0; } } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs new file mode 100644 index 0000000..e5d9811 --- /dev/null +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs @@ -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(); +} diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs.meta b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs.meta new file mode 100644 index 0000000..8d7ad76 --- /dev/null +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphBehaviourBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a16393b81b47f1844a49d492284be475 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs index d5879d3..7a5911c 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs @@ -1,10 +1,7 @@ -using System; +using System; using System.Collections.Generic; -using TMPro; using UnityEngine; using UnityEngine.InputSystem; -using UnityEngine.U2D; - [Serializable] public sealed class GlyphEntry @@ -17,205 +14,265 @@ public sealed class GlyphEntry public sealed class DeviceGlyphTable { public string deviceName; - public Texture2D spriteSheetTexture; - public Sprite platformIcons; - public List entries = new List(); } [CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "InputGlyphs/InputGlyphDatabase", order = 400)] public sealed class InputGlyphDatabase : ScriptableObject { - private const string DEVICE_KEYBOARD = "Keyboard"; - private const string DEVICE_XBOX = "Xbox"; - private const string DEVICE_PLAYSTATION = "PlayStation"; + private const string DeviceKeyboard = "Keyboard"; + private const string DeviceXbox = "Xbox"; + 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 tables = new List(); - - // 当 FindEntryByControlPath 传空 path 时返回的占位 sprite public Sprite placeholderSprite; - // 用于更快查找的缓存 private Dictionary _tableCache; - private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache; + private Dictionary> _pathLookup; - /// - /// 启用时构建缓存 - /// private void OnEnable() { BuildCache(); } - /// - /// 构建表和精灵的查找缓存 - /// - private void BuildCache() +#if UNITY_EDITOR + private void OnValidate() { - if (_tableCache == null) - { - _tableCache = new Dictionary(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; - } - } + BuildCache(); } +#endif - /// - /// 根据设备名称获取设备图标表 - /// - /// 设备名称 - /// 设备图标表 public DeviceGlyphTable GetTable(string deviceName) { - if (string.IsNullOrEmpty(deviceName)) return null; - if (tables == null) return null; - - // 确保缓存已构建 - if (_tableCache == null || _tableCache.Count == 0) + if (string.IsNullOrWhiteSpace(deviceName) || tables == null) { - BuildCache(); + return null; } - // 使用缓存进行 O(1) 查找 - if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table)) - { - return table; - } - - return null; + EnsureCache(); + _tableCache.TryGetValue(deviceName.ToLowerInvariant(), out DeviceGlyphTable table); + return table; } - /// - /// 获取平台图标 - /// - /// 设备类型 - /// 平台图标 Sprite - public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device) - { - var table = GetTable(device); - if (table == null) return null; - return table.platformIcons; - } - - /// - /// 根据设备类型获取设备图标表 - /// - /// 设备类型 - /// 设备图标表 public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) { - // 使用常量避免字符串分配 - string name; switch (device) { case InputDeviceWatcher.InputDeviceCategory.Keyboard: - name = DEVICE_KEYBOARD; - break; + return GetTable(DeviceKeyboard); case InputDeviceWatcher.InputDeviceCategory.Xbox: - name = DEVICE_XBOX; - break; + return GetTable(DeviceXbox); case InputDeviceWatcher.InputDeviceCategory.PlayStation: - name = DEVICE_PLAYSTATION; - break; + return GetTable(DevicePlayStation); default: - name = DEVICE_XBOX; - break; + return GetTable(DeviceOther) ?? GetTable(DeviceXbox); } - - return GetTable(name); } - /// - /// 根据控制路径和设备类型查找 Sprite - /// - /// 控制路径 - /// 设备类型 - /// 找到的 Sprite,未找到则返回占位 Sprite + 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 map) && map.TryGetValue(key, out sprite) && sprite != null) + { + return true; + } + } + + sprite = placeholderSprite; + return sprite != null; + } + public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) { - if (string.IsNullOrEmpty(controlPath)) - { - 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; + return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite; } - /// - /// 根据控制路径和设备类型查找图标条目 - /// - /// 控制路径 - /// 设备类型 - /// 找到的图标条目,未找到则返回 null public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device) { - var t = GetTable(device); - if (t != null && t.entries != null) + if (!TryGetSprite(controlPath, device, out Sprite sprite) || sprite == null) { - for (int i = 0; i < t.entries.Count; ++i) + return null; + } + + InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device); + for (int i = 0; i < lookupOrder.Length; i++) + { + DeviceGlyphTable table = GetTable(lookupOrder[i]); + if (table == null || table.entries == null) { - var e = t.entries[i]; - if (e == null) continue; - if (e.action == null) continue; + continue; + } - var bindings = e.action.bindings; - int bindingCount = bindings.Count; - if (bindingCount <= 0) continue; - - for (int j = 0; j < bindingCount; j++) + for (int j = 0; j < table.entries.Count; j++) + { + GlyphEntry entry = table.entries[j]; + if (entry != null && entry.Sprite == sprite) { - 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)) - { - return e; - } + return entry; } } } return null; } + + private void EnsureCache() + { + if (_tableCache == null || _pathLookup == null) + { + BuildCache(); + } + } + + private void BuildCache() + { + _tableCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + _tableCache.Clear(); + + _pathLookup ??= new Dictionary>(); + _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 map = _pathLookup[category]; + RegisterEntries(table, map); + } + } + + private void InitializeLookup(InputDeviceWatcher.InputDeviceCategory category) + { + _pathLookup[category] = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + private void RegisterEntries(DeviceGlyphTable table, Dictionary 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 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; + } + } } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphImage.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphImage.cs index 4696938..54700d6 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphImage.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphImage.cs @@ -1,72 +1,62 @@ -using UnityEngine; -using UnityEngine.UI; +using UnityEngine; using UnityEngine.InputSystem; +using UnityEngine.UI; public sealed class InputGlyphImage : MonoBehaviour +public sealed class InputGlyphImage : InputGlyphBehaviourBase { [SerializeField] private InputActionReference actionReference; [SerializeField] private Image targetImage; [SerializeField] private bool hideIfMissing = false; [SerializeField] private GameObject hideTargetObject; - private InputDeviceWatcher.InputDeviceCategory _cachedCategory; private Sprite _cachedSprite; - /// - /// 启用时初始化组件并订阅设备变更事件 - /// - void OnEnable() + protected override void OnEnable() { - if (targetImage == null) targetImage = GetComponent(); - InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; - _cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard; - UpdatePrompt(); - } - - /// - /// 禁用时取消订阅设备变更事件 - /// - void OnDisable() - { - InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; - } - - /// - /// 设备类型变更时的回调,更新图标显示 - /// - void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) - { - if (_cachedCategory != cat) + if (targetImage == null) { - _cachedCategory = cat; - UpdatePrompt(); - } - } - - /// - /// 更新输入提示图标,并根据配置控制目标对象的显示/隐藏 - /// - void UpdatePrompt() - { - if (actionReference == null || actionReference.action == null || targetImage == null) return; - - // 使用缓存的设备类型,避免重复查询 CurrentCategory - if (GlyphService.TryGetUISpriteForActionPath(actionReference, "", _cachedCategory, out Sprite sprite)) - { - if (_cachedSprite != sprite) - { - _cachedSprite = sprite; - targetImage.sprite = sprite; - } + targetImage = GetComponent(); } - if (hideTargetObject != null) + base.OnEnable(); + } + + protected override void RefreshGlyph() + { + if (actionReference == null || actionReference.action == null || targetImage == null) { - bool shouldBeActive = sprite != null && !hideIfMissing; - if (hideTargetObject.activeSelf != shouldBeActive) + if (targetImage != null && _cachedSprite != null) { - hideTargetObject.SetActive(shouldBeActive); + _cachedSprite = null; + targetImage.sprite = null; } + + ApplyVisibility(false); + return; + } + + bool hasSprite = GlyphService.TryGetUISpriteForActionPath(actionReference, string.Empty, CurrentCategory, out Sprite sprite); + if (_cachedSprite != sprite) + { + _cachedSprite = sprite; + targetImage.sprite = sprite; + } + + ApplyVisibility(hasSprite && sprite != null); + } + + private void ApplyVisibility(bool hasSprite) + { + if (hideTargetObject == null) + { + return; + } + + bool shouldBeActive = !hideIfMissing || hasSprite; + if (hideTargetObject.activeSelf != shouldBeActive) + { + hideTargetObject.SetActive(shouldBeActive); } } } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs index 577140b..dbf83c8 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs @@ -1,75 +1,53 @@ -using System; -using System.Linq; -using AlicizaX; -using UnityEngine; +using AlicizaX; using TMPro; +using UnityEngine; using UnityEngine.InputSystem; [RequireComponent(typeof(TextMeshProUGUI))] -public sealed class InputGlyphText : MonoBehaviour +public sealed class InputGlyphText : InputGlyphBehaviourBase { [SerializeField] private InputActionReference actionReference; - private TMP_Text textField; - private string _oldText; - private InputDeviceWatcher.InputDeviceCategory _cachedCategory; + + private TMP_Text _textField; + private string _templateText; private string _cachedFormattedText; - /// - /// 启用时初始化组件并订阅设备变更事件 - /// - void OnEnable() + protected override void OnEnable() { - if (textField == null) textField = GetComponent(); - InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; - _oldText = textField.text; - _cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard; - UpdatePrompt(); - } - - /// - /// 禁用时取消订阅设备变更事件 - /// - void OnDisable() - { - InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; - } - - /// - /// 设备类型变更时的回调,更新文本显示 - /// - void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) - { - if (_cachedCategory != cat) + if (_textField == null) { - _cachedCategory = cat; - UpdatePrompt(); + _textField = GetComponent(); } + + if (string.IsNullOrEmpty(_templateText) && _textField != null) + { + _templateText = _textField.text; + } + + base.OnEnable(); } - /// - /// 更新文本中的输入提示标签,使用 TextMeshPro 的 sprite 标签或文本回退 - /// - void UpdatePrompt() + protected override void RefreshGlyph() { - if (actionReference == null || actionReference.action == null || textField == null) return; - - // 使用缓存的设备类型,避免重复查询 CurrentCategory - if (GlyphService.TryGetTMPTagForActionPath(actionReference, "", _cachedCategory, out string tag, out string displayFallback)) + if (actionReference == null || actionReference.action == null || _textField == null) { - string formattedText = Utility.Text.Format(_oldText, tag); - if (_cachedFormattedText != formattedText) - { - _cachedFormattedText = formattedText; - textField.text = formattedText; - } return; } - string fallbackText = Utility.Text.Format(_oldText, displayFallback); - if (_cachedFormattedText != fallbackText) + string formattedText; + if (GlyphService.TryGetTMPTagForActionPath(actionReference, string.Empty, CurrentCategory, out string tag, out string displayFallback)) { - _cachedFormattedText = fallbackText; - textField.text = fallbackText; + formattedText = Utility.Text.Format(_templateText, tag); + } + else + { + formattedText = Utility.Text.Format(_templateText, displayFallback); + } + + if (_cachedFormattedText != formattedText) + { + _cachedFormattedText = formattedText; + _textField.text = formattedText; } } } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphUXButton.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphUXButton.cs index b119273..ae95bac 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphUXButton.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphUXButton.cs @@ -1,21 +1,16 @@ -using System; -using UnityEngine; -using UnityEngine.UI; +using UnityEngine; using UnityEngine.InputSystem; +using UnityEngine.UI; [RequireComponent(typeof(UXButton))] -public sealed class InputGlyphUXButton : MonoBehaviour +public sealed class InputGlyphUXButton : InputGlyphBehaviourBase { [SerializeField] private UXButton button; [SerializeField] private Image targetImage; - private InputActionReference _actionReference; - private InputDeviceWatcher.InputDeviceCategory _cachedCategory; + private Sprite _cachedSprite; #if UNITY_EDITOR - /// - /// 编辑器验证,自动获取 UXButton 组件 - /// private void OnValidate() { if (button == null) @@ -25,54 +20,45 @@ public sealed class InputGlyphUXButton : MonoBehaviour } #endif - /// - /// 启用时初始化组件并订阅设备变更事件 - /// - void OnEnable() + protected override void OnEnable() { - if (button == null) button = GetComponent(); - if (targetImage == null) targetImage = GetComponent(); - _actionReference = button.HotKeyRefrence; - InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; - _cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard; - UpdatePrompt(); - } - - /// - /// 禁用时取消订阅设备变更事件 - /// - void OnDisable() - { - InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; - } - - /// - /// 设备类型变更时的回调,更新图标显示 - /// - void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) - { - if (_cachedCategory != cat) + if (button == null) { - _cachedCategory = cat; - UpdatePrompt(); + button = GetComponent(); } + + if (targetImage == null) + { + targetImage = GetComponent(); + } + + base.OnEnable(); } - /// - /// 更新按钮的输入提示图标 - /// - void UpdatePrompt() + protected override void RefreshGlyph() { - if (_actionReference == null || _actionReference.action == null || targetImage == null) return; - - // 使用缓存的设备类型,避免重复查询 CurrentCategory - if (GlyphService.TryGetUISpriteForActionPath(_actionReference, "", _cachedCategory, out Sprite sprite)) + InputActionReference actionReference = button != null ? button.HotKeyRefrence : null; + if (actionReference == null || actionReference.action == null || targetImage == null) { - if (_cachedSprite != sprite) + if (targetImage != null && _cachedSprite != null) { - _cachedSprite = sprite; - targetImage.sprite = sprite; + _cachedSprite = null; + targetImage.sprite = null; } + + return; + } + + bool hasSprite = GlyphService.TryGetUISpriteForActionPath(actionReference, string.Empty, CurrentCategory, out Sprite sprite); + if (!hasSprite) + { + sprite = null; + } + + if (_cachedSprite != sprite) + { + _cachedSprite = sprite; + targetImage.sprite = sprite; } } } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/TestRebindScript.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/TestRebindScript.cs index a2de64e..dd541af 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/TestRebindScript.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/TestRebindScript.cs @@ -27,7 +27,8 @@ public class TestRebindScript : MonoBehaviour private void Start() { if (btn != null) btn.onClick.AddListener(OnBtnClicked); - InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; + InputDeviceWatcher.OnDeviceContextChanged += OnDeviceContextChanged; + InputBindingManager.BindingsChanged += OnBindingsChanged; UpdateBindingText(); if (InputBindingManager.Instance != null) @@ -46,7 +47,8 @@ public class TestRebindScript : MonoBehaviour private void OnDisable() { if (btn != null) btn.onClick.RemoveListener(OnBtnClicked); - InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; + InputDeviceWatcher.OnDeviceContextChanged -= OnDeviceContextChanged; + InputBindingManager.BindingsChanged -= OnBindingsChanged; if (InputBindingManager.Instance != null) { @@ -115,7 +117,12 @@ public class TestRebindScript : MonoBehaviour /// /// 设备变更的回调 /// - private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _) + private void OnDeviceContextChanged(InputDeviceWatcher.DeviceContext _) + { + UpdateBindingText(); + } + + private void OnBindingsChanged() { UpdateBindingText(); } @@ -207,6 +214,7 @@ public class TestRebindScript : MonoBehaviour private void UpdateBindingText() { var action = GetAction(); + var deviceCat = InputDeviceWatcher.CurrentCategory; if (action == null) { bindKeyText.text = ""; @@ -215,15 +223,12 @@ public class TestRebindScript : MonoBehaviour } - bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName); + bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName, deviceCat); try { - var deviceCat = InputDeviceWatcher.CurrentCategory; - InputActionReference refr=default; - // string controlPath = GlyphService.GetBindingControlPath(action, compositePartName, deviceCat); - if ( GlyphService.TryGetUISpriteForActionPath(action,compositePartName, deviceCat, out Sprite sprite)) + if (GlyphService.TryGetUISpriteForActionPath(action, compositePartName, deviceCat, out Sprite sprite)) { if (targetImage != null) targetImage.sprite = sprite; }