Input模块增加缓存

This commit is contained in:
陈思海 2026-03-19 14:24:12 +08:00
parent 843cd5e38e
commit 86a110aacc
5 changed files with 195 additions and 102 deletions

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; 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[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" };
private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" }; private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" };
private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
private static readonly Dictionary<string, string> DisplayNameCache = new(StringComparer.Ordinal);
private static readonly Dictionary<int, string> SpriteTagCache = new();
private static InputGlyphDatabase _database; private static InputGlyphDatabase _database;
@ -102,7 +105,7 @@ public static class GlyphService
return false; return false;
} }
tag = $"<sprite name=\"{sprite.name}\">"; tag = GetSpriteTag(sprite);
return true; return true;
} }
@ -138,14 +141,21 @@ public static class GlyphService
return string.Empty; return string.Empty;
} }
if (DisplayNameCache.TryGetValue(controlPath, out string cachedDisplayName))
{
return cachedDisplayName;
}
string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice); string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice);
if (!string.IsNullOrWhiteSpace(humanReadable)) if (!string.IsNullOrWhiteSpace(humanReadable))
{ {
DisplayNameCache[controlPath] = humanReadable;
return humanReadable; return humanReadable;
} }
string[] parts = controlPath.Split('/'); int separatorIndex = controlPath.LastIndexOf('/');
string last = parts[parts.Length - 1].Trim(TrimChars); string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars);
DisplayNameCache[controlPath] = last;
return last; return last;
} }
@ -237,39 +247,77 @@ public static class GlyphService
} }
string[] hints = GetGroupHints(category); string[] hints = GetGroupHints(category);
string[] tokens = groups.Split(InputBinding.Separator); int tokenStart = 0;
for (int i = 0; i < tokens.Length; i++) for (int i = 0; i <= groups.Length; i++)
{ {
string token = tokens[i].Trim(); 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)) if (ContainsAny(token, hints))
{ {
return true; return true;
} }
} }
tokenStart = i + 1;
}
return false; return false;
} }
private static bool MatchesControlPath(string path, InputDeviceWatcher.InputDeviceCategory category) private static string GetSpriteTag(Sprite sprite)
{ {
if (string.IsNullOrWhiteSpace(path)) if (sprite == null)
{
return null;
}
int instanceId = sprite.GetInstanceID();
if (SpriteTagCache.TryGetValue(instanceId, out string cachedTag))
{
return cachedTag;
}
cachedTag = $"<sprite name=\"{sprite.name}\">";
SpriteTagCache[instanceId] = cachedTag;
return cachedTag;
}
private static bool ContainsAny(string source, string[] hints)
{
if (string.IsNullOrWhiteSpace(source) || hints == null)
{ {
return false; return false;
} }
switch (category) for (int i = 0; i < hints.Length; i++)
{ {
case InputDeviceWatcher.InputDeviceCategory.Keyboard: if (source.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0)
return StartsWithDevice(path, "<Keyboard>") || StartsWithDevice(path, "<Mouse>"); {
case InputDeviceWatcher.InputDeviceCategory.Xbox: return true;
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, XboxGroupHints);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, PlayStationGroupHints);
default:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, OtherGamepadGroupHints);
} }
} }
return false;
}
private static bool StartsWithDevice(string path, string deviceTag) private static bool StartsWithDevice(string path, string deviceTag)
{ {
return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase); 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) private static string GetEffectivePath(InputBinding binding)
{ {
return string.IsNullOrWhiteSpace(binding.effectivePath) ? binding.path : binding.effectivePath; 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, "<Keyboard>") || StartsWithDevice(path, "<Mouse>");
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, XboxGroupHints);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, PlayStationGroupHints);
default:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, OtherGamepadGroupHints);
}
}
} }

View File

@ -268,7 +268,6 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
action.name, action.name,
binding.name, binding.name,
bindingIndex, bindingIndex,
binding.groups?.Split(InputBinding.Separator) ?? Array.Empty<string>(),
new BindingPath(binding.path, binding.overridePath), new BindingPath(binding.path, binding.overridePath),
binding binding
)); ));
@ -281,18 +280,16 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
public readonly string parentAction; public readonly string parentAction;
public readonly string compositePart; public readonly string compositePart;
public readonly int bindingIndex; public readonly int bindingIndex;
public readonly string[] group;
public readonly BindingPath bindingPath; public readonly BindingPath bindingPath;
public readonly InputBinding inputBinding; public readonly InputBinding inputBinding;
public Binding(string name, string parentAction, string compositePart, int bindingIndex, public Binding(string name, string parentAction, string compositePart, int bindingIndex,
string[] group, BindingPath bindingPath, InputBinding inputBinding) BindingPath bindingPath, InputBinding inputBinding)
{ {
this.name = name; this.name = name;
this.parentAction = parentAction; this.parentAction = parentAction;
this.compositePart = compositePart; this.compositePart = compositePart;
this.bindingIndex = bindingIndex; this.bindingIndex = bindingIndex;
this.group = group;
this.bindingPath = bindingPath; this.bindingPath = bindingPath;
this.inputBinding = inputBinding; this.inputBinding = inputBinding;
} }
@ -472,7 +469,9 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
try try
{ {
// 在清除之前创建准备好的重绑定的副本 // 在清除之前创建准备好的重绑定的副本
var appliedContexts = new HashSet<RebindContext>(Instance.preparedRebinds); HashSet<RebindContext> appliedContexts = Instance.OnApply != null
? new HashSet<RebindContext>(Instance.preparedRebinds)
: null;
foreach (var ctx in Instance.preparedRebinds) foreach (var ctx in Instance.preparedRebinds)
{ {
@ -510,7 +509,7 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex); Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex);
Instance.OnApply?.Invoke(false, new HashSet<RebindContext>()); Instance.OnApply?.Invoke(false, null);
return false; return false;
} }
} }
@ -523,7 +522,9 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
if (!Instance.isApplyPending) return; if (!Instance.isApplyPending) return;
// 在清除之前创建准备好的重绑定的副本(用于事件通知) // 在清除之前创建准备好的重绑定的副本(用于事件通知)
var discardedContexts = new HashSet<RebindContext>(Instance.preparedRebinds); HashSet<RebindContext> discardedContexts = Instance.OnApply != null
? new HashSet<RebindContext>(Instance.preparedRebinds)
: null;
Instance.preparedRebinds.Clear(); Instance.preparedRebinds.Clear();
Instance.isApplyPending = false; Instance.isApplyPending = false;
@ -554,21 +555,23 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
rebindOperation = op rebindOperation = op
.OnApplyBinding((o, path) => .OnApplyBinding((o, path) =>
{ {
RebindContext preparedContext = new RebindContext(action, bindingIndex, path);
if (AnyPreparedRebind(path, action, bindingIndex, out var existing)) 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)); 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)) 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)); 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 else
{ {
PrepareRebind(new RebindContext(action, bindingIndex, path)); PrepareRebind(preparedContext);
} }
}) })
.OnComplete(opc => .OnComplete(opc =>
@ -647,18 +650,17 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
private void PrepareRebind(RebindContext context) private void PrepareRebind(RebindContext context)
{ {
// 如果存在相同操作/绑定的现有重绑定,则移除 // Remove any existing prepared state for the same action/binding pair.
preparedRebinds.Remove(context); preparedRebinds.Remove(context);
BindingPath bindingPath = GetBindingPath(context.action, context.bindingIndex);
if (bindingPath == null) return;
if (string.IsNullOrEmpty(context.overridePath)) if (string.IsNullOrEmpty(context.overridePath))
{ {
var bp = GetBindingPath(context.action, context.bindingIndex); context.overridePath = bindingPath.bindingPath;
if (bp != null) context.overridePath = bp.bindingPath;
} }
var bindingPath = GetBindingPath(context.action, context.bindingIndex);
if (bindingPath == null) return;
if (bindingPath.EffectivePath != context.overridePath) if (bindingPath.EffectivePath != context.overridePath)
{ {
preparedRebinds.Add(context); preparedRebinds.Add(context);

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
#if UNITY_EDITOR #if UNITY_EDITOR
using UnityEditor; using UnityEditor;
#endif #endif
@ -93,6 +94,7 @@ public static class InputDeviceWatcher
private static InputAction _anyInputAction; private static InputAction _anyInputAction;
private static float _lastSwitchTime = -Mathf.Infinity; private static float _lastSwitchTime = -Mathf.Infinity;
private static DeviceContext _lastEmittedContext = CreateDefaultContext(); private static DeviceContext _lastEmittedContext = CreateDefaultContext();
private static readonly Dictionary<int, DeviceContext> DeviceContextCache = new();
private static bool _initialized; private static bool _initialized;
public static event Action<InputDeviceCategory> OnDeviceChanged; public static event Action<InputDeviceCategory> OnDeviceChanged;
@ -154,6 +156,7 @@ public static class InputDeviceWatcher
} }
InputSystem.onDeviceChange -= OnDeviceChange; InputSystem.onDeviceChange -= OnDeviceChange;
DeviceContextCache.Clear();
ApplyContext(CreateDefaultContext(), false); ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext; _lastEmittedContext = CurrentContext;
@ -171,7 +174,13 @@ public static class InputDeviceWatcher
return; 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) if (deviceContext.DeviceId == CurrentDeviceId)
{ {
return; return;
@ -198,6 +207,7 @@ public static class InputDeviceWatcher
{ {
case InputDeviceChange.Removed: case InputDeviceChange.Removed:
case InputDeviceChange.Disconnected: case InputDeviceChange.Disconnected:
DeviceContextCache.Remove(device.deviceId);
if (device.deviceId == CurrentDeviceId) if (device.deviceId == CurrentDeviceId)
{ {
PromoteFallbackDevice(device.deviceId); PromoteFallbackDevice(device.deviceId);
@ -205,6 +215,7 @@ public static class InputDeviceWatcher
break; break;
case InputDeviceChange.Reconnected: case InputDeviceChange.Reconnected:
case InputDeviceChange.Added: case InputDeviceChange.Added:
DeviceContextCache.Remove(device.deviceId);
if (CurrentDeviceId < 0 && IsRelevantDevice(device)) if (CurrentDeviceId < 0 && IsRelevantDevice(device))
{ {
SetCurrentContext(BuildContext(device)); SetCurrentContext(BuildContext(device));
@ -271,15 +282,22 @@ public static class InputDeviceWatcher
return CreateDefaultContext(); return CreateDefaultContext();
} }
if (DeviceContextCache.TryGetValue(device.deviceId, out DeviceContext cachedContext))
{
return cachedContext;
}
TryParseVendorProductIds(device.description.capabilities, out int vendorId, out int productId); TryParseVendorProductIds(device.description.capabilities, out int vendorId, out int productId);
string deviceName = string.IsNullOrWhiteSpace(device.displayName) ? device.name : device.displayName; string deviceName = string.IsNullOrWhiteSpace(device.displayName) ? device.name : device.displayName;
return new DeviceContext( DeviceContext context = new DeviceContext(
DetermineCategoryFromDevice(device), DetermineCategoryFromDevice(device, vendorId),
device.deviceId, device.deviceId,
vendorId, vendorId,
productId, productId,
deviceName, deviceName,
device.layout); device.layout);
DeviceContextCache[device.deviceId] = context;
return context;
} }
private static DeviceContext CreateDefaultContext() 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); 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) if (device == null)
{ {
@ -301,18 +319,17 @@ public static class InputDeviceWatcher
if (IsGamepadLike(device)) if (IsGamepadLike(device))
{ {
return GetGamepadCategory(device); return GetGamepadCategory(device, vendorId);
} }
string combined = CombineDeviceDescription(device); if (DescriptionContains(device, "xbox") || DescriptionContains(device, "xinput"))
if (ContainsIgnoreCase(combined, "xbox") || ContainsIgnoreCase(combined, "xinput"))
{ {
return InputDeviceCategory.Xbox; return InputDeviceCategory.Xbox;
} }
if (ContainsIgnoreCase(combined, "dualshock") if (DescriptionContains(device, "dualshock")
|| ContainsIgnoreCase(combined, "dualsense") || DescriptionContains(device, "dualsense")
|| ContainsIgnoreCase(combined, "playstation")) || DescriptionContains(device, "playstation"))
{ {
return InputDeviceCategory.PlayStation; return InputDeviceCategory.PlayStation;
} }
@ -367,7 +384,7 @@ public static class InputDeviceWatcher
|| ContainsIgnoreCase(layout, "Joystick"); || ContainsIgnoreCase(layout, "Joystick");
} }
private static InputDeviceCategory GetGamepadCategory(InputDevice device) private static InputDeviceCategory GetGamepadCategory(InputDevice device, int vendorId = 0)
{ {
if (device == null) if (device == null)
{ {
@ -380,8 +397,11 @@ public static class InputDeviceWatcher
return InputDeviceCategory.Xbox; return InputDeviceCategory.Xbox;
} }
if (TryParseVendorProductIds(device.description.capabilities, out int vendorId, out _)) if (vendorId == 0 && TryParseVendorProductIds(device.description.capabilities, out int parsedVendorId, out _))
{ {
vendorId = parsedVendorId;
}
if (vendorId == 0x045E || vendorId == 1118) if (vendorId == 0x045E || vendorId == 1118)
{ {
return InputDeviceCategory.Xbox; return InputDeviceCategory.Xbox;
@ -391,17 +411,15 @@ public static class InputDeviceWatcher
{ {
return InputDeviceCategory.PlayStation; return InputDeviceCategory.PlayStation;
} }
}
string combined = CombineDeviceDescription(device); if (DescriptionContains(device, "xbox"))
if (ContainsIgnoreCase(combined, "xbox"))
{ {
return InputDeviceCategory.Xbox; return InputDeviceCategory.Xbox;
} }
if (ContainsIgnoreCase(combined, "dualshock") if (DescriptionContains(device, "dualshock")
|| ContainsIgnoreCase(combined, "dualsense") || DescriptionContains(device, "dualsense")
|| ContainsIgnoreCase(combined, "playstation")) || DescriptionContains(device, "playstation"))
{ {
return InputDeviceCategory.PlayStation; return InputDeviceCategory.PlayStation;
} }
@ -409,14 +427,20 @@ public static class InputDeviceWatcher
return InputDeviceCategory.Other; return InputDeviceCategory.Other;
} }
private static string CombineDeviceDescription(InputDevice device) private static bool DescriptionContains(InputDevice device, string value)
{ {
return string.Concat( if (device == null)
device.description.interfaceName, " ", {
device.layout, " ", return false;
device.description.product, " ", }
device.description.manufacturer, " ",
device.displayName); 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) private static bool TryParseVendorProductIds(string capabilities, out int vendorId, out int productId)

View File

@ -45,6 +45,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
InputDeviceWatcher.InputDeviceCategory.Xbox, InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Keyboard, InputDeviceWatcher.InputDeviceCategory.Keyboard,
}; };
private static readonly Dictionary<string, string> NormalizedPathCache = new(StringComparer.Ordinal);
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>(); public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>();
public Sprite placeholderSprite; public Sprite placeholderSprite;
@ -253,7 +254,14 @@ public sealed class InputGlyphDatabase : ScriptableObject
return string.Empty; 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) private static string CanonicalizeDeviceLayout(string controlPath)

View File

@ -12,6 +12,7 @@ public sealed class InputGlyphText : InputGlyphBehaviourBase
private TMP_Text _textField; private TMP_Text _textField;
private string _templateText; private string _templateText;
private string _cachedFormattedText; private string _cachedFormattedText;
private string _cachedReplacementToken;
protected override void OnEnable() protected override void OnEnable()
{ {
@ -35,16 +36,23 @@ public sealed class InputGlyphText : InputGlyphBehaviourBase
return; return;
} }
string formattedText; string replacementToken;
if (GlyphService.TryGetTMPTagForActionPath(actionReference, compositePartName, CurrentCategory, out string tag, out string displayFallback)) if (GlyphService.TryGetTMPTagForActionPath(actionReference, compositePartName, CurrentCategory, out string tag, out string displayFallback))
{ {
formattedText = Utility.Text.Format(_templateText, tag); replacementToken = tag;
} }
else 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) if (_cachedFormattedText != formattedText)
{ {
_cachedFormattedText = formattedText; _cachedFormattedText = formattedText;