250 lines
9.8 KiB
C#
250 lines
9.8 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.InputSystem;
|
||
|
||
namespace SLSUtilities.UI
|
||
{
|
||
/// <summary>
|
||
/// 从 <see cref="InputActionAsset"/> 读取当前按键绑定,
|
||
/// 将 Input Action 名映射为 InputGlyphs SpriteAsset 中的 glyph token 名。
|
||
/// <para>
|
||
/// 初始化后,<see cref="InputGlyphParser"/> 自动使用本解析器,
|
||
/// 使得本地化文本可以使用 Action 名作为 Token(如 <c>[Interact]</c>),
|
||
/// 无需硬编码具体按键名(如 <c>[R]</c>)。
|
||
/// 当玩家重新绑定按键时,重新调用 <see cref="Initialize"/> 即可自动更新图标。
|
||
/// </para>
|
||
/// </summary>
|
||
public static class InputBindingResolver
|
||
{
|
||
private static readonly Dictionary<string, string> ActionGlyphMap = new();
|
||
private static bool isInitialized;
|
||
|
||
/// <summary>是否已初始化。</summary>
|
||
public static bool IsInitialized => isInitialized;
|
||
|
||
// ================================================================
|
||
// Control Path → Glyph Token 特殊映射
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 无法通过通用规则推导的控制路径,手动映射到 glyph token。
|
||
/// </summary>
|
||
private static readonly Dictionary<string, string> ControlPathOverrides = new()
|
||
{
|
||
// 鼠标
|
||
{ "<Mouse>/leftButton", "LMB" },
|
||
{ "<Mouse>/rightButton", "RMB" },
|
||
{ "<Mouse>/middleButton", "MMB" },
|
||
{ "<Mouse>/forwardButton", "Mouse4" },
|
||
{ "<Mouse>/backButton", "Mouse5" },
|
||
|
||
// 修饰键(左右合并为同一图标)
|
||
{ "<Keyboard>/leftCtrl", "Ctrl" },
|
||
{ "<Keyboard>/rightCtrl", "Ctrl" },
|
||
{ "<Keyboard>/leftShift", "Shift" },
|
||
{ "<Keyboard>/rightShift", "Shift" },
|
||
{ "<Keyboard>/leftAlt", "Alt" },
|
||
{ "<Keyboard>/rightAlt", "Alt" },
|
||
|
||
// 名称差异较大的按键
|
||
{ "<Keyboard>/escape", "Esc" },
|
||
{ "<Keyboard>/delete", "Del" },
|
||
{ "<Keyboard>/insert", "Ins" },
|
||
{ "<Keyboard>/pageUp", "Pgup" },
|
||
{ "<Keyboard>/pageDown", "Pgdn" },
|
||
{ "<Keyboard>/printScreen","Prtsc" },
|
||
{ "<Keyboard>/scrollLock", "Scrlk" },
|
||
{ "<Keyboard>/capsLock", "Caps" },
|
||
{ "<Keyboard>/numpadEnter","Enter" },
|
||
{ "<Keyboard>/contextMenu","Context" },
|
||
{ "<Keyboard>/leftMeta", "Windows" },
|
||
{ "<Keyboard>/rightMeta", "Windows" },
|
||
{ "<Keyboard>/backquote", "Tilde" },
|
||
{ "<Keyboard>/minus", "Hyphen" },
|
||
{ "<Keyboard>/equals", "Equals" },
|
||
{ "<Keyboard>/leftBracket","BracketOpen" },
|
||
{ "<Keyboard>/rightBracket","BracketClose" },
|
||
{ "<Keyboard>/backslash", "BackwardSlash" },
|
||
{ "<Keyboard>/semicolon", "SemiColon" },
|
||
{ "<Keyboard>/quote", "Quote" },
|
||
{ "<Keyboard>/comma", "Comma" },
|
||
{ "<Keyboard>/period", "Dot" },
|
||
{ "<Keyboard>/slash", "ForwardSlash" },
|
||
{ "<Keyboard>/numpadPlus", "Plus" },
|
||
|
||
// 箭头键
|
||
{ "<Keyboard>/upArrow", "ArrowUp" },
|
||
{ "<Keyboard>/downArrow", "ArrowDown" },
|
||
{ "<Keyboard>/leftArrow", "ArrowLeft" },
|
||
{ "<Keyboard>/rightArrow", "ArrowRight" },
|
||
};
|
||
|
||
// ================================================================
|
||
// 初始化
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 从 InputActionAsset 构建 Action 名 → Glyph Token 的映射表。
|
||
/// 仅处理指定 controlScheme 的非 composite 绑定。
|
||
/// </summary>
|
||
/// <param name="inputActions">包含所有 ActionMap 的 InputActionAsset。</param>
|
||
/// <param name="controlScheme">
|
||
/// 要匹配的 Control Scheme 名称(如 "KeyboardMouse")。
|
||
/// 为 null 时取第一个匹配的绑定。
|
||
/// </param>
|
||
public static void Initialize(InputActionAsset inputActions, string controlScheme = "KeyboardMouse")
|
||
{
|
||
ActionGlyphMap.Clear();
|
||
|
||
if (inputActions == null)
|
||
{
|
||
Debug.LogWarning("[InputBindingResolver] InputActionAsset 为 null,无法初始化。");
|
||
isInitialized = false;
|
||
return;
|
||
}
|
||
|
||
foreach (InputActionMap map in inputActions.actionMaps)
|
||
{
|
||
foreach (InputAction action in map.actions)
|
||
{
|
||
string glyph = ResolveFirstBinding(action, controlScheme);
|
||
if (glyph != null)
|
||
{
|
||
ActionGlyphMap[action.name] = glyph;
|
||
}
|
||
}
|
||
}
|
||
|
||
ActionGlyphMap["M0"] = ActionGlyphMap["MainWeaponPrimary"];
|
||
ActionGlyphMap["M1"] = ActionGlyphMap["MainWeaponSecondary"];
|
||
ActionGlyphMap["MA"] = ActionGlyphMap["MainWeaponSpecialA"];
|
||
ActionGlyphMap["MB"] = ActionGlyphMap["MainWeaponSpecialB"];
|
||
ActionGlyphMap["MC"] = ActionGlyphMap["MainWeaponSpecialC"];
|
||
ActionGlyphMap["S0"] = ActionGlyphMap["SupportEquipment0"];
|
||
ActionGlyphMap["S1"] = ActionGlyphMap["SupportEquipment1"];
|
||
ActionGlyphMap["S2"] = ActionGlyphMap["SupportEquipment2"];
|
||
ActionGlyphMap["S3"] = ActionGlyphMap["SupportEquipment3"];
|
||
|
||
isInitialized = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 Token 解析为 glyph token 名。
|
||
/// 如果 token 匹配已注册的 Action 名,则返回对应按键的 glyph token;
|
||
/// 否则原样返回(可能是直接按键名,如 "LMB"、"Q")。
|
||
/// </summary>
|
||
public static string ResolveToken(string token)
|
||
{
|
||
if (isInitialized && ActionGlyphMap.TryGetValue(token, out string glyph))
|
||
{
|
||
return glyph;
|
||
}
|
||
return token;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定 Action 名当前绑定的 glyph token。
|
||
/// 未找到时返回 null。
|
||
/// </summary>
|
||
public static string GetGlyphForAction(string actionName)
|
||
{
|
||
if (isInitialized && ActionGlyphMap.TryGetValue(actionName, out string glyph))
|
||
{
|
||
return glyph;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ================================================================
|
||
// 内部实现
|
||
// ================================================================
|
||
|
||
/// <summary>
|
||
/// 取 Action 的第一个匹配 controlScheme 的非 composite 绑定,
|
||
/// 解析其控制路径为 glyph token。
|
||
/// </summary>
|
||
private static string ResolveFirstBinding(InputAction action, string controlScheme)
|
||
{
|
||
foreach (InputBinding binding in action.bindings)
|
||
{
|
||
if (binding.isComposite || binding.isPartOfComposite)
|
||
continue;
|
||
|
||
if (!IsBindingMatchingScheme(binding, controlScheme))
|
||
continue;
|
||
|
||
string path = binding.effectivePath;
|
||
string glyph = ControlPathToGlyphToken(path);
|
||
if (glyph != null)
|
||
return glyph;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查绑定是否属于指定的 Control Scheme。
|
||
/// controlScheme 为 null 时始终匹配。
|
||
/// </summary>
|
||
private static bool IsBindingMatchingScheme(InputBinding binding, string controlScheme)
|
||
{
|
||
if (string.IsNullOrEmpty(controlScheme))
|
||
return true;
|
||
|
||
// binding.groups 格式为 "KeyboardMouse" 或 "KeyboardMouse;Gamepad" 等
|
||
if (string.IsNullOrEmpty(binding.groups))
|
||
return false;
|
||
|
||
return binding.groups.Contains(controlScheme);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 Input System 控制路径转换为 InputGlyphs SpriteAsset 中的 token 名。
|
||
/// </summary>
|
||
private static string ControlPathToGlyphToken(string controlPath)
|
||
{
|
||
if (string.IsNullOrEmpty(controlPath))
|
||
return null;
|
||
|
||
// 优先查找特殊映射
|
||
if (ControlPathOverrides.TryGetValue(controlPath, out string token))
|
||
return token;
|
||
|
||
// 通用键盘规则
|
||
const string keyboardPrefix = "<Keyboard>/";
|
||
if (controlPath.StartsWith(keyboardPrefix))
|
||
{
|
||
string key = controlPath.Substring(keyboardPrefix.Length);
|
||
return KeyNameToGlyphToken(key);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 Input System 的键盘 key 名转换为 glyph token 名。
|
||
/// 规则与 <c>InputGlyphsSpriteAssetCreator.MapKeyboardName</c> 保持一致。
|
||
/// </summary>
|
||
private static string KeyNameToGlyphToken(string key)
|
||
{
|
||
if (string.IsNullOrEmpty(key))
|
||
return null;
|
||
|
||
// 单字母 → 大写
|
||
if (key.Length == 1 && char.IsLetter(key[0]))
|
||
return key.ToUpperInvariant();
|
||
|
||
// 单数字 → 原样
|
||
if (key.Length == 1 && char.IsDigit(key[0]))
|
||
return key;
|
||
|
||
// 功能键 f1-f12 → 全大写
|
||
if (key.Length >= 2 && key[0] == 'f' && int.TryParse(key.Substring(1), out _))
|
||
return key.ToUpperInvariant();
|
||
|
||
// 其余 → 首字母大写(space → Space, tab → Tab, enter → Enter, etc.)
|
||
return char.ToUpperInvariant(key[0]) + key.Substring(1);
|
||
}
|
||
}
|
||
}
|