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