This commit is contained in:
陈思海 2026-03-11 13:04:31 +08:00
parent 97fa4770bf
commit 2b572c7989
29 changed files with 334 additions and 53 deletions

View File

@ -1,7 +1,8 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(cd InputGlyph && ls -la *.cs)" "Bash(cd InputGlyph && ls -la *.cs)",
"Bash(ls -la InputGlyph/*.cs | grep -v Editor)"
] ]
} }
} }

View File

@ -4,12 +4,15 @@ using UnityEngine.InputSystem;
public static class GlyphService public static class GlyphService
{ {
// Cached device hint arrays to avoid allocations // 缓存的设备提示数组,避免内存分配
private static readonly string[] KeyboardHints = { "Keyboard", "Mouse" }; private static readonly string[] KeyboardHints = { "Keyboard", "Mouse" };
private static readonly string[] XboxHints = { "XInput", "Xbox", "Gamepad" }; private static readonly string[] XboxHints = { "XInput", "Xbox", "Gamepad" };
private static readonly string[] PlayStationHints = { "DualShock", "DualSense", "PlayStation", "Gamepad" }; private static readonly string[] PlayStationHints = { "DualShock", "DualSense", "PlayStation", "Gamepad" };
private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
/// <summary>
/// 获取输入图标数据库实例
/// </summary>
public static InputGlyphDatabase Database public static InputGlyphDatabase Database
{ {
get get
@ -26,6 +29,13 @@ public static class GlyphService
private static InputGlyphDatabase _database; private static InputGlyphDatabase _database;
/// <summary>
/// 获取输入操作的绑定控制路径
/// </summary>
/// <param name="action">输入操作</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <param name="deviceOverride">设备类型覆盖(可选)</param>
/// <returns>绑定控制路径</returns>
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; if (action == null) return string.Empty;
@ -33,18 +43,46 @@ public static class GlyphService
return binding.hasOverrides ? binding.effectivePath : binding.path; return binding.hasOverrides ? binding.effectivePath : binding.path;
} }
/// <summary>
/// 尝试获取输入操作的 TextMeshPro 标签
/// </summary>
/// <param name="reference">输入操作引用</param>
/// <param name="compositePartName">复合部分名称</param>
/// <param name="device">设备类型</param>
/// <param name="tag">输出的 TMP 标签</param>
/// <param name="displayFallback">显示回退文本</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetTMPTagForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) public static bool TryGetTMPTagForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null)
{ {
string path = GetBindingControlPath(reference, compositePartName, device); string path = GetBindingControlPath(reference, compositePartName, device);
return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db); return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db);
} }
/// <summary>
/// 尝试获取输入操作的 UI Sprite
/// </summary>
/// <param name="reference">输入操作引用</param>
/// <param name="compositePartName">复合部分名称</param>
/// <param name="device">设备类型</param>
/// <param name="sprite">输出的 Sprite</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetUISpriteForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) public static bool TryGetUISpriteForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null)
{ {
string path = GetBindingControlPath(reference, compositePartName, device); string path = GetBindingControlPath(reference, compositePartName, device);
return TryGetUISpriteForActionPath(path, device, out sprite, db); return TryGetUISpriteForActionPath(path, device, out sprite, db);
} }
/// <summary>
/// 根据控制路径尝试获取 TextMeshPro 标签
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <param name="tag">输出的 TMP 标签</param>
/// <param name="displayFallback">显示回退文本</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetTMPTagForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) public static bool TryGetTMPTagForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null)
{ {
tag = null; tag = null;
@ -60,6 +98,14 @@ public static class GlyphService
return true; return true;
} }
/// <summary>
/// 根据控制路径尝试获取 UI Sprite
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <param name="sprite">输出的 Sprite</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
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; sprite = null;
@ -70,6 +116,9 @@ public static class GlyphService
} }
/// <summary>
/// 获取输入操作的绑定控制
/// </summary>
static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{ {
if (action == null) return default; if (action == null) return default;
@ -85,7 +134,7 @@ public static class GlyphService
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
} }
// Replace LINQ Any() to avoid delegate allocation // 替换 LINQ Any() 以避免委托分配
if (!string.IsNullOrEmpty(b.path) && ContainsAnyHint(b.path, hints)) return b; if (!string.IsNullOrEmpty(b.path) && ContainsAnyHint(b.path, hints)) return b;
if (!string.IsNullOrEmpty(b.effectivePath) && ContainsAnyHint(b.effectivePath, hints)) return b; if (!string.IsNullOrEmpty(b.effectivePath) && ContainsAnyHint(b.effectivePath, hints)) return b;
} }
@ -93,7 +142,10 @@ public static class GlyphService
return default; return default;
} }
// Helper method to avoid LINQ Any() allocation // 辅助方法,避免 LINQ Any() 的内存分配
/// <summary>
/// 检查路径是否包含任何提示字符串
/// </summary>
static bool ContainsAnyHint(string path, string[] hints) static bool ContainsAnyHint(string path, string[] hints)
{ {
for (int i = 0; i < hints.Length; i++) for (int i = 0; i < hints.Length; i++)
@ -104,6 +156,9 @@ public static class GlyphService
return false; return false;
} }
/// <summary>
/// 根据设备类型获取设备提示字符串数组
/// </summary>
static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat) static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat)
{ {
switch (cat) switch (cat)
@ -120,6 +175,13 @@ public static class GlyphService
} }
/// <summary>
/// 从输入操作获取显示名称
/// </summary>
/// <param name="action">输入操作</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <param name="deviceOverride">设备类型覆盖</param>
/// <returns>显示名称</returns>
public static string GetDisplayNameFromInputAction(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory deviceOverride = InputDeviceWatcher.InputDeviceCategory.Keyboard) public static string GetDisplayNameFromInputAction(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory deviceOverride = InputDeviceWatcher.InputDeviceCategory.Keyboard)
{ {
if (action == null) return string.Empty; if (action == null) return string.Empty;
@ -127,6 +189,11 @@ public static class GlyphService
return binding.ToDisplayString(); return binding.ToDisplayString();
} }
/// <summary>
/// 从控制路径获取显示名称
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <returns>显示名称</returns>
public static string GetDisplayNameFromControlPath(string controlPath) public static string GetDisplayNameFromControlPath(string controlPath)
{ {
if (string.IsNullOrEmpty(controlPath)) return string.Empty; if (string.IsNullOrEmpty(controlPath)) return string.Empty;

View File

@ -37,14 +37,14 @@ using Cysharp.Threading.Tasks;
private string cachedSavePath; private string cachedSavePath;
private Dictionary<string, (ActionMap map, ActionMap.Action action)> actionLookup = new Dictionary<string, (ActionMap, ActionMap.Action)>(); private Dictionary<string, (ActionMap map, ActionMap.Action action)> actionLookup = new Dictionary<string, (ActionMap, ActionMap.Action)>();
// Events to replace Rx.NET Subjects // 用于替代 Rx.NET Subjects 的事件
private event Action _onInputsInit; private event Action _onInputsInit;
public event Action OnInputsInit public event Action OnInputsInit
{ {
add add
{ {
_onInputsInit += value; _onInputsInit += value;
// Replay behavior: if already initialized, invoke immediately // 重放行为:如果已经初始化,立即调用
if (isInputsInitialized) if (isInputsInitialized)
{ {
value?.Invoke(); value?.Invoke();
@ -152,7 +152,7 @@ using Cysharp.Threading.Tasks;
rebindOperation?.Dispose(); rebindOperation?.Dispose();
rebindOperation = null; rebindOperation = null;
// Clear all event handlers // 清除所有事件处理器
_onInputsInit = null; _onInputsInit = null;
OnApply = null; OnApply = null;
OnRebindPrepare = null; OnRebindPrepare = null;
@ -163,19 +163,19 @@ using Cysharp.Threading.Tasks;
private void BuildActionMap() private void BuildActionMap()
{ {
// Pre-allocate with known capacity to avoid resizing // 预分配已知容量以避免调整大小
int mapCount = actions.actionMaps.Count; int mapCount = actions.actionMaps.Count;
actionMap.Clear(); actionMap.Clear();
actionLookup.Clear(); actionLookup.Clear();
// Estimate total action count for better allocation // 估算总操作数以便更好地分配内存
int estimatedActionCount = 0; int estimatedActionCount = 0;
foreach (var map in actions.actionMaps) foreach (var map in actions.actionMaps)
{ {
estimatedActionCount += map.actions.Count; estimatedActionCount += map.actions.Count;
} }
// Ensure capacity to avoid rehashing // 确保容量以避免重新哈希
if (actionMap.Count == 0) if (actionMap.Count == 0)
{ {
actionMap = new Dictionary<string, ActionMap>(mapCount); actionMap = new Dictionary<string, ActionMap>(mapCount);
@ -187,7 +187,7 @@ using Cysharp.Threading.Tasks;
var actionMapObj = new ActionMap(map); var actionMapObj = new ActionMap(map);
actionMap.Add(map.name, actionMapObj); actionMap.Add(map.name, actionMapObj);
// Build lookup dictionary for O(1) action access // 构建查找字典以实现 O(1) 操作访问
foreach (var actionPair in actionMapObj.actions) foreach (var actionPair in actionMapObj.actions)
{ {
actionLookup[actionPair.Key] = (actionMapObj, actionPair.Value); actionLookup[actionPair.Key] = (actionMapObj, actionPair.Value);
@ -367,6 +367,11 @@ using Cysharp.Threading.Tasks;
/* ---------------- Public API ---------------- */ /* ---------------- Public API ---------------- */
/// <summary>
/// 根据操作名称获取输入操作
/// </summary>
/// <param name="actionName">操作名称</param>
/// <returns>输入操作,未找到则返回 null</returns>
public static InputAction Action(string actionName) public static InputAction Action(string actionName)
{ {
var instance = Instance; var instance = Instance;
@ -381,12 +386,17 @@ using Cysharp.Threading.Tasks;
return null; return null;
} }
/// <summary>
/// 开始重新绑定指定的输入操作
/// </summary>
/// <param name="actionName">操作名称</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
public static void StartRebind(string actionName, string compositePartName = null) public static void StartRebind(string actionName, string compositePartName = null)
{ {
var action = Action(actionName); var action = Action(actionName);
if (action == null) return; if (action == null) return;
// decide bindingIndex & deviceMatch automatically // 自动决定 bindingIndex 和 deviceMatch
int bindingIndex = Instance.FindBestBindingIndexForKeyboard(action, compositePartName); int bindingIndex = Instance.FindBestBindingIndexForKeyboard(action, compositePartName);
if (bindingIndex < 0) if (bindingIndex < 0)
{ {
@ -403,15 +413,23 @@ using Cysharp.Threading.Tasks;
} }
} }
/// <summary>
/// 取消当前的重新绑定操作
/// </summary>
public static void CancelRebind() => Instance.rebindOperation?.Cancel(); public static void CancelRebind() => Instance.rebindOperation?.Cancel();
/// <summary>
/// 确认并应用准备好的重新绑定
/// </summary>
/// <param name="clearConflicts">是否清除冲突</param>
/// <returns>是否成功应用</returns>
public static async UniTask<bool> ConfirmApply(bool clearConflicts = true) public static async UniTask<bool> ConfirmApply(bool clearConflicts = true)
{ {
if (!Instance.isApplyPending) return false; if (!Instance.isApplyPending) return false;
try try
{ {
// Create a copy of the prepared rebinds before clearing // 在清除之前创建准备好的重绑定的副本
var appliedContexts = new HashSet<RebindContext>(Instance.preparedRebinds); var appliedContexts = new HashSet<RebindContext>(Instance.preparedRebinds);
foreach (var ctx in Instance.preparedRebinds) foreach (var ctx in Instance.preparedRebinds)
@ -454,11 +472,14 @@ using Cysharp.Threading.Tasks;
} }
} }
/// <summary>
/// 丢弃准备好的重新绑定
/// </summary>
public static void DiscardPrepared() public static void DiscardPrepared()
{ {
if (!Instance.isApplyPending) return; if (!Instance.isApplyPending) return;
// Create a copy of the prepared rebinds before clearing (for event notification) // 在清除之前创建准备好的重绑定的副本(用于事件通知)
var discardedContexts = new HashSet<RebindContext>(Instance.preparedRebinds); var discardedContexts = new HashSet<RebindContext>(Instance.preparedRebinds);
Instance.preparedRebinds.Clear(); Instance.preparedRebinds.Clear();
@ -583,7 +604,7 @@ using Cysharp.Threading.Tasks;
private void PrepareRebind(RebindContext context) private void PrepareRebind(RebindContext context)
{ {
// Remove existing rebind for same action/binding if exists // 如果存在相同操作/绑定的现有重绑定,则移除
preparedRebinds.Remove(context); preparedRebinds.Remove(context);
if (string.IsNullOrEmpty(context.overridePath)) if (string.IsNullOrEmpty(context.overridePath))
@ -626,6 +647,9 @@ using Cysharp.Threading.Tasks;
} }
} }
/// <summary>
/// 重置所有绑定到默认值
/// </summary>
public async UniTask ResetToDefaultAsync() public async UniTask ResetToDefaultAsync()
{ {
try try
@ -661,6 +685,12 @@ using Cysharp.Threading.Tasks;
} }
} }
/// <summary>
/// 获取指定操作的绑定路径
/// </summary>
/// <param name="actionName">操作名称</param>
/// <param name="bindingIndex">绑定索引</param>
/// <returns>绑定路径,未找到则返回 null</returns>
public static BindingPath GetBindingPath(string actionName, int bindingIndex = 0) public static BindingPath GetBindingPath(string actionName, int bindingIndex = 0)
{ {
var instance = Instance; var instance = Instance;
@ -678,7 +708,13 @@ using Cysharp.Threading.Tasks;
} }
// choose best binding index for keyboard; if compositePartName != null then look for part // 为键盘选择最佳绑定索引;如果 compositePartName != null 则查找部分
/// <summary>
/// 为键盘查找最佳的绑定索引
/// </summary>
/// <param name="action">输入操作</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <returns>绑定索引,未找到则返回 -1</returns>
public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null) public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null)
{ {
if (action == null) return -1; if (action == null) return -1;
@ -691,14 +727,14 @@ using Cysharp.Threading.Tasks;
{ {
var b = action.bindings[i]; var b = action.bindings[i];
// If searching for a specific composite part, skip non-matching bindings // 如果搜索特定的复合部分,跳过不匹配的绑定
if (searchingForCompositePart) if (searchingForCompositePart)
{ {
if (!b.isPartOfComposite) continue; if (!b.isPartOfComposite) continue;
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
} }
// Check if this binding is for keyboard // 检查此绑定是否用于键盘
bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) || bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) ||
(!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)); (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase));

View File

@ -37,15 +37,21 @@ public sealed class InputGlyphDatabase : ScriptableObject
// 当 FindEntryByControlPath 传空 path 时返回的占位 sprite // 当 FindEntryByControlPath 传空 path 时返回的占位 sprite
public Sprite placeholderSprite; public Sprite placeholderSprite;
// Cache for faster lookups // 用于更快查找的缓存
private Dictionary<string, DeviceGlyphTable> _tableCache; private Dictionary<string, DeviceGlyphTable> _tableCache;
private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache; private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache;
/// <summary>
/// 启用时构建缓存
/// </summary>
private void OnEnable() private void OnEnable()
{ {
BuildCache(); BuildCache();
} }
/// <summary>
/// 构建表和精灵的查找缓存
/// </summary>
private void BuildCache() private void BuildCache()
{ {
if (_tableCache == null) if (_tableCache == null)
@ -76,18 +82,23 @@ public sealed class InputGlyphDatabase : ScriptableObject
} }
} }
/// <summary>
/// 根据设备名称获取设备图标表
/// </summary>
/// <param name="deviceName">设备名称</param>
/// <returns>设备图标表</returns>
public DeviceGlyphTable GetTable(string deviceName) public DeviceGlyphTable GetTable(string deviceName)
{ {
if (string.IsNullOrEmpty(deviceName)) return null; if (string.IsNullOrEmpty(deviceName)) return null;
if (tables == null) return null; if (tables == null) return null;
// Ensure cache is built // 确保缓存已构建
if (_tableCache == null || _tableCache.Count == 0) if (_tableCache == null || _tableCache.Count == 0)
{ {
BuildCache(); BuildCache();
} }
// Use cache for O(1) lookup // 使用缓存进行 O(1) 查找
if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table)) if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table))
{ {
return table; return table;
@ -96,6 +107,11 @@ public sealed class InputGlyphDatabase : ScriptableObject
return null; return null;
} }
/// <summary>
/// 获取平台图标
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>平台图标 Sprite</returns>
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device) public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{ {
var table = GetTable(device); var table = GetTable(device);
@ -103,9 +119,14 @@ public sealed class InputGlyphDatabase : ScriptableObject
return table.platformIcons; return table.platformIcons;
} }
/// <summary>
/// 根据设备类型获取设备图标表
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>设备图标表</returns>
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
{ {
// Use constants to avoid string allocations // 使用常量避免字符串分配
string name; string name;
switch (device) switch (device)
{ {
@ -126,6 +147,12 @@ public sealed class InputGlyphDatabase : ScriptableObject
return GetTable(name); return GetTable(name);
} }
/// <summary>
/// 根据控制路径和设备类型查找 Sprite
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <returns>找到的 Sprite未找到则返回占位 Sprite</returns>
public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{ {
if (string.IsNullOrEmpty(controlPath)) if (string.IsNullOrEmpty(controlPath))
@ -133,7 +160,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
return placeholderSprite; return placeholderSprite;
} }
// Check cache first // 首先检查缓存
var cacheKey = (controlPath, device); var cacheKey = (controlPath, device);
if (_spriteCache != null && _spriteCache.TryGetValue(cacheKey, out var cachedSprite)) if (_spriteCache != null && _spriteCache.TryGetValue(cacheKey, out var cachedSprite))
{ {
@ -143,7 +170,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
var entry = FindEntryByControlPath(controlPath, device); var entry = FindEntryByControlPath(controlPath, device);
var sprite = entry?.Sprite ?? placeholderSprite; var sprite = entry?.Sprite ?? placeholderSprite;
// Cache the result (including null results to avoid repeated lookups) // 缓存结果(包括 null 结果以避免重复查找)
if (_spriteCache != null) if (_spriteCache != null)
{ {
_spriteCache[cacheKey] = sprite; _spriteCache[cacheKey] = sprite;
@ -152,6 +179,12 @@ public sealed class InputGlyphDatabase : ScriptableObject
return sprite; return sprite;
} }
/// <summary>
/// 根据控制路径和设备类型查找图标条目
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <returns>找到的图标条目,未找到则返回 null</returns>
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device) public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{ {
var t = GetTable(device); var t = GetTable(device);

View File

@ -17,7 +17,7 @@ public class InputGlyphDatabaseEditor : Editor
List<string> searchStrings = new List<string>(); List<string> searchStrings = new List<string>();
List<int> currentPages = new List<int>(); List<int> currentPages = new List<int>();
// per-table temporary fields for adding single entry (only sprite now) // 每个表的临时字段,用于添加单个条目(目前仅支持 sprite
List<Sprite> newEntrySprites = new List<Sprite>(); List<Sprite> newEntrySprites = new List<Sprite>();
const int itemsPerPage = 10; const int itemsPerPage = 10;
@ -174,7 +174,7 @@ public class InputGlyphDatabaseEditor : Editor
EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginVertical("box");
if (tabIndex == tablesProp.arraySize) if (tabIndex == tablesProp.arraySize)
{ {
// Settings // 设置
EditorGUILayout.LabelField("Settings", EditorStyles.boldLabel); EditorGUILayout.LabelField("Settings", EditorStyles.boldLabel);
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(placeholderSpriteProp, new GUIContent("Placeholder Sprite")); EditorGUILayout.PropertyField(placeholderSpriteProp, new GUIContent("Placeholder Sprite"));
@ -205,7 +205,7 @@ public class InputGlyphDatabaseEditor : Editor
EnsureEditorListsLength(); EnsureEditorListsLength();
// compute deviceName & runtime index for this table (used when deleting single entry) // 计算此表的 deviceName 和运行时索引(用于删除单个条目时)
var nameProp = tableProp.FindPropertyRelative("deviceName"); var nameProp = tableProp.FindPropertyRelative("deviceName");
string deviceName = nameProp != null ? nameProp.stringValue : ""; string deviceName = nameProp != null ? nameProp.stringValue : "";
int runtimeTableIndex = MapSerializedTableToRuntimeIndex(deviceName); int runtimeTableIndex = MapSerializedTableToRuntimeIndex(deviceName);
@ -356,10 +356,10 @@ public class InputGlyphDatabaseEditor : Editor
var eProp = entries.GetArrayElementAtIndex(i); var eProp = entries.GetArrayElementAtIndex(i);
if (eProp == null) continue; if (eProp == null) continue;
// display one entry with a small remove button on the right // 显示一个条目,右侧带有小的删除按钮
using (new EditorGUILayout.HorizontalScope("box")) using (new EditorGUILayout.HorizontalScope("box"))
{ {
// left: preview column // 左侧:预览列
using (new EditorGUILayout.VerticalScope(GUILayout.Width(80))) using (new EditorGUILayout.VerticalScope(GUILayout.Width(80)))
{ {
var spriteProp = eProp.FindPropertyRelative("Sprite"); var spriteProp = eProp.FindPropertyRelative("Sprite");
@ -385,14 +385,14 @@ public class InputGlyphDatabaseEditor : Editor
} }
} }
// middle: action column // 中间:操作列
EditorGUILayout.BeginVertical(); EditorGUILayout.BeginVertical();
var actionProp = eProp.FindPropertyRelative("action"); var actionProp = eProp.FindPropertyRelative("action");
EditorGUILayout.Space(2); EditorGUILayout.Space(2);
EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true)); EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true));
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
// right: small remove button // 右侧:小的删除按钮
GUILayout.Space(6); GUILayout.Space(6);
if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(24))) if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(24)))
{ {
@ -403,16 +403,16 @@ public class InputGlyphDatabaseEditor : Editor
if (EditorUtility.DisplayDialog("Remove Entry?", if (EditorUtility.DisplayDialog("Remove Entry?",
$"Remove entry '{(string.IsNullOrEmpty(spriteName) ? "<missing>" : spriteName)}' from table '{deviceName}'?", "Remove", "Cancel")) $"Remove entry '{(string.IsNullOrEmpty(spriteName) ? "<missing>" : spriteName)}' from table '{deviceName}'?", "Remove", "Cancel"))
{ {
// remove from serialized array // 从序列化数组中移除
var entriesProp = tableProp.FindPropertyRelative("entries"); var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp != null && i >= 0 && i < entriesProp.arraySize) if (entriesProp != null && i >= 0 && i < entriesProp.arraySize)
{ {
entriesProp.DeleteArrayElementAtIndex(i); entriesProp.DeleteArrayElementAtIndex(i);
// apply then remove from runtime to keep both in sync // 应用后从运行时移除以保持两者同步
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }
// remove from runtime list (db.tables) // 从运行时列表中移除db.tables
if (runtimeTableIndex >= 0 && db != null && db.tables != null && runtimeTableIndex < db.tables.Count) if (runtimeTableIndex >= 0 && db != null && db.tables != null && runtimeTableIndex < db.tables.Count)
{ {
var runtimeTable = db.tables[runtimeTableIndex]; var runtimeTable = db.tables[runtimeTableIndex];
@ -425,7 +425,7 @@ public class InputGlyphDatabaseEditor : Editor
EditorUtility.SetDirty(db); EditorUtility.SetDirty(db);
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
// reset paging and return to avoid continuing to iterate mutated serialized array // 重置分页并返回,避免继续迭代已变更的序列化数组
currentPages[tabIndex] = 0; currentPages[tabIndex] = 0;
return; return;
} }
@ -514,7 +514,7 @@ public class InputGlyphDatabaseEditor : Editor
if (spriteProp != null) spriteProp.objectReferenceValue = sprite; if (spriteProp != null) spriteProp.objectReferenceValue = sprite;
// leave action serialized as-is (most projects can't serialize InputAction directly here) // 保持 action 序列化不变(大多数项目无法在此处直接序列化 InputAction
if (actionProp != null) if (actionProp != null)
{ {
try try
@ -529,7 +529,7 @@ public class InputGlyphDatabaseEditor : Editor
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
// also add to runtime list // 同时添加到运行时列表
var nameProp = tableProp.FindPropertyRelative("deviceName"); var nameProp = tableProp.FindPropertyRelative("deviceName");
string deviceName = nameProp != null ? nameProp.stringValue : ""; string deviceName = nameProp != null ? nameProp.stringValue : "";
int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); int tableIndex = MapSerializedTableToRuntimeIndex(deviceName);

View File

@ -12,6 +12,9 @@ public sealed class InputGlyphImage : MonoBehaviour
private InputDeviceWatcher.InputDeviceCategory _cachedCategory; private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite; private Sprite _cachedSprite;
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable() void OnEnable()
{ {
if (targetImage == null) targetImage = GetComponent<Image>(); if (targetImage == null) targetImage = GetComponent<Image>();
@ -20,11 +23,17 @@ public sealed class InputGlyphImage : MonoBehaviour
UpdatePrompt(); UpdatePrompt();
} }
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable() void OnDisable()
{ {
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
} }
/// <summary>
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{ {
if (_cachedCategory != cat) if (_cachedCategory != cat)
@ -34,11 +43,14 @@ public sealed class InputGlyphImage : MonoBehaviour
} }
} }
/// <summary>
/// 更新输入提示图标,并根据配置控制目标对象的显示/隐藏
/// </summary>
void UpdatePrompt() void UpdatePrompt()
{ {
if (actionReference == null || actionReference.action == null || targetImage == null) return; if (actionReference == null || actionReference.action == null || targetImage == null) return;
// Use cached category instead of re-querying CurrentCategory // 使用缓存的设备类型,避免重复查询 CurrentCategory
if (GlyphService.TryGetUISpriteForActionPath(actionReference, "", _cachedCategory, out Sprite sprite)) if (GlyphService.TryGetUISpriteForActionPath(actionReference, "", _cachedCategory, out Sprite sprite))
{ {
if (_cachedSprite != sprite) if (_cachedSprite != sprite)

View File

@ -14,6 +14,9 @@ public sealed class InputGlyphText : MonoBehaviour
private InputDeviceWatcher.InputDeviceCategory _cachedCategory; private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private string _cachedFormattedText; private string _cachedFormattedText;
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable() void OnEnable()
{ {
if (textField == null) textField = GetComponent<TMP_Text>(); if (textField == null) textField = GetComponent<TMP_Text>();
@ -23,11 +26,17 @@ public sealed class InputGlyphText : MonoBehaviour
UpdatePrompt(); UpdatePrompt();
} }
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable() void OnDisable()
{ {
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
} }
/// <summary>
/// 设备类型变更时的回调,更新文本显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{ {
if (_cachedCategory != cat) if (_cachedCategory != cat)
@ -37,11 +46,14 @@ public sealed class InputGlyphText : MonoBehaviour
} }
} }
/// <summary>
/// 更新文本中的输入提示标签,使用 TextMeshPro 的 sprite 标签或文本回退
/// </summary>
void UpdatePrompt() void UpdatePrompt()
{ {
if (actionReference == null || actionReference.action == null || textField == null) return; if (actionReference == null || actionReference.action == null || textField == null) return;
// Use cached category instead of re-querying CurrentCategory // 使用缓存的设备类型,避免重复查询 CurrentCategory
if (GlyphService.TryGetTMPTagForActionPath(actionReference, "", _cachedCategory, out string tag, out string displayFallback)) if (GlyphService.TryGetTMPTagForActionPath(actionReference, "", _cachedCategory, out string tag, out string displayFallback))
{ {
string formattedText = Utility.Text.Format(_oldText, tag); string formattedText = Utility.Text.Format(_oldText, tag);

View File

@ -13,6 +13,9 @@ public sealed class InputGlyphUXButton : MonoBehaviour
private Sprite _cachedSprite; private Sprite _cachedSprite;
#if UNITY_EDITOR #if UNITY_EDITOR
/// <summary>
/// 编辑器验证,自动获取 UXButton 组件
/// </summary>
private void OnValidate() private void OnValidate()
{ {
if (button == null) if (button == null)
@ -22,6 +25,9 @@ public sealed class InputGlyphUXButton : MonoBehaviour
} }
#endif #endif
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable() void OnEnable()
{ {
if (button == null) button = GetComponent<UXButton>(); if (button == null) button = GetComponent<UXButton>();
@ -32,11 +38,17 @@ public sealed class InputGlyphUXButton : MonoBehaviour
UpdatePrompt(); UpdatePrompt();
} }
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable() void OnDisable()
{ {
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
} }
/// <summary>
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{ {
if (_cachedCategory != cat) if (_cachedCategory != cat)
@ -46,11 +58,14 @@ public sealed class InputGlyphUXButton : MonoBehaviour
} }
} }
/// <summary>
/// 更新按钮的输入提示图标
/// </summary>
void UpdatePrompt() void UpdatePrompt()
{ {
if (_actionReference == null || _actionReference.action == null || targetImage == null) return; if (_actionReference == null || _actionReference.action == null || targetImage == null) return;
// Use cached category instead of re-querying CurrentCategory // 使用缓存的设备类型,避免重复查询 CurrentCategory
if (GlyphService.TryGetUISpriteForActionPath(_actionReference, "", _cachedCategory, out Sprite sprite)) if (GlyphService.TryGetUISpriteForActionPath(_actionReference, "", _cachedCategory, out Sprite sprite))
{ {
if (_cachedSprite != sprite) if (_cachedSprite != sprite)

View File

@ -21,6 +21,9 @@ public class TestRebindScript : MonoBehaviour
[Header("Behavior")] [Tooltip("如果 true在 Prepare 后自动调用 ConfirmApply() 并保存;否则等待手动 ConfirmPrepared()/CancelPrepared()")] [Header("Behavior")] [Tooltip("如果 true在 Prepare 后自动调用 ConfirmApply() 并保存;否则等待手动 ConfirmPrepared()/CancelPrepared()")]
public bool autoConfirm = false; public bool autoConfirm = false;
/// <summary>
/// 启动时初始化并订阅事件
/// </summary>
private void Start() private void Start()
{ {
if (btn != null) btn.onClick.AddListener(OnBtnClicked); if (btn != null) btn.onClick.AddListener(OnBtnClicked);
@ -29,7 +32,7 @@ public class TestRebindScript : MonoBehaviour
if (InputBindingManager.Instance != null) if (InputBindingManager.Instance != null)
{ {
// Subscribe to events // 订阅事件
InputBindingManager.Instance.OnRebindPrepare += OnRebindPrepareHandler; InputBindingManager.Instance.OnRebindPrepare += OnRebindPrepareHandler;
InputBindingManager.Instance.OnApply += OnApplyHandler; InputBindingManager.Instance.OnApply += OnApplyHandler;
InputBindingManager.Instance.OnRebindEnd += OnRebindEndHandler; InputBindingManager.Instance.OnRebindEnd += OnRebindEndHandler;
@ -37,6 +40,9 @@ public class TestRebindScript : MonoBehaviour
} }
} }
/// <summary>
/// 禁用时取消订阅事件
/// </summary>
private void OnDisable() private void OnDisable()
{ {
if (btn != null) btn.onClick.RemoveListener(OnBtnClicked); if (btn != null) btn.onClick.RemoveListener(OnBtnClicked);
@ -51,6 +57,9 @@ public class TestRebindScript : MonoBehaviour
} }
} }
/// <summary>
/// 重新绑定准备完成的处理器
/// </summary>
private void OnRebindPrepareHandler(InputBindingManager.RebindContext ctx) private void OnRebindPrepareHandler(InputBindingManager.RebindContext ctx)
{ {
if (IsTargetContext(ctx)) if (IsTargetContext(ctx))
@ -61,11 +70,14 @@ public class TestRebindScript : MonoBehaviour
} }
} }
/// <summary>
/// 应用重新绑定的处理器
/// </summary>
private void OnApplyHandler(bool success, HashSet<InputBindingManager.RebindContext> appliedContexts) private void OnApplyHandler(bool success, HashSet<InputBindingManager.RebindContext> appliedContexts)
{ {
if (appliedContexts != null) if (appliedContexts != null)
{ {
// Only update if any of the applied/discarded contexts match this instance // 仅当任何应用/丢弃的上下文与此实例匹配时才更新
foreach (var ctx in appliedContexts) foreach (var ctx in appliedContexts)
{ {
if (IsTargetContext(ctx)) if (IsTargetContext(ctx))
@ -77,6 +89,9 @@ public class TestRebindScript : MonoBehaviour
} }
} }
/// <summary>
/// 重新绑定结束的处理器
/// </summary>
private void OnRebindEndHandler(bool success, InputBindingManager.RebindContext context) private void OnRebindEndHandler(bool success, InputBindingManager.RebindContext context)
{ {
if (IsTargetContext(context)) if (IsTargetContext(context))
@ -85,64 +100,84 @@ public class TestRebindScript : MonoBehaviour
} }
} }
/// <summary>
/// 重新绑定冲突的处理器
/// </summary>
private void OnRebindConflictHandler(InputBindingManager.RebindContext prepared, InputBindingManager.RebindContext conflict) private void OnRebindConflictHandler(InputBindingManager.RebindContext prepared, InputBindingManager.RebindContext conflict)
{ {
// Update if either the prepared or conflict context matches this instance // 如果准备的或冲突的上下文匹配此实例,则更新
if (IsTargetContext(prepared) || IsTargetContext(conflict)) if (IsTargetContext(prepared) || IsTargetContext(conflict))
{ {
UpdateBindingText(); UpdateBindingText();
} }
} }
/// <summary>
/// 设备变更的回调
/// </summary>
private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _) private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _)
{ {
UpdateBindingText(); UpdateBindingText();
} }
/// <summary>
/// 获取当前的输入操作
/// </summary>
private InputAction GetAction() private InputAction GetAction()
{ {
return InputBindingManager.Action(actionName); return InputBindingManager.Action(actionName);
} }
/// <summary>
/// 判断上下文是否为目标上下文
/// </summary>
private bool IsTargetContext(InputBindingManager.RebindContext ctx) private bool IsTargetContext(InputBindingManager.RebindContext ctx)
{ {
if (ctx == null || ctx.action == null) return false; if (ctx == null || ctx.action == null) return false;
var action = GetAction(); var action = GetAction();
if (action == null) return false; if (action == null) return false;
// Must match the action // 必须匹配操作
if (ctx.action != action) return false; if (ctx.action != action) return false;
// If we have a composite part specified, we need to match the binding index // 如果指定了复合部分,需要匹配绑定索引
if (!string.IsNullOrEmpty(compositePartName)) if (!string.IsNullOrEmpty(compositePartName))
{ {
// Get the binding at the context's index // 获取上下文索引处的绑定
if (ctx.bindingIndex < 0 || ctx.bindingIndex >= action.bindings.Count) if (ctx.bindingIndex < 0 || ctx.bindingIndex >= action.bindings.Count)
return false; return false;
var binding = action.bindings[ctx.bindingIndex]; var binding = action.bindings[ctx.bindingIndex];
// Check if the binding's name matches our composite part // 检查绑定的名称是否与我们的复合部分匹配
return string.Equals(binding.name, compositePartName, StringComparison.OrdinalIgnoreCase); return string.Equals(binding.name, compositePartName, StringComparison.OrdinalIgnoreCase);
} }
// If no composite part specified, just matching the action is enough // 如果未指定复合部分,仅匹配操作就足够了
return true; return true;
} }
/// <summary>
/// 按钮点击的回调
/// </summary>
private void OnBtnClicked() private void OnBtnClicked()
{ {
// Use manager API (we pass part name so manager can pick proper binding if needed) // 使用管理器 API我们传递部分名称以便管理器可以在需要时选择适当的绑定
InputBindingManager.StartRebind(actionName, string.IsNullOrEmpty(compositePartName) ? null : compositePartName); InputBindingManager.StartRebind(actionName, string.IsNullOrEmpty(compositePartName) ? null : compositePartName);
} }
/// <summary>
/// 确认准备好的重新绑定(公共方法)
/// </summary>
public async void ConfirmPrepared() public async void ConfirmPrepared()
{ {
bool ok = await ConfirmPreparedAsync(); bool ok = await ConfirmPreparedAsync();
if (!ok) Debug.LogError("ConfirmPrepared: apply failed."); if (!ok) Debug.LogError("ConfirmPrepared: apply failed.");
} }
/// <summary>
/// 确认准备好的重新绑定(异步)
/// </summary>
private async Task<bool> ConfirmPreparedAsync() private async Task<bool> ConfirmPreparedAsync()
{ {
try try
@ -157,12 +192,18 @@ public class TestRebindScript : MonoBehaviour
} }
} }
/// <summary>
/// 取消准备好的重新绑定
/// </summary>
public void CancelPrepared() public void CancelPrepared()
{ {
InputBindingManager.DiscardPrepared(); InputBindingManager.DiscardPrepared();
// UpdateBindingText will be called automatically via OnApply event // UpdateBindingText 将通过 OnApply 事件自动调用
} }
/// <summary>
/// 更新绑定文本和图标显示
/// </summary>
private void UpdateBindingText() private void UpdateBindingText()
{ {
var action = GetAction(); var action = GetAction();

8
Client/Assets/Test.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 120be4f03dcf52a4c827a44807402f14
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fece5a979e9f6ad489f077bad6fb358c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d7fad1223ee359940a43eccb1814103f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 66312dfa5dc7a594fa239d5e23983099
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0be83be7a9ef27d4a98952a568da1bc4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ca9087bc37b3f8d48b90396b79b283f3
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 200d1a5604e5e984e89a9bf9f4317d22
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3bd16e6ed56d37c448334a4fe1a0fec3
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9eb4e0f52fdd98d4dafef1d7c14a07fb
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -10,7 +10,7 @@
"BuildPipeline": "EditorSimulateBuildPipeline", "BuildPipeline": "EditorSimulateBuildPipeline",
"PackageName": "DefaultPackage", "PackageName": "DefaultPackage",
"PackageVersion": "Simulate", "PackageVersion": "Simulate",
"PackageNote": "2026/3/11 11:39:18", "PackageNote": "2026/3/11 11:40:21",
"AssetList": [ "AssetList": [
{ {
"Address": "Click", "Address": "Click",