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": {
"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
{
// Cached device hint arrays to avoid allocations
// 缓存的设备提示数组,避免内存分配
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 char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
/// <summary>
/// 获取输入图标数据库实例
/// </summary>
public static InputGlyphDatabase Database
{
get
@ -26,6 +29,13 @@ public static class GlyphService
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)
{
if (action == null) return string.Empty;
@ -33,18 +43,46 @@ public static class GlyphService
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)
{
string path = GetBindingControlPath(reference, compositePartName, device);
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)
{
string path = GetBindingControlPath(reference, compositePartName, device);
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)
{
tag = null;
@ -60,6 +98,14 @@ public static class GlyphService
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)
{
sprite = null;
@ -70,6 +116,9 @@ public static class GlyphService
}
/// <summary>
/// 获取输入操作的绑定控制
/// </summary>
static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{
if (action == null) return default;
@ -85,7 +134,7 @@ public static class GlyphService
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.effectivePath) && ContainsAnyHint(b.effectivePath, hints)) return b;
}
@ -93,7 +142,10 @@ public static class GlyphService
return default;
}
// Helper method to avoid LINQ Any() allocation
// 辅助方法,避免 LINQ Any() 的内存分配
/// <summary>
/// 检查路径是否包含任何提示字符串
/// </summary>
static bool ContainsAnyHint(string path, string[] hints)
{
for (int i = 0; i < hints.Length; i++)
@ -104,6 +156,9 @@ public static class GlyphService
return false;
}
/// <summary>
/// 根据设备类型获取设备提示字符串数组
/// </summary>
static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory 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)
{
if (action == null) return string.Empty;
@ -127,6 +189,11 @@ public static class GlyphService
return binding.ToDisplayString();
}
/// <summary>
/// 从控制路径获取显示名称
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <returns>显示名称</returns>
public static string GetDisplayNameFromControlPath(string controlPath)
{
if (string.IsNullOrEmpty(controlPath)) return string.Empty;

View File

@ -37,14 +37,14 @@ using Cysharp.Threading.Tasks;
private string cachedSavePath;
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;
public event Action OnInputsInit
{
add
{
_onInputsInit += value;
// Replay behavior: if already initialized, invoke immediately
// 重放行为:如果已经初始化,立即调用
if (isInputsInitialized)
{
value?.Invoke();
@ -152,7 +152,7 @@ using Cysharp.Threading.Tasks;
rebindOperation?.Dispose();
rebindOperation = null;
// Clear all event handlers
// 清除所有事件处理器
_onInputsInit = null;
OnApply = null;
OnRebindPrepare = null;
@ -163,19 +163,19 @@ using Cysharp.Threading.Tasks;
private void BuildActionMap()
{
// Pre-allocate with known capacity to avoid resizing
// 预分配已知容量以避免调整大小
int mapCount = actions.actionMaps.Count;
actionMap.Clear();
actionLookup.Clear();
// Estimate total action count for better allocation
// 估算总操作数以便更好地分配内存
int estimatedActionCount = 0;
foreach (var map in actions.actionMaps)
{
estimatedActionCount += map.actions.Count;
}
// Ensure capacity to avoid rehashing
// 确保容量以避免重新哈希
if (actionMap.Count == 0)
{
actionMap = new Dictionary<string, ActionMap>(mapCount);
@ -187,7 +187,7 @@ using Cysharp.Threading.Tasks;
var actionMapObj = new ActionMap(map);
actionMap.Add(map.name, actionMapObj);
// Build lookup dictionary for O(1) action access
// 构建查找字典以实现 O(1) 操作访问
foreach (var actionPair in actionMapObj.actions)
{
actionLookup[actionPair.Key] = (actionMapObj, actionPair.Value);
@ -367,6 +367,11 @@ using Cysharp.Threading.Tasks;
/* ---------------- Public API ---------------- */
/// <summary>
/// 根据操作名称获取输入操作
/// </summary>
/// <param name="actionName">操作名称</param>
/// <returns>输入操作,未找到则返回 null</returns>
public static InputAction Action(string actionName)
{
var instance = Instance;
@ -381,12 +386,17 @@ using Cysharp.Threading.Tasks;
return null;
}
/// <summary>
/// 开始重新绑定指定的输入操作
/// </summary>
/// <param name="actionName">操作名称</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
public static void StartRebind(string actionName, string compositePartName = null)
{
var action = Action(actionName);
if (action == null) return;
// decide bindingIndex & deviceMatch automatically
// 自动决定 bindingIndex 和 deviceMatch
int bindingIndex = Instance.FindBestBindingIndexForKeyboard(action, compositePartName);
if (bindingIndex < 0)
{
@ -403,15 +413,23 @@ using Cysharp.Threading.Tasks;
}
}
/// <summary>
/// 取消当前的重新绑定操作
/// </summary>
public static void CancelRebind() => Instance.rebindOperation?.Cancel();
/// <summary>
/// 确认并应用准备好的重新绑定
/// </summary>
/// <param name="clearConflicts">是否清除冲突</param>
/// <returns>是否成功应用</returns>
public static async UniTask<bool> ConfirmApply(bool clearConflicts = true)
{
if (!Instance.isApplyPending) return false;
try
{
// Create a copy of the prepared rebinds before clearing
// 在清除之前创建准备好的重绑定的副本
var appliedContexts = new HashSet<RebindContext>(Instance.preparedRebinds);
foreach (var ctx in Instance.preparedRebinds)
@ -454,11 +472,14 @@ using Cysharp.Threading.Tasks;
}
}
/// <summary>
/// 丢弃准备好的重新绑定
/// </summary>
public static void DiscardPrepared()
{
if (!Instance.isApplyPending) return;
// Create a copy of the prepared rebinds before clearing (for event notification)
// 在清除之前创建准备好的重绑定的副本(用于事件通知)
var discardedContexts = new HashSet<RebindContext>(Instance.preparedRebinds);
Instance.preparedRebinds.Clear();
@ -583,7 +604,7 @@ using Cysharp.Threading.Tasks;
private void PrepareRebind(RebindContext context)
{
// Remove existing rebind for same action/binding if exists
// 如果存在相同操作/绑定的现有重绑定,则移除
preparedRebinds.Remove(context);
if (string.IsNullOrEmpty(context.overridePath))
@ -626,6 +647,9 @@ using Cysharp.Threading.Tasks;
}
}
/// <summary>
/// 重置所有绑定到默认值
/// </summary>
public async UniTask ResetToDefaultAsync()
{
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)
{
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)
{
if (action == null) return -1;
@ -691,14 +727,14 @@ using Cysharp.Threading.Tasks;
{
var b = action.bindings[i];
// If searching for a specific composite part, skip non-matching bindings
// 如果搜索特定的复合部分,跳过不匹配的绑定
if (searchingForCompositePart)
{
if (!b.isPartOfComposite) 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)) ||
(!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
public Sprite placeholderSprite;
// Cache for faster lookups
// 用于更快查找的缓存
private Dictionary<string, DeviceGlyphTable> _tableCache;
private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache;
/// <summary>
/// 启用时构建缓存
/// </summary>
private void OnEnable()
{
BuildCache();
}
/// <summary>
/// 构建表和精灵的查找缓存
/// </summary>
private void BuildCache()
{
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)
{
if (string.IsNullOrEmpty(deviceName)) return null;
if (tables == null) return null;
// Ensure cache is built
// 确保缓存已构建
if (_tableCache == null || _tableCache.Count == 0)
{
BuildCache();
}
// Use cache for O(1) lookup
// 使用缓存进行 O(1) 查找
if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table))
{
return table;
@ -96,6 +107,11 @@ public sealed class InputGlyphDatabase : ScriptableObject
return null;
}
/// <summary>
/// 获取平台图标
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>平台图标 Sprite</returns>
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{
var table = GetTable(device);
@ -103,9 +119,14 @@ public sealed class InputGlyphDatabase : ScriptableObject
return table.platformIcons;
}
/// <summary>
/// 根据设备类型获取设备图标表
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>设备图标表</returns>
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
{
// Use constants to avoid string allocations
// 使用常量避免字符串分配
string name;
switch (device)
{
@ -126,6 +147,12 @@ public sealed class InputGlyphDatabase : ScriptableObject
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)
{
if (string.IsNullOrEmpty(controlPath))
@ -133,7 +160,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
return placeholderSprite;
}
// Check cache first
// 首先检查缓存
var cacheKey = (controlPath, device);
if (_spriteCache != null && _spriteCache.TryGetValue(cacheKey, out var cachedSprite))
{
@ -143,7 +170,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
var entry = FindEntryByControlPath(controlPath, device);
var sprite = entry?.Sprite ?? placeholderSprite;
// Cache the result (including null results to avoid repeated lookups)
// 缓存结果(包括 null 结果以避免重复查找)
if (_spriteCache != null)
{
_spriteCache[cacheKey] = sprite;
@ -152,6 +179,12 @@ public sealed class InputGlyphDatabase : ScriptableObject
return sprite;
}
/// <summary>
/// 根据控制路径和设备类型查找图标条目
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <returns>找到的图标条目,未找到则返回 null</returns>
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{
var t = GetTable(device);

View File

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

View File

@ -12,6 +12,9 @@ public sealed class InputGlyphImage : MonoBehaviour
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite;
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
{
if (targetImage == null) targetImage = GetComponent<Image>();
@ -20,11 +23,17 @@ public sealed class InputGlyphImage : MonoBehaviour
UpdatePrompt();
}
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
}
/// <summary>
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
@ -34,11 +43,14 @@ public sealed class InputGlyphImage : MonoBehaviour
}
}
/// <summary>
/// 更新输入提示图标,并根据配置控制目标对象的显示/隐藏
/// </summary>
void UpdatePrompt()
{
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 (_cachedSprite != sprite)

View File

@ -14,6 +14,9 @@ public sealed class InputGlyphText : MonoBehaviour
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private string _cachedFormattedText;
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
{
if (textField == null) textField = GetComponent<TMP_Text>();
@ -23,11 +26,17 @@ public sealed class InputGlyphText : MonoBehaviour
UpdatePrompt();
}
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
}
/// <summary>
/// 设备类型变更时的回调,更新文本显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
@ -37,11 +46,14 @@ public sealed class InputGlyphText : MonoBehaviour
}
}
/// <summary>
/// 更新文本中的输入提示标签,使用 TextMeshPro 的 sprite 标签或文本回退
/// </summary>
void UpdatePrompt()
{
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))
{
string formattedText = Utility.Text.Format(_oldText, tag);

View File

@ -13,6 +13,9 @@ public sealed class InputGlyphUXButton : MonoBehaviour
private Sprite _cachedSprite;
#if UNITY_EDITOR
/// <summary>
/// 编辑器验证,自动获取 UXButton 组件
/// </summary>
private void OnValidate()
{
if (button == null)
@ -22,6 +25,9 @@ public sealed class InputGlyphUXButton : MonoBehaviour
}
#endif
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
{
if (button == null) button = GetComponent<UXButton>();
@ -32,11 +38,17 @@ public sealed class InputGlyphUXButton : MonoBehaviour
UpdatePrompt();
}
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
}
/// <summary>
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
@ -46,11 +58,14 @@ public sealed class InputGlyphUXButton : MonoBehaviour
}
}
/// <summary>
/// 更新按钮的输入提示图标
/// </summary>
void UpdatePrompt()
{
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 (_cachedSprite != sprite)

View File

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