11
This commit is contained in:
parent
97fa4770bf
commit
2b572c7989
@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
8
Client/Assets/Test.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 120be4f03dcf52a4c827a44807402f14
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameBase.dll.bytes
Normal file
BIN
Client/Assets/Test/GameBase.dll.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameBase.dll.bytes.meta
Normal file
7
Client/Assets/Test/GameBase.dll.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fece5a979e9f6ad489f077bad6fb358c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameBase.pdb.bytes
Normal file
BIN
Client/Assets/Test/GameBase.pdb.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameBase.pdb.bytes.meta
Normal file
7
Client/Assets/Test/GameBase.pdb.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7fad1223ee359940a43eccb1814103f
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameLib.dll.bytes
Normal file
BIN
Client/Assets/Test/GameLib.dll.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameLib.dll.bytes.meta
Normal file
7
Client/Assets/Test/GameLib.dll.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66312dfa5dc7a594fa239d5e23983099
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameLib.pdb.bytes
Normal file
BIN
Client/Assets/Test/GameLib.pdb.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameLib.pdb.bytes.meta
Normal file
7
Client/Assets/Test/GameLib.pdb.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0be83be7a9ef27d4a98952a568da1bc4
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameLogic.dll.bytes
Normal file
BIN
Client/Assets/Test/GameLogic.dll.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameLogic.dll.bytes.meta
Normal file
7
Client/Assets/Test/GameLogic.dll.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca9087bc37b3f8d48b90396b79b283f3
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameLogic.pdb.bytes
Normal file
BIN
Client/Assets/Test/GameLogic.pdb.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameLogic.pdb.bytes.meta
Normal file
7
Client/Assets/Test/GameLogic.pdb.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 200d1a5604e5e984e89a9bf9f4317d22
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameProto.dll.bytes
Normal file
BIN
Client/Assets/Test/GameProto.dll.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameProto.dll.bytes.meta
Normal file
7
Client/Assets/Test/GameProto.dll.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bd16e6ed56d37c448334a4fe1a0fec3
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Client/Assets/Test/GameProto.pdb.bytes
Normal file
BIN
Client/Assets/Test/GameProto.pdb.bytes
Normal file
Binary file not shown.
7
Client/Assets/Test/GameProto.pdb.bytes.meta
Normal file
7
Client/Assets/Test/GameProto.pdb.bytes.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9eb4e0f52fdd98d4dafef1d7c14a07fb
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@ -1 +1 @@
|
||||
00e06e6b
|
||||
034249e9
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user