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.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<string, string> DisplayNameCache = new(StringComparer.Ordinal);
private static readonly Dictionary<int, string> SpriteTagCache = new();
private static InputGlyphDatabase _database;
@ -102,7 +105,7 @@ public static class GlyphService
return false;
}
tag = $"<sprite name=\"{sprite.name}\">";
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 = $"<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;
}
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, "<Keyboard>") || StartsWithDevice(path, "<Mouse>");
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, XboxGroupHints);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, PlayStationGroupHints);
default:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, OtherGamepadGroupHints);
}
}
private static bool StartsWithDevice(string path, string deviceTag)
{
return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase);
@ -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, "<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,
binding.name,
bindingIndex,
binding.groups?.Split(InputBinding.Separator) ?? Array.Empty<string>(),
new BindingPath(binding.path, binding.overridePath),
binding
));
@ -281,18 +280,16 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
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<InputBindingManager>
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)
{
@ -510,7 +509,7 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
catch (Exception ex)
{
Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex);
Instance.OnApply?.Invoke(false, new HashSet<RebindContext>());
Instance.OnApply?.Invoke(false, null);
return false;
}
}
@ -523,7 +522,9 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
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.isApplyPending = false;
@ -554,21 +555,23 @@ public class InputBindingManager : MonoSingleton<InputBindingManager>
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<InputBindingManager>
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);

View File

@ -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<int, DeviceContext> DeviceContextCache = new();
private static bool _initialized;
public static event Action<InputDeviceCategory> 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)

View File

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

View File

@ -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;