diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs index 4b7df7c..5028dd0 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/GlyphService.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; @@ -9,6 +10,8 @@ public static class GlyphService 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 readonly Dictionary DisplayNameCache = new(StringComparer.Ordinal); + private static readonly Dictionary SpriteTagCache = new(); private static InputGlyphDatabase _database; @@ -102,7 +105,7 @@ public static class GlyphService return false; } - tag = $""; + tag = GetSpriteTag(sprite); return true; } @@ -138,14 +141,21 @@ public static class GlyphService return string.Empty; } + if (DisplayNameCache.TryGetValue(controlPath, out string cachedDisplayName)) + { + return cachedDisplayName; + } + string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice); if (!string.IsNullOrWhiteSpace(humanReadable)) { + DisplayNameCache[controlPath] = humanReadable; return humanReadable; } - string[] parts = controlPath.Split('/'); - string last = parts[parts.Length - 1].Trim(TrimChars); + int separatorIndex = controlPath.LastIndexOf('/'); + string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars); + DisplayNameCache[controlPath] = last; return last; } @@ -237,11 +247,69 @@ public static class GlyphService } string[] hints = GetGroupHints(category); - string[] tokens = groups.Split(InputBinding.Separator); - for (int i = 0; i < tokens.Length; i++) + int tokenStart = 0; + for (int i = 0; i <= groups.Length; i++) { - string token = tokens[i].Trim(); - if (ContainsAny(token, hints)) + if (i < groups.Length && groups[i] != InputBinding.Separator) + { + continue; + } + + int tokenLength = i - tokenStart; + while (tokenLength > 0 && char.IsWhiteSpace(groups[tokenStart])) + { + tokenStart++; + tokenLength--; + } + + while (tokenLength > 0 && char.IsWhiteSpace(groups[tokenStart + tokenLength - 1])) + { + tokenLength--; + } + + if (tokenLength > 0) + { + string token = groups.Substring(tokenStart, tokenLength); + if (ContainsAny(token, hints)) + { + return true; + } + } + + tokenStart = i + 1; + } + + return false; + } + + private static string GetSpriteTag(Sprite sprite) + { + if (sprite == null) + { + return null; + } + + int instanceId = sprite.GetInstanceID(); + if (SpriteTagCache.TryGetValue(instanceId, out string cachedTag)) + { + return cachedTag; + } + + cachedTag = $""; + SpriteTagCache[instanceId] = cachedTag; + return cachedTag; + } + + 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; } @@ -250,26 +318,6 @@ public static class GlyphService 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, "") || StartsWithDevice(path, ""); - case InputDeviceWatcher.InputDeviceCategory.Xbox: - return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, XboxGroupHints); - case InputDeviceWatcher.InputDeviceCategory.PlayStation: - return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, PlayStationGroupHints); - default: - return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, OtherGamepadGroupHints); - } - } - private static bool StartsWithDevice(string path, string deviceTag) { return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase); @@ -290,26 +338,29 @@ public static class GlyphService } } - 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; } + + private static bool MatchesControlPath(string path, InputDeviceWatcher.InputDeviceCategory category) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + switch (category) + { + case InputDeviceWatcher.InputDeviceCategory.Keyboard: + return StartsWithDevice(path, "") || StartsWithDevice(path, ""); + case InputDeviceWatcher.InputDeviceCategory.Xbox: + return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, XboxGroupHints); + case InputDeviceWatcher.InputDeviceCategory.PlayStation: + return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, PlayStationGroupHints); + default: + return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, OtherGamepadGroupHints); + } + } + } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs index c1e41a2..04857c6 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputBindingManager.cs @@ -268,7 +268,6 @@ public class InputBindingManager : MonoSingleton action.name, binding.name, bindingIndex, - binding.groups?.Split(InputBinding.Separator) ?? Array.Empty(), new BindingPath(binding.path, binding.overridePath), binding )); @@ -281,18 +280,16 @@ public class InputBindingManager : MonoSingleton 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) + 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; } @@ -472,7 +469,9 @@ public class InputBindingManager : MonoSingleton try { // 在清除之前创建准备好的重绑定的副本 - var appliedContexts = new HashSet(Instance.preparedRebinds); + HashSet appliedContexts = Instance.OnApply != null + ? new HashSet(Instance.preparedRebinds) + : null; foreach (var ctx in Instance.preparedRebinds) { @@ -510,7 +509,7 @@ public class InputBindingManager : MonoSingleton catch (Exception ex) { Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex); - Instance.OnApply?.Invoke(false, new HashSet()); + Instance.OnApply?.Invoke(false, null); return false; } } @@ -523,7 +522,9 @@ public class InputBindingManager : MonoSingleton if (!Instance.isApplyPending) return; // 在清除之前创建准备好的重绑定的副本(用于事件通知) - var discardedContexts = new HashSet(Instance.preparedRebinds); + HashSet discardedContexts = Instance.OnApply != null + ? new HashSet(Instance.preparedRebinds) + : null; Instance.preparedRebinds.Clear(); Instance.isApplyPending = false; @@ -554,21 +555,23 @@ public class InputBindingManager : MonoSingleton rebindOperation = op .OnApplyBinding((o, path) => { + RebindContext preparedContext = new RebindContext(action, bindingIndex, path); if (AnyPreparedRebind(path, action, bindingIndex, out var existing)) { - PrepareRebind(new RebindContext(action, bindingIndex, path)); + PrepareRebind(preparedContext); PrepareRebind(new RebindContext(existing.action, existing.bindingIndex, NULL_BINDING)); - OnRebindConflict?.Invoke(new RebindContext(action, bindingIndex, path), existing); + OnRebindConflict?.Invoke(preparedContext, existing); } else if (AnyBindingPath(path, action, bindingIndex, out var dup)) { - PrepareRebind(new RebindContext(action, bindingIndex, path)); + RebindContext conflictingContext = new RebindContext(dup.action, dup.bindingIndex, dup.action.bindings[dup.bindingIndex].path); + PrepareRebind(preparedContext); 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)); + OnRebindConflict?.Invoke(preparedContext, conflictingContext); } else { - PrepareRebind(new RebindContext(action, bindingIndex, path)); + PrepareRebind(preparedContext); } }) .OnComplete(opc => @@ -647,18 +650,17 @@ public class InputBindingManager : MonoSingleton private void PrepareRebind(RebindContext context) { - // 如果存在相同操作/绑定的现有重绑定,则移除 + // Remove any existing prepared state for the same action/binding pair. preparedRebinds.Remove(context); + BindingPath bindingPath = GetBindingPath(context.action, context.bindingIndex); + if (bindingPath == null) return; + if (string.IsNullOrEmpty(context.overridePath)) { - var bp = GetBindingPath(context.action, context.bindingIndex); - if (bp != null) context.overridePath = bp.bindingPath; + context.overridePath = bindingPath.bindingPath; } - var bindingPath = GetBindingPath(context.action, context.bindingIndex); - if (bindingPath == null) return; - if (bindingPath.EffectivePath != context.overridePath) { preparedRebinds.Add(context); diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs index 9d54659..d942990 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif @@ -93,6 +94,7 @@ public static class InputDeviceWatcher private static InputAction _anyInputAction; private static float _lastSwitchTime = -Mathf.Infinity; private static DeviceContext _lastEmittedContext = CreateDefaultContext(); + private static readonly Dictionary DeviceContextCache = new(); private static bool _initialized; public static event Action OnDeviceChanged; @@ -154,6 +156,7 @@ public static class InputDeviceWatcher } InputSystem.onDeviceChange -= OnDeviceChange; + DeviceContextCache.Clear(); ApplyContext(CreateDefaultContext(), false); _lastEmittedContext = CurrentContext; @@ -171,7 +174,13 @@ public static class InputDeviceWatcher return; } - DeviceContext deviceContext = BuildContext(control.device); + InputDevice device = control.device; + if (device == null || device.deviceId == CurrentDeviceId) + { + return; + } + + DeviceContext deviceContext = BuildContext(device); if (deviceContext.DeviceId == CurrentDeviceId) { return; @@ -198,6 +207,7 @@ public static class InputDeviceWatcher { case InputDeviceChange.Removed: case InputDeviceChange.Disconnected: + DeviceContextCache.Remove(device.deviceId); if (device.deviceId == CurrentDeviceId) { PromoteFallbackDevice(device.deviceId); @@ -205,6 +215,7 @@ public static class InputDeviceWatcher break; case InputDeviceChange.Reconnected: case InputDeviceChange.Added: + DeviceContextCache.Remove(device.deviceId); if (CurrentDeviceId < 0 && IsRelevantDevice(device)) { SetCurrentContext(BuildContext(device)); @@ -271,15 +282,22 @@ public static class InputDeviceWatcher return CreateDefaultContext(); } + if (DeviceContextCache.TryGetValue(device.deviceId, out DeviceContext cachedContext)) + { + return cachedContext; + } + 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), + DeviceContext context = new DeviceContext( + DetermineCategoryFromDevice(device, vendorId), device.deviceId, vendorId, productId, deviceName, device.layout); + DeviceContextCache[device.deviceId] = context; + return context; } private static DeviceContext CreateDefaultContext() @@ -287,7 +305,7 @@ public static class InputDeviceWatcher 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, int vendorId = 0) { if (device == null) { @@ -301,18 +319,17 @@ public static class InputDeviceWatcher if (IsGamepadLike(device)) { - return GetGamepadCategory(device); + return GetGamepadCategory(device, vendorId); } - string combined = CombineDeviceDescription(device); - if (ContainsIgnoreCase(combined, "xbox") || ContainsIgnoreCase(combined, "xinput")) + if (DescriptionContains(device, "xbox") || DescriptionContains(device, "xinput")) { return InputDeviceCategory.Xbox; } - if (ContainsIgnoreCase(combined, "dualshock") - || ContainsIgnoreCase(combined, "dualsense") - || ContainsIgnoreCase(combined, "playstation")) + if (DescriptionContains(device, "dualshock") + || DescriptionContains(device, "dualsense") + || DescriptionContains(device, "playstation")) { return InputDeviceCategory.PlayStation; } @@ -367,7 +384,7 @@ public static class InputDeviceWatcher || ContainsIgnoreCase(layout, "Joystick"); } - private static InputDeviceCategory GetGamepadCategory(InputDevice device) + private static InputDeviceCategory GetGamepadCategory(InputDevice device, int vendorId = 0) { if (device == null) { @@ -380,28 +397,29 @@ public static class InputDeviceWatcher return InputDeviceCategory.Xbox; } - if (TryParseVendorProductIds(device.description.capabilities, out int vendorId, out _)) + if (vendorId == 0 && TryParseVendorProductIds(device.description.capabilities, out int parsedVendorId, out _)) { - if (vendorId == 0x045E || vendorId == 1118) - { - return InputDeviceCategory.Xbox; - } - - if (vendorId == 0x054C || vendorId == 1356) - { - return InputDeviceCategory.PlayStation; - } + vendorId = parsedVendorId; } - string combined = CombineDeviceDescription(device); - if (ContainsIgnoreCase(combined, "xbox")) + if (vendorId == 0x045E || vendorId == 1118) { return InputDeviceCategory.Xbox; } - if (ContainsIgnoreCase(combined, "dualshock") - || ContainsIgnoreCase(combined, "dualsense") - || ContainsIgnoreCase(combined, "playstation")) + if (vendorId == 0x054C || vendorId == 1356) + { + return InputDeviceCategory.PlayStation; + } + + if (DescriptionContains(device, "xbox")) + { + return InputDeviceCategory.Xbox; + } + + if (DescriptionContains(device, "dualshock") + || DescriptionContains(device, "dualsense") + || DescriptionContains(device, "playstation")) { return InputDeviceCategory.PlayStation; } @@ -409,14 +427,20 @@ public static class InputDeviceWatcher return InputDeviceCategory.Other; } - private static string CombineDeviceDescription(InputDevice device) + private static bool DescriptionContains(InputDevice device, string value) { - return string.Concat( - device.description.interfaceName, " ", - device.layout, " ", - device.description.product, " ", - device.description.manufacturer, " ", - device.displayName); + if (device == null) + { + return false; + } + + var description = device.description; + return ContainsIgnoreCase(description.interfaceName, value) + || ContainsIgnoreCase(device.layout, value) + || ContainsIgnoreCase(description.product, value) + || ContainsIgnoreCase(description.manufacturer, value) + || ContainsIgnoreCase(device.displayName, value) + || ContainsIgnoreCase(device.name, value); } private static bool TryParseVendorProductIds(string capabilities, out int vendorId, out int productId) diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs index 0102c1f..30cc42d 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs @@ -45,6 +45,7 @@ public sealed class InputGlyphDatabase : ScriptableObject InputDeviceWatcher.InputDeviceCategory.Xbox, InputDeviceWatcher.InputDeviceCategory.Keyboard, }; + private static readonly Dictionary NormalizedPathCache = new(StringComparer.Ordinal); public List tables = new List(); public Sprite placeholderSprite; @@ -253,7 +254,14 @@ public sealed class InputGlyphDatabase : ScriptableObject return string.Empty; } - return CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant()); + if (NormalizedPathCache.TryGetValue(controlPath, out string normalizedPath)) + { + return normalizedPath; + } + + normalizedPath = CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant()); + NormalizedPathCache[controlPath] = normalizedPath; + return normalizedPath; } private static string CanonicalizeDeviceLayout(string controlPath) diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs index a2f7754..40cdc5e 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphText.cs @@ -12,6 +12,7 @@ public sealed class InputGlyphText : InputGlyphBehaviourBase private TMP_Text _textField; private string _templateText; private string _cachedFormattedText; + private string _cachedReplacementToken; protected override void OnEnable() { @@ -35,16 +36,23 @@ public sealed class InputGlyphText : InputGlyphBehaviourBase return; } - string formattedText; + string replacementToken; if (GlyphService.TryGetTMPTagForActionPath(actionReference, compositePartName, CurrentCategory, out string tag, out string displayFallback)) { - formattedText = Utility.Text.Format(_templateText, tag); + replacementToken = tag; } else { - formattedText = Utility.Text.Format(_templateText, displayFallback); + replacementToken = displayFallback; } + if (_cachedReplacementToken == replacementToken) + { + return; + } + + _cachedReplacementToken = replacementToken; + string formattedText = Utility.Text.Format(_templateText, replacementToken); if (_cachedFormattedText != formattedText) { _cachedFormattedText = formattedText;