更新详见各模块Package

This commit is contained in:
陈思海 2026-03-20 16:50:52 +08:00
parent 93023a995d
commit a3e1cf0407
31 changed files with 55 additions and 4561 deletions

View File

@ -190,7 +190,7 @@ GameObject:
- component: {fileID: 1839643559262572090}
- component: {fileID: 8270483239104722155}
- component: {fileID: 3099109932356522100}
- component: {fileID: 7001433845748019590}
- component: {fileID: 5439601954591314253}
m_Layer: 5
m_Name: UILoadUpdateWindow
m_TagString: Untagged
@ -295,7 +295,7 @@ CanvasGroup:
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!114 &7001433845748019590
--- !u!114 &5439601954591314253
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -304,15 +304,15 @@ MonoBehaviour:
m_GameObject: {fileID: 526598954257632073}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1ad79303854072f4798edbea92187a26, type: 3}
m_Script: {fileID: 11500000, guid: 85e4995cea5647b46995bb92b329207d, type: 3}
m_Name:
m_EditorClassIdentifier:
AnimationNodes: []
openClip: Open
closeClip: Close
references:
version: 2
RefIds: []
_defaultSelectable: {fileID: 8242433937099588481}
_explicitSelectables: []
_rememberLastSelection: 1
_requireSelectionWhenGamepad: 1
_blockLowerScopes: 1
_autoSelectFirstAvailable: 1
--- !u!1 &541431694581587512
GameObject:
m_ObjectHideFlags: 0
@ -464,9 +464,9 @@ MonoBehaviour:
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_HighlightedColor: {r: 1, g: 0, b: 0, a: 1}
m_PressedColor: {r: 0.9866247, g: 1, b: 0, a: 1}
m_SelectedColor: {r: 0, g: 1, b: 0.25335073, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
@ -683,9 +683,9 @@ MonoBehaviour:
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_HighlightedColor: {r: 1, g: 0, b: 0, a: 1}
m_PressedColor: {r: 0.9866247, g: 1, b: 0, a: 1}
m_SelectedColor: {r: 0, g: 1, b: 0.25335073, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
@ -917,7 +917,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 0.547935, b: 0.109803915, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@ -956,9 +956,9 @@ MonoBehaviour:
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_HighlightedColor: {r: 1, g: 0, b: 0, a: 1}
m_PressedColor: {r: 0.9866247, g: 1, b: 0, a: 1}
m_SelectedColor: {r: 0, g: 1, b: 0.25335073, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
@ -1342,9 +1342,9 @@ MonoBehaviour:
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_HighlightedColor: {r: 1, g: 0, b: 0, a: 1}
m_PressedColor: {r: 0.9866247, g: 1, b: 0, a: 1}
m_SelectedColor: {r: 0, g: 1, b: 0.25335073, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
@ -1695,7 +1695,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: 0
m_text: 4444
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
@ -1704,8 +1704,8 @@ MonoBehaviour:
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
rgba: 4278190080
m_fontColor: {r: 0, g: 0, b: 0, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:

View File

@ -234,9 +234,9 @@ MonoBehaviour:
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_HighlightedColor: {r: 1, g: 0, b: 0, a: 1}
m_PressedColor: {r: 1, g: 0.99862754, b: 0, a: 1}
m_SelectedColor: {r: 0, g: 1, b: 0.0013723373, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
@ -495,6 +495,7 @@ GameObject:
- component: {fileID: 3538879237153299783}
- component: {fileID: 2650058657611945293}
- component: {fileID: 7349756297182561896}
- component: {fileID: 3817868405936479492}
m_Layer: 5
m_Name: UILogicTestAlert
m_TagString: Untagged
@ -578,6 +579,24 @@ MonoBehaviour:
m_EditorClassIdentifier:
mBtnEscTest: {fileID: 8761522827132804065}
mBtnGTest: {fileID: 6868640316405957368}
--- !u!114 &3817868405936479492
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3802536979087650729}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 85e4995cea5647b46995bb92b329207d, type: 3}
m_Name:
m_EditorClassIdentifier:
_defaultSelectable: {fileID: 8761522827132804065}
_explicitSelectables: []
_rememberLastSelection: 1
_requireSelectionWhenGamepad: 1
_blockLowerScopes: 1
_autoSelectFirstAvailable: 1
--- !u!1 &6865905647236409969
GameObject:
m_ObjectHideFlags: 0
@ -678,9 +697,9 @@ MonoBehaviour:
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_HighlightedColor: {r: 1, g: 0, b: 0, a: 1}
m_PressedColor: {r: 1, g: 0.99862754, b: 0, a: 1}
m_SelectedColor: {r: 0, g: 1, b: 0.0013723373, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1

View File

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

View File

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

View File

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

View File

@ -1,366 +0,0 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public static class GlyphService
{
private static readonly string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" };
private static readonly string[] XboxGroupHints = { "xbox", "xinput", "gamepad", "controller" };
private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" };
private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" };
private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
private static readonly Dictionary<string, string> DisplayNameCache = new(StringComparer.Ordinal);
private static readonly Dictionary<int, string> SpriteTagCache = new();
private static InputGlyphDatabase _database;
public static InputGlyphDatabase Database
{
get
{
if (_database == null)
{
_database = Resources.Load<InputGlyphDatabase>("InputGlyphDatabase");
}
return _database;
}
}
public static string GetBindingControlPath(
InputAction action,
string compositePartName = null,
InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{
return TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding)
? GetEffectivePath(binding)
: string.Empty;
}
public static string GetBindingControlPath(
InputActionReference actionReference,
string compositePartName = null,
InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{
return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride);
}
public static bool TryGetTMPTagForActionPath(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out string tag,
out string displayFallback,
InputGlyphDatabase db = null)
{
string controlPath = GetBindingControlPath(action, compositePartName, device);
return TryGetTMPTagForActionPath(controlPath, device, out tag, out displayFallback, db);
}
public static bool TryGetTMPTagForActionPath(
InputActionReference actionReference,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out string tag,
out string displayFallback,
InputGlyphDatabase db = null)
{
return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db);
}
public static bool TryGetUISpriteForActionPath(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
string controlPath = GetBindingControlPath(action, compositePartName, device);
return TryGetUISpriteForActionPath(controlPath, device, out sprite, db);
}
public static bool TryGetUISpriteForActionPath(
InputActionReference actionReference,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
return TryGetUISpriteForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out sprite, db);
}
public static bool TryGetTMPTagForActionPath(
string controlPath,
InputDeviceWatcher.InputDeviceCategory device,
out string tag,
out string displayFallback,
InputGlyphDatabase db = null)
{
displayFallback = GetDisplayNameFromControlPath(controlPath);
tag = null;
if (!TryGetUISpriteForActionPath(controlPath, device, out Sprite sprite, db))
{
return false;
}
tag = GetSpriteTag(sprite);
return true;
}
public static bool TryGetUISpriteForActionPath(
string controlPath,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
sprite = null;
db ??= Database;
return db != null && db.TryGetSprite(controlPath, device, out sprite);
}
public static string GetDisplayNameFromInputAction(
InputAction action,
string compositePartName = null,
InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{
if (!TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding))
{
return string.Empty;
}
string display = binding.ToDisplayString();
return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display;
}
public static string GetDisplayNameFromControlPath(string controlPath)
{
if (string.IsNullOrWhiteSpace(controlPath))
{
return string.Empty;
}
if (DisplayNameCache.TryGetValue(controlPath, out string cachedDisplayName))
{
return cachedDisplayName;
}
string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice);
if (!string.IsNullOrWhiteSpace(humanReadable))
{
DisplayNameCache[controlPath] = humanReadable;
return humanReadable;
}
int separatorIndex = controlPath.LastIndexOf('/');
string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars);
DisplayNameCache[controlPath] = last;
return last;
}
public static bool TryGetBindingControl(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory? deviceOverride,
out InputBinding binding)
{
binding = default;
if (action == null)
{
return false;
}
InputDeviceWatcher.InputDeviceCategory category = deviceOverride ?? InputDeviceWatcher.CurrentCategory;
int bestScore = int.MinValue;
bool requireCompositePart = !string.IsNullOrEmpty(compositePartName);
for (int i = 0; i < action.bindings.Count; i++)
{
InputBinding candidate = action.bindings[i];
if (candidate.isComposite)
{
continue;
}
if (requireCompositePart)
{
if (!candidate.isPartOfComposite || !string.Equals(candidate.name, compositePartName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
}
else if (candidate.isPartOfComposite)
{
continue;
}
string path = GetEffectivePath(candidate);
if (string.IsNullOrWhiteSpace(path))
{
continue;
}
int score = ScoreBinding(candidate, category);
if (score > bestScore)
{
bestScore = score;
binding = candidate;
}
}
return bestScore > int.MinValue;
}
private static int ScoreBinding(InputBinding binding, InputDeviceWatcher.InputDeviceCategory category)
{
int score = 0;
string path = GetEffectivePath(binding);
if (MatchesBindingGroups(binding.groups, category))
{
score += 100;
}
else if (!string.IsNullOrWhiteSpace(binding.groups))
{
score -= 20;
}
if (MatchesControlPath(path, category))
{
score += 60;
}
if (!binding.isPartOfComposite)
{
score += 5;
}
return score;
}
private static bool MatchesBindingGroups(string groups, InputDeviceWatcher.InputDeviceCategory category)
{
if (string.IsNullOrWhiteSpace(groups))
{
return false;
}
string[] hints = GetGroupHints(category);
int tokenStart = 0;
for (int i = 0; i <= groups.Length; i++)
{
if (i < groups.Length && groups[i] != InputBinding.Separator)
{
continue;
}
int tokenLength = i - tokenStart;
while (tokenLength > 0 && char.IsWhiteSpace(groups[tokenStart]))
{
tokenStart++;
tokenLength--;
}
while (tokenLength > 0 && char.IsWhiteSpace(groups[tokenStart + tokenLength - 1]))
{
tokenLength--;
}
if (tokenLength > 0)
{
string token = groups.Substring(tokenStart, tokenLength);
if (ContainsAny(token, hints))
{
return true;
}
}
tokenStart = i + 1;
}
return false;
}
private static string GetSpriteTag(Sprite sprite)
{
if (sprite == null)
{
return null;
}
int instanceId = sprite.GetInstanceID();
if (SpriteTagCache.TryGetValue(instanceId, out string cachedTag))
{
return cachedTag;
}
cachedTag = $"<sprite name=\"{sprite.name}\">";
SpriteTagCache[instanceId] = cachedTag;
return cachedTag;
}
private static bool ContainsAny(string source, string[] hints)
{
if (string.IsNullOrWhiteSpace(source) || hints == null)
{
return false;
}
for (int i = 0; i < hints.Length; i++)
{
if (source.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private static bool StartsWithDevice(string path, string deviceTag)
{
return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase);
}
private static string[] GetGroupHints(InputDeviceWatcher.InputDeviceCategory category)
{
switch (category)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardGroupHints;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxGroupHints;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationGroupHints;
default:
return OtherGamepadGroupHints;
}
}
private static string GetEffectivePath(InputBinding binding)
{
return string.IsNullOrWhiteSpace(binding.effectivePath) ? binding.path : binding.effectivePath;
}
private static bool MatchesControlPath(string path, InputDeviceWatcher.InputDeviceCategory category)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
switch (category)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return StartsWithDevice(path, "<Keyboard>") || StartsWithDevice(path, "<Mouse>");
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, XboxGroupHints);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, PlayStationGroupHints);
default:
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, OtherGamepadGroupHints);
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1cc7ec0447a544ac984f3aac7a7b71d4
timeCreated: 1764917633

View File

@ -1,300 +0,0 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// 输入读取工具。
/// 负责运行时输入轮询、单次触发和切换态管理,
/// </summary>
public static class InputActionReader
{
/// <summary>
/// 用于标识一次输入读取上下文。
/// 同一个 Action 在不同 owner 或 key 下会拥有独立的按下状态。
/// </summary>
private readonly struct InputReadKey : IEquatable<InputReadKey>
{
public readonly string ActionName;
public readonly int OwnerId;
public readonly string OwnerKey;
/// <summary>
/// 使用实例 ID 作为拥有者标识,适合 Unity 对象。
/// </summary>
public InputReadKey(string actionName, int ownerId)
{
ActionName = actionName ?? string.Empty;
OwnerId = ownerId;
OwnerKey = string.Empty;
}
/// <summary>
/// 使用字符串作为拥有者标识,适合外部系统或手动传入的 key。
/// </summary>
public InputReadKey(string actionName, string ownerKey)
{
ActionName = actionName ?? string.Empty;
OwnerId = 0;
OwnerKey = ownerKey ?? string.Empty;
}
public bool Equals(InputReadKey other)
{
return OwnerId == other.OwnerId
&& string.Equals(ActionName, other.ActionName, StringComparison.Ordinal)
&& string.Equals(OwnerKey, other.OwnerKey, StringComparison.Ordinal);
}
public override bool Equals(object obj)
{
return obj is InputReadKey other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = 17;
hashCode = (hashCode * 31) + OwnerId;
hashCode = (hashCode * 31) + StringComparer.Ordinal.GetHashCode(ActionName);
hashCode = (hashCode * 31) + StringComparer.Ordinal.GetHashCode(OwnerKey);
return hashCode;
}
}
}
// 记录“本次按下已消费”的键,用于 Once 语义。
private static readonly HashSet<InputReadKey> PressedKeys = new();
// 记录当前处于开启状态的切换键。
private static readonly HashSet<InputReadKey> ToggledKeys = new();
/// <summary>
/// 直接读取指定 Action 的值。
/// </summary>
public static T ReadValue<T>(string actionName) where T : struct
{
return ResolveAction(actionName).ReadValue<T>();
}
/// <summary>
/// 以 object 形式读取指定 Action 的值。
/// </summary>
public static object ReadValue(string actionName)
{
return ResolveAction(actionName).ReadValueAsObject();
}
/// <summary>
/// 仅在 Action 处于按下状态时读取值。
/// </summary>
public static bool TryReadValue<T>(string actionName, out T value) where T : struct
{
InputAction inputAction = ResolveAction(actionName);
if (inputAction.IsPressed())
{
value = inputAction.ReadValue<T>();
return true;
}
value = default;
return false;
}
/// <summary>
/// 仅在 Action 处于按下状态时以 object 形式读取值。
/// </summary>
public static bool TryReadValue(string actionName, out object value)
{
InputAction inputAction = ResolveAction(actionName);
if (inputAction.IsPressed())
{
value = inputAction.ReadValueAsObject();
return true;
}
value = default;
return false;
}
/// <summary>
/// 只在本次按下的第一帧返回 true并输出当前值。
/// owner 用来隔离不同对象的读取状态。
/// </summary>
public static bool TryReadValueOnce<T>(UnityEngine.Object owner, string actionName, out T value) where T : struct
{
if (owner == null)
{
value = default;
return false;
}
return TryReadValueOnceInternal(new InputReadKey(actionName, owner.GetInstanceID()), actionName, out value);
}
/// <summary>
/// 读取按钮型 Action。
/// 非按钮类型会直接抛出异常,避免误用。
/// </summary>
public static bool ReadButton(string actionName)
{
InputAction inputAction = ResolveAction(actionName);
if (inputAction.type == InputActionType.Button)
{
return Convert.ToBoolean(inputAction.ReadValueAsObject());
}
throw new NotSupportedException("[InputActionReader] The Input Action must be a button type.");
}
/// <summary>
/// 对 Unity 对象做一次性按钮读取。
/// </summary>
public static bool ReadButtonOnce(UnityEngine.Object owner, string actionName)
{
return owner != null && ReadButtonOnce(owner.GetInstanceID(), actionName);
}
/// <summary>
/// 对实例 ID 做一次性按钮读取。
/// </summary>
public static bool ReadButtonOnce(int instanceID, string actionName)
{
return ReadButtonOnceInternal(new InputReadKey(actionName, instanceID), actionName);
}
/// <summary>
/// 对字符串 key 做一次性按钮读取。
/// </summary>
public static bool ReadButtonOnce(string key, string actionName)
{
return ReadButtonOnceInternal(new InputReadKey(actionName, key), actionName);
}
/// <summary>
/// 对 Unity 对象读取按钮切换态。
/// 每次新的按下沿会在开/关之间切换。
/// </summary>
public static bool ReadButtonToggle(UnityEngine.Object owner, string actionName)
{
return owner != null && ReadButtonToggle(owner.GetInstanceID(), actionName);
}
/// <summary>
/// 对实例 ID 读取按钮切换态。
/// </summary>
public static bool ReadButtonToggle(int instanceID, string actionName)
{
return ReadButtonToggleInternal(new InputReadKey(actionName, instanceID), actionName);
}
/// <summary>
/// 对字符串 key 读取按钮切换态。
/// </summary>
public static bool ReadButtonToggle(string key, string actionName)
{
return ReadButtonToggleInternal(new InputReadKey(actionName, key), actionName);
}
/// <summary>
/// 重置指定 key 的切换态。
/// </summary>
public static void ResetToggledButton(string key, string actionName)
{
ToggledKeys.Remove(new InputReadKey(actionName, key));
}
/// <summary>
/// 重置某个 Action 名称下的所有切换态。
/// </summary>
public static void ResetToggledButton(string actionName)
{
if (string.IsNullOrEmpty(actionName) || ToggledKeys.Count == 0)
{
return;
}
InputReadKey[] snapshot = new InputReadKey[ToggledKeys.Count];
ToggledKeys.CopyTo(snapshot);
for (int i = 0; i < snapshot.Length; i++)
{
if (string.Equals(snapshot[i].ActionName, actionName, StringComparison.Ordinal))
{
ToggledKeys.Remove(snapshot[i]);
}
}
}
/// <summary>
/// 清空全部切换态缓存。
/// </summary>
public static void ResetToggledButtons()
{
ToggledKeys.Clear();
}
/// <summary>
/// 解析 Action找不到时立即抛错避免静默失败。
/// </summary>
private static InputAction ResolveAction(string actionName)
{
return InputBindingManager.Action(actionName)
?? throw new InvalidOperationException($"[InputActionReader] Action '{actionName}' is not available.");
}
/// <summary>
/// 内部的单次值读取逻辑。
/// 当按键抬起时,会清理 PressedKeys 中对应状态。
/// </summary>
private static bool TryReadValueOnceInternal<T>(InputReadKey readKey, string actionName, out T value) where T : struct
{
InputAction inputAction = ResolveAction(actionName);
if (inputAction.IsPressed())
{
if (PressedKeys.Add(readKey))
{
value = inputAction.ReadValue<T>();
return true;
}
}
else
{
PressedKeys.Remove(readKey);
}
value = default;
return false;
}
/// <summary>
/// 内部的按钮单次触发逻辑。
/// 只有第一次按下返回 true持续按住不会重复触发。
/// </summary>
private static bool ReadButtonOnceInternal(InputReadKey readKey, string actionName)
{
if (ReadButton(actionName))
{
return PressedKeys.Add(readKey);
}
PressedKeys.Remove(readKey);
return false;
}
/// <summary>
/// 内部的按钮切换逻辑。
/// 基于 Once 触发,在每次新的按下沿时切换状态。
/// </summary>
private static bool ReadButtonToggleInternal(InputReadKey readKey, string actionName)
{
if (ReadButtonOnceInternal(readKey, actionName))
{
if (!ToggledKeys.Add(readKey))
{
ToggledKeys.Remove(readKey);
}
}
return ToggledKeys.Contains(readKey);
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b9368556ed4729ae618e0a19d3a7925b
timeCreated: 1773811724

View File

@ -1,844 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using AlicizaX;
using Cysharp.Threading.Tasks;
public class InputBindingManager : MonoSingleton<InputBindingManager>
{
public const string NULL_BINDING = "__NULL__";
private const string KEYBOARD_DEVICE = "<Keyboard>";
private const string MOUSE_DELTA = "<Mouse>/delta";
private const string MOUSE_SCROLL = "<Mouse>/scroll";
private const string MOUSE_SCROLL_X = "<Mouse>/scroll/x";
private const string MOUSE_SCROLL_Y = "<Mouse>/scroll/y";
private const string KEYBOARD_ESCAPE = "<Keyboard>/escape";
[Tooltip("InputActionAsset to manage")]
public InputActionAsset actions;
private const string FILE_NAME = "input_bindings.json";
public bool debugMode = false;
internal InputActionRebindingExtensions.RebindingOperation rebindOperation;
private bool isApplyPending = false;
private string defaultBindingsJson = string.Empty;
private string cachedSavePath;
private readonly Dictionary<string, ActionMap> actionMap = new(StringComparer.Ordinal);
private readonly HashSet<RebindContext> preparedRebinds = new();
private readonly Dictionary<string, (ActionMap map, ActionMap.Action action)> actionLookup = new(StringComparer.Ordinal);
private readonly Dictionary<Guid, (ActionMap map, ActionMap.Action action)> actionLookupById = new();
private readonly HashSet<string> ambiguousActionNames = new(StringComparer.Ordinal);
private event Action _onInputsInit;
public event Action OnInputsInit
{
add
{
_onInputsInit += value;
// 重放行为:如果已经初始化,立即调用
if (isInputsInitialized)
{
value?.Invoke();
}
}
remove { _onInputsInit -= value; }
}
public event Action<bool, HashSet<RebindContext>> OnApply;
public event Action<RebindContext> OnRebindPrepare;
public event Action OnRebindStart;
public event Action<bool, RebindContext> OnRebindEnd;
public event Action<RebindContext, RebindContext> OnRebindConflict;
public static event Action BindingsChanged;
private bool isInputsInitialized = false;
public IReadOnlyDictionary<string, ActionMap> ActionMaps => actionMap;
public IReadOnlyCollection<RebindContext> PreparedRebinds => preparedRebinds;
public string SavePath
{
get
{
if (!string.IsNullOrEmpty(cachedSavePath))
return cachedSavePath;
#if UNITY_EDITOR
string folder = Application.dataPath;
#else
string folder = Application.persistentDataPath;
#endif
cachedSavePath = Path.Combine(folder, FILE_NAME);
return cachedSavePath;
}
}
private void EnsureSaveDirectoryExists()
{
var directory = Path.GetDirectoryName(SavePath);
if (!Directory.Exists(directory))
Directory.CreateDirectory(directory);
}
protected override void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
if (actions == null)
{
Log.Error("InputBindingManager: InputActionAsset not assigned.");
return;
}
BuildActionMap();
try
{
defaultBindingsJson = actions.SaveBindingOverridesAsJson();
}
catch (Exception ex)
{
Log.Warning($"[InputBindingManager] Failed to save default bindings: {ex.Message}");
defaultBindingsJson = string.Empty;
}
if (File.Exists(SavePath))
{
try
{
var json = File.ReadAllText(SavePath);
if (!string.IsNullOrEmpty(json))
{
actions.LoadBindingOverridesFromJson(json);
RefreshBindingPathsFromActions();
BindingsChanged?.Invoke();
if (debugMode)
{
Log.Info($"Loaded overrides from {SavePath}");
}
}
}
catch (Exception ex)
{
Log.Error("Failed to load overrides: " + ex);
}
}
isInputsInitialized = true;
_onInputsInit?.Invoke();
actions.Enable();
}
protected override void OnDestroy()
{
if (_instance == this)
{
_instance = null;
}
rebindOperation?.Dispose();
rebindOperation = null;
// 清除所有事件处理器
_onInputsInit = null;
OnApply = null;
OnRebindPrepare = null;
OnRebindStart = null;
OnRebindEnd = null;
OnRebindConflict = null;
BindingsChanged = null;
}
private void BuildActionMap()
{
actionMap.Clear();
actionLookup.Clear();
actionLookupById.Clear();
ambiguousActionNames.Clear();
foreach (var map in actions.actionMaps)
{
var actionMapObj = new ActionMap(map);
actionMap.Add(map.name, actionMapObj);
foreach (var actionPair in actionMapObj.actions)
{
RegisterActionLookup(map.name, actionPair.Key, actionMapObj, actionPair.Value);
}
}
}
private void RegisterActionLookup(string mapName, string actionName, ActionMap map, ActionMap.Action action)
{
actionLookupById[action.action.id] = (map, action);
actionLookup[$"{mapName}/{actionName}"] = (map, action);
if (ambiguousActionNames.Contains(actionName))
{
return;
}
if (actionLookup.TryGetValue(actionName, out var existing))
{
if (existing.action.action != action.action)
{
actionLookup.Remove(actionName);
ambiguousActionNames.Add(actionName);
Log.Warning($"[InputBindingManager] Duplicate action name '{actionName}' detected. Use 'MapName/{actionName}' to resolve it.");
}
return;
}
actionLookup[actionName] = (map, action);
}
private void RefreshBindingPathsFromActions()
{
foreach (var mapPair in actionMap.Values)
{
foreach (var actionPair in mapPair.actions.Values)
{
var a = actionPair;
foreach (var bpair in a.bindings)
{
bpair.Value.bindingPath.EffectivePath = a.action.bindings[bpair.Key].effectivePath;
}
}
}
}
public sealed class ActionMap
{
public string name;
public Dictionary<string, Action> actions;
public ActionMap(InputActionMap map)
{
name = map.name;
int actionCount = map.actions.Count;
actions = new Dictionary<string, Action>(actionCount);
foreach (var action in map.actions)
{
actions.Add(action.name, new Action(action));
}
}
public sealed class Action
{
public InputAction action;
public Dictionary<int, Binding> bindings;
public Action(InputAction action)
{
this.action = action;
int count = action.bindings.Count;
bindings = new Dictionary<int, Binding>(count);
for (int i = 0; i < count; i++)
{
if (action.bindings[i].isComposite)
{
int first = i + 1;
int last = first;
while (last < count && action.bindings[last].isPartOfComposite) last++;
for (int p = first; p < last; p++)
AddBinding(action.bindings[p], p);
i = last - 1;
}
else
{
AddBinding(action.bindings[i], i);
}
}
void AddBinding(InputBinding binding, int bindingIndex)
{
bindings.Add(bindingIndex, new Binding(
binding.name,
action.name,
binding.name,
bindingIndex,
new BindingPath(binding.path, binding.overridePath),
binding
));
}
}
public readonly struct Binding
{
public readonly string name;
public readonly string parentAction;
public readonly string compositePart;
public readonly int bindingIndex;
public readonly BindingPath bindingPath;
public readonly InputBinding inputBinding;
public Binding(string name, string parentAction, string compositePart, int bindingIndex,
BindingPath bindingPath, InputBinding inputBinding)
{
this.name = name;
this.parentAction = parentAction;
this.compositePart = compositePart;
this.bindingIndex = bindingIndex;
this.bindingPath = bindingPath;
this.inputBinding = inputBinding;
}
}
}
}
public sealed class BindingPath
{
public string bindingPath;
public string overridePath;
private event Action<string> onEffectivePathChanged;
public BindingPath(string bindingPath, string overridePath)
{
this.bindingPath = bindingPath;
this.overridePath = overridePath;
}
public string EffectivePath
{
get => !string.IsNullOrEmpty(overridePath) ? overridePath : bindingPath;
set
{
overridePath = (value == bindingPath) ? string.Empty : value;
onEffectivePathChanged?.Invoke(EffectivePath);
}
}
public void SubscribeToEffectivePathChanged(Action<string> callback)
{
onEffectivePathChanged += callback;
}
public void UnsubscribeFromEffectivePathChanged(Action<string> callback)
{
onEffectivePathChanged -= callback;
}
public void Dispose()
{
onEffectivePathChanged = null;
}
}
public sealed class RebindContext
{
public InputAction action;
public int bindingIndex;
public string overridePath;
private string cachedToString;
public RebindContext(InputAction action, int bindingIndex, string overridePath)
{
this.action = action;
this.bindingIndex = bindingIndex;
this.overridePath = overridePath;
}
public override bool Equals(object obj)
{
if (obj is not RebindContext other) return false;
if (action == null || other.action == null) return false;
return action.id == other.action.id && bindingIndex == other.bindingIndex;
}
public override int GetHashCode()
{
unchecked
{
int hashCode = 17;
hashCode = (hashCode * 31) + (action != null ? action.id.GetHashCode() : 0);
hashCode = (hashCode * 31) + bindingIndex;
return hashCode;
}
}
public override string ToString()
{
if (cachedToString == null && action != null)
{
string mapName = action.actionMap != null ? action.actionMap.name : "<no-map>";
cachedToString = $"{mapName}/{action.name}:{bindingIndex}";
}
return cachedToString ?? "<null>";
}
}
/* ---------------- Public API ---------------- */
/// <summary>
/// 根据操作名称获取输入操作
/// </summary>
/// <param name="actionName">操作名称</param>
/// <returns>输入操作,未找到则返回 null</returns>
public static InputAction Action(string actionName)
{
var instance = Instance;
if (instance == null) return null;
if (TryGetAction(actionName, out InputAction action))
{
return action;
}
if (instance.ambiguousActionNames.Contains(actionName))
{
Log.Error($"[InputBindingManager] Action name '{actionName}' is ambiguous. Use 'MapName/{actionName}' instead.");
return null;
}
Log.Error($"[InputBindingManager] Could not find action '{actionName}'");
return null;
}
public static bool TryGetAction(string actionName, out InputAction action)
{
var instance = Instance;
if (instance == null || string.IsNullOrWhiteSpace(actionName))
{
action = null;
return false;
}
if (instance.actionLookup.TryGetValue(actionName, out var result))
{
action = result.action.action;
return true;
}
action = null;
return false;
}
/// <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;
// 自动决定 bindingIndex 和 deviceMatch
int bindingIndex = Instance.FindBestBindingIndexForKeyboard(action, compositePartName);
if (bindingIndex < 0)
{
Log.Error($"[InputBindingManager] No suitable binding found for action '{actionName}' (part={compositePartName ?? "<null>"})");
return;
}
Instance.actions.Disable();
Instance.PerformInteractiveRebinding(action, bindingIndex, KEYBOARD_DEVICE, true);
Instance.OnRebindStart?.Invoke();
if (Instance.debugMode)
{
Log.Info("[InputBindingManager] Rebind started");
}
}
/// <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
{
// 在清除之前创建准备好的重绑定的副本
HashSet<RebindContext> appliedContexts = Instance.OnApply != null
? new HashSet<RebindContext>(Instance.preparedRebinds)
: null;
foreach (var ctx in Instance.preparedRebinds)
{
if (!string.IsNullOrEmpty(ctx.overridePath))
{
if (ctx.overridePath == NULL_BINDING)
{
ctx.action.RemoveBindingOverride(ctx.bindingIndex);
}
else
{
ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath);
}
}
var bp = GetBindingPath(ctx.action, ctx.bindingIndex);
if (bp != null)
{
bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath;
}
}
Instance.preparedRebinds.Clear();
await Instance.WriteOverridesToDiskAsync();
BindingsChanged?.Invoke();
Instance.OnApply?.Invoke(true, appliedContexts);
Instance.isApplyPending = false;
if (Instance.debugMode)
{
Log.Info("[InputBindingManager] Apply confirmed and saved.");
}
return true;
}
catch (Exception ex)
{
Log.Error("[InputBindingManager] Failed to apply binds: " + ex);
Instance.OnApply?.Invoke(false, null);
return false;
}
}
/// <summary>
/// 丢弃准备好的重新绑定
/// </summary>
public static void DiscardPrepared()
{
if (!Instance.isApplyPending) return;
// 在清除之前创建准备好的重绑定的副本(用于事件通知)
HashSet<RebindContext> discardedContexts = Instance.OnApply != null
? new HashSet<RebindContext>(Instance.preparedRebinds)
: null;
Instance.preparedRebinds.Clear();
Instance.isApplyPending = false;
Instance.OnApply?.Invoke(false, discardedContexts);
if (Instance.debugMode)
{
Log.Info("[InputBindingManager] Prepared rebinds discarded.");
}
}
private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true)
{
var op = action.PerformInteractiveRebinding(bindingIndex);
if (!string.IsNullOrEmpty(deviceMatchPath))
{
op = op.WithControlsHavingToMatchPath(deviceMatchPath);
}
if (excludeMouseMovementAndScroll)
{
op = op.WithControlsExcluding(MOUSE_DELTA)
.WithControlsExcluding(MOUSE_SCROLL)
.WithControlsExcluding(MOUSE_SCROLL_X)
.WithControlsExcluding(MOUSE_SCROLL_Y);
}
rebindOperation = op
.OnApplyBinding((o, path) =>
{
RebindContext preparedContext = new RebindContext(action, bindingIndex, path);
if (AnyPreparedRebind(path, action, bindingIndex, out var existing))
{
PrepareRebind(preparedContext);
PrepareRebind(new RebindContext(existing.action, existing.bindingIndex, NULL_BINDING));
OnRebindConflict?.Invoke(preparedContext, existing);
}
else if (AnyBindingPath(path, action, bindingIndex, out var dup))
{
RebindContext conflictingContext = new RebindContext(dup.action, dup.bindingIndex, dup.action.bindings[dup.bindingIndex].path);
PrepareRebind(preparedContext);
PrepareRebind(new RebindContext(dup.action, dup.bindingIndex, NULL_BINDING));
OnRebindConflict?.Invoke(preparedContext, conflictingContext);
}
else
{
PrepareRebind(preparedContext);
}
})
.OnComplete(opc =>
{
if (debugMode)
{
Log.Info("[InputBindingManager] Rebind completed");
}
actions.Enable();
OnRebindEnd?.Invoke(true, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath));
CleanRebindOperation();
})
.OnCancel(opc =>
{
if (debugMode)
{
Log.Info("[InputBindingManager] Rebind cancelled");
}
actions.Enable();
OnRebindEnd?.Invoke(false, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath));
CleanRebindOperation();
})
.WithCancelingThrough(KEYBOARD_ESCAPE)
.Start();
}
private void CleanRebindOperation()
{
rebindOperation?.Dispose();
rebindOperation = null;
}
private bool AnyPreparedRebind(string bindingPath, InputAction currentAction, int currentIndex, out RebindContext duplicate)
{
foreach (var ctx in preparedRebinds)
{
if (ctx.overridePath == bindingPath && (ctx.action != currentAction || (ctx.action == currentAction && ctx.bindingIndex != currentIndex)))
{
duplicate = ctx;
return true;
}
}
duplicate = null;
return false;
}
private bool AnyBindingPath(string bindingPath, InputAction currentAction, int currentIndex, out (InputAction action, int bindingIndex) duplicate)
{
foreach (var map in actionMap.Values)
{
foreach (var actionPair in map.actions.Values)
{
bool isSameAction = actionPair.action == currentAction;
foreach (var bindingPair in actionPair.bindings)
{
// Skip if it's the same action and same binding index
if (isSameAction && bindingPair.Key == currentIndex)
continue;
if (bindingPair.Value.bindingPath.EffectivePath == bindingPath)
{
duplicate = (actionPair.action, bindingPair.Key);
return true;
}
}
}
}
duplicate = default;
return false;
}
private void PrepareRebind(RebindContext context)
{
// Remove any existing prepared state for the same action/binding pair.
preparedRebinds.Remove(context);
BindingPath bindingPath = GetBindingPath(context.action, context.bindingIndex);
if (bindingPath == null) return;
if (string.IsNullOrEmpty(context.overridePath))
{
context.overridePath = bindingPath.bindingPath;
}
if (bindingPath.EffectivePath != context.overridePath)
{
preparedRebinds.Add(context);
isApplyPending = true;
OnRebindPrepare?.Invoke(context);
if (debugMode)
{
Log.Info($"Prepared rebind: {context} -> {context.overridePath}");
}
}
}
private async UniTask WriteOverridesToDiskAsync()
{
try
{
var json = actions.SaveBindingOverridesAsJson();
EnsureSaveDirectoryExists();
using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json);
if (debugMode)
{
Log.Info($"Overrides saved to {SavePath}");
}
}
catch (Exception ex)
{
Log.Error("Failed to save overrides: " + ex);
throw;
}
}
/// <summary>
/// 重置所有绑定到默认值
/// </summary>
public async UniTask ResetToDefaultAsync()
{
try
{
if (!string.IsNullOrEmpty(defaultBindingsJson))
{
actions.LoadBindingOverridesFromJson(defaultBindingsJson);
}
else
{
foreach (var map in actionMap.Values)
{
foreach (var a in map.actions.Values)
{
for (int b = 0; b < a.action.bindings.Count; b++)
{
a.action.RemoveBindingOverride(b);
}
}
}
}
RefreshBindingPathsFromActions();
await WriteOverridesToDiskAsync();
BindingsChanged?.Invoke();
if (debugMode)
{
Log.Info("Reset to default and saved.");
}
}
catch (Exception ex)
{
Log.Error("Failed to reset defaults: " + ex);
}
}
/// <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;
if (instance == null) return null;
if (instance.TryGetActionRecord(actionName, out var result)
&& result.action.bindings.TryGetValue(bindingIndex, out var binding))
{
return binding.bindingPath;
}
return null;
}
public static BindingPath GetBindingPath(InputAction action, int bindingIndex = 0)
{
var instance = Instance;
if (instance == null || action == null) return null;
if (instance.TryGetActionRecord(action, out var result)
&& result.action.bindings.TryGetValue(bindingIndex, out var binding))
{
return binding.bindingPath;
}
return null;
}
private bool TryGetActionRecord(string actionName, out (ActionMap map, ActionMap.Action action) result)
{
return actionLookup.TryGetValue(actionName, out result);
}
private bool TryGetActionRecord(InputAction action, out (ActionMap map, ActionMap.Action action) result)
{
if (action != null && actionLookupById.TryGetValue(action.id, out result))
{
return result.action.action == action;
}
result = default;
return false;
}
// 为键盘选择最佳绑定索引;如果 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;
int fallbackPart = -1;
int fallbackNonComposite = -1;
bool searchingForCompositePart = !string.IsNullOrEmpty(compositePartName);
for (int i = 0; i < action.bindings.Count; i++)
{
var b = action.bindings[i];
// 如果搜索特定的复合部分,跳过不匹配的绑定
if (searchingForCompositePart)
{
if (!b.isPartOfComposite) continue;
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
}
// 检查此绑定是否用于键盘
bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) ||
(!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase));
if (b.isPartOfComposite)
{
if (fallbackPart == -1) fallbackPart = i;
if (isKeyboardBinding) return i;
}
else
{
if (fallbackNonComposite == -1) fallbackNonComposite = i;
if (isKeyboardBinding) return i;
}
}
return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart;
}
public static InputBindingManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<InputBindingManager>();
}
return _instance;
}
}
private static InputBindingManager _instance;
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 7c25ae9d04ef4e03a723135aa298de16
timeCreated: 1765271070

View File

@ -1,472 +0,0 @@
using System;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
public static class InputDeviceWatcher
{
public enum InputDeviceCategory
{
Keyboard,
Xbox,
PlayStation,
Other
}
public readonly struct DeviceContext : IEquatable<DeviceContext>
{
public readonly InputDeviceCategory Category;
public readonly int DeviceId;
public readonly int VendorId;
public readonly int ProductId;
public readonly string DeviceName;
public readonly string Layout;
public DeviceContext(
InputDeviceCategory category,
int deviceId,
int vendorId,
int productId,
string deviceName,
string layout)
{
Category = category;
DeviceId = deviceId;
VendorId = vendorId;
ProductId = productId;
DeviceName = deviceName ?? string.Empty;
Layout = layout ?? string.Empty;
}
public bool Equals(DeviceContext other)
{
return Category == other.Category
&& DeviceId == other.DeviceId
&& VendorId == other.VendorId
&& ProductId == other.ProductId
&& string.Equals(DeviceName, other.DeviceName, StringComparison.Ordinal)
&& string.Equals(Layout, other.Layout, StringComparison.Ordinal);
}
public override bool Equals(object obj)
{
return obj is DeviceContext other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = (int)Category;
hashCode = (hashCode * 397) ^ DeviceId;
hashCode = (hashCode * 397) ^ VendorId;
hashCode = (hashCode * 397) ^ ProductId;
hashCode = (hashCode * 397) ^ (DeviceName != null ? DeviceName.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (Layout != null ? Layout.GetHashCode() : 0);
return hashCode;
}
}
}
[Serializable]
private struct DeviceCapabilityInfo
{
public int vendorId;
public int productId;
}
private const float SameCategoryDebounceWindow = 0.15f;
private const float AxisActivationThreshold = 0.5f;
private const float StickActivationThreshold = 0.25f;
private const string DefaultKeyboardDeviceName = "Keyboard&Mouse";
public static InputDeviceCategory CurrentCategory { get; private set; } = InputDeviceCategory.Keyboard;
public static string CurrentDeviceName { get; private set; } = DefaultKeyboardDeviceName;
public static int CurrentDeviceId { get; private set; } = -1;
public static int CurrentVendorId { get; private set; }
public static int CurrentProductId { get; private set; }
public static DeviceContext CurrentContext { get; private set; } = CreateDefaultContext();
private static InputAction _anyInputAction;
private static float _lastSwitchTime = -Mathf.Infinity;
private static DeviceContext _lastEmittedContext = CreateDefaultContext();
private static readonly Dictionary<int, DeviceContext> DeviceContextCache = new();
private static bool _initialized;
public static event Action<InputDeviceCategory> OnDeviceChanged;
public static event Action<DeviceContext> OnDeviceContextChanged;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void Initialize()
{
if (_initialized)
{
return;
}
_initialized = true;
ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext;
_anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough);
_anyInputAction.AddBinding("<Keyboard>/anyKey");
// _anyInputAction.AddBinding("<Mouse>/leftButton");
// _anyInputAction.AddBinding("<Mouse>/rightButton");
// _anyInputAction.AddBinding("<Mouse>/middleButton");
// _anyInputAction.AddBinding("<Mouse>/scroll");
_anyInputAction.AddBinding("<Gamepad>/*");
_anyInputAction.AddBinding("<Joystick>/*");
_anyInputAction.performed += OnAnyInputPerformed;
_anyInputAction.Enable();
InputSystem.onDeviceChange += OnDeviceChange;
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#endif
}
#if UNITY_EDITOR
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.ExitingPlayMode)
{
Dispose();
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
}
}
#endif
public static void Dispose()
{
if (!_initialized)
{
return;
}
if (_anyInputAction != null)
{
_anyInputAction.performed -= OnAnyInputPerformed;
_anyInputAction.Disable();
_anyInputAction.Dispose();
_anyInputAction = null;
}
InputSystem.onDeviceChange -= OnDeviceChange;
DeviceContextCache.Clear();
ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext;
_lastSwitchTime = -Mathf.Infinity;
OnDeviceChanged = null;
OnDeviceContextChanged = null;
_initialized = false;
}
private static void OnAnyInputPerformed(InputAction.CallbackContext context)
{
InputControl control = context.control;
if (!IsRelevantControl(control))
{
return;
}
InputDevice device = control.device;
if (device == null || device.deviceId == CurrentDeviceId)
{
return;
}
DeviceContext deviceContext = BuildContext(device);
if (deviceContext.DeviceId == CurrentDeviceId)
{
return;
}
float now = Time.realtimeSinceStartup;
if (deviceContext.Category == CurrentCategory && now - _lastSwitchTime < SameCategoryDebounceWindow)
{
return;
}
_lastSwitchTime = now;
SetCurrentContext(deviceContext);
}
private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
if (device == null)
{
return;
}
switch (change)
{
case InputDeviceChange.Removed:
case InputDeviceChange.Disconnected:
DeviceContextCache.Remove(device.deviceId);
if (device.deviceId == CurrentDeviceId)
{
PromoteFallbackDevice(device.deviceId);
}
break;
case InputDeviceChange.Reconnected:
case InputDeviceChange.Added:
DeviceContextCache.Remove(device.deviceId);
if (CurrentDeviceId < 0 && IsRelevantDevice(device))
{
SetCurrentContext(BuildContext(device));
}
break;
}
}
private static void PromoteFallbackDevice(int removedDeviceId)
{
for (int i = InputSystem.devices.Count - 1; i >= 0; i--)
{
InputDevice device = InputSystem.devices[i];
if (device == null || device.deviceId == removedDeviceId || !device.added || !IsRelevantDevice(device))
{
continue;
}
SetCurrentContext(BuildContext(device));
return;
}
SetCurrentContext(CreateDefaultContext());
}
private static void SetCurrentContext(DeviceContext context)
{
bool categoryChanged = CurrentCategory != context.Category;
ApplyContext(context, true);
if (!_lastEmittedContext.Equals(context))
{
OnDeviceContextChanged?.Invoke(context);
if (categoryChanged)
{
OnDeviceChanged?.Invoke(context.Category);
}
_lastEmittedContext = context;
}
}
private static void ApplyContext(DeviceContext context, bool log)
{
CurrentContext = context;
CurrentCategory = context.Category;
CurrentDeviceId = context.DeviceId;
CurrentVendorId = context.VendorId;
CurrentProductId = context.ProductId;
CurrentDeviceName = context.DeviceName;
#if UNITY_EDITOR
if (log)
{
AlicizaX.Log.Info($"Input device -> {CurrentCategory} name={CurrentDeviceName} vid=0x{CurrentVendorId:X} pid=0x{CurrentProductId:X} id={CurrentDeviceId}");
}
#endif
}
private static DeviceContext BuildContext(InputDevice device)
{
if (device == null)
{
return CreateDefaultContext();
}
if (DeviceContextCache.TryGetValue(device.deviceId, out DeviceContext cachedContext))
{
return cachedContext;
}
TryParseVendorProductIds(device.description.capabilities, out int vendorId, out int productId);
string deviceName = string.IsNullOrWhiteSpace(device.displayName) ? device.name : device.displayName;
DeviceContext context = new DeviceContext(
DetermineCategoryFromDevice(device, vendorId),
device.deviceId,
vendorId,
productId,
deviceName,
device.layout);
DeviceContextCache[device.deviceId] = context;
return context;
}
private static DeviceContext CreateDefaultContext()
{
return new DeviceContext(InputDeviceCategory.Keyboard, -1, 0, 0, DefaultKeyboardDeviceName, Keyboard.current != null ? Keyboard.current.layout : string.Empty);
}
private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device, int vendorId = 0)
{
if (device == null)
{
return InputDeviceCategory.Keyboard;
}
if (device is Keyboard || device is Mouse)
{
return InputDeviceCategory.Keyboard;
}
if (IsGamepadLike(device))
{
return GetGamepadCategory(device, vendorId);
}
if (DescriptionContains(device, "xbox") || DescriptionContains(device, "xinput"))
{
return InputDeviceCategory.Xbox;
}
if (DescriptionContains(device, "dualshock")
|| DescriptionContains(device, "dualsense")
|| DescriptionContains(device, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
return InputDeviceCategory.Other;
}
private static bool IsRelevantDevice(InputDevice device)
{
return device is Keyboard || device is Mouse || IsGamepadLike(device);
}
private static bool IsRelevantControl(InputControl control)
{
if (control == null || control.device == null || !IsRelevantDevice(control.device) || control.synthetic)
{
return false;
}
switch (control)
{
case ButtonControl button:
return button.IsPressed();
case StickControl stick:
return stick.ReadValue().sqrMagnitude >= StickActivationThreshold;
case Vector2Control vector2:
return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold;
case AxisControl axis:
return Mathf.Abs(axis.ReadValue()) >= AxisActivationThreshold;
default:
return !control.noisy;
}
}
private static bool IsGamepadLike(InputDevice device)
{
if (device is Gamepad || device is Joystick)
{
return true;
}
string layout = device.layout ?? string.Empty;
if (ContainsIgnoreCase(layout, "Mouse")
|| ContainsIgnoreCase(layout, "Touch")
|| ContainsIgnoreCase(layout, "Pen"))
{
return false;
}
return ContainsIgnoreCase(layout, "Gamepad")
|| ContainsIgnoreCase(layout, "Controller")
|| ContainsIgnoreCase(layout, "Joystick");
}
private static InputDeviceCategory GetGamepadCategory(InputDevice device, int vendorId = 0)
{
if (device == null)
{
return InputDeviceCategory.Other;
}
string interfaceName = device.description.interfaceName ?? string.Empty;
if (ContainsIgnoreCase(interfaceName, "xinput"))
{
return InputDeviceCategory.Xbox;
}
if (vendorId == 0 && TryParseVendorProductIds(device.description.capabilities, out int parsedVendorId, out _))
{
vendorId = parsedVendorId;
}
if (vendorId == 0x045E || vendorId == 1118)
{
return InputDeviceCategory.Xbox;
}
if (vendorId == 0x054C || vendorId == 1356)
{
return InputDeviceCategory.PlayStation;
}
if (DescriptionContains(device, "xbox"))
{
return InputDeviceCategory.Xbox;
}
if (DescriptionContains(device, "dualshock")
|| DescriptionContains(device, "dualsense")
|| DescriptionContains(device, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
return InputDeviceCategory.Other;
}
private static bool DescriptionContains(InputDevice device, string value)
{
if (device == null)
{
return false;
}
var description = device.description;
return ContainsIgnoreCase(description.interfaceName, value)
|| ContainsIgnoreCase(device.layout, value)
|| ContainsIgnoreCase(description.product, value)
|| ContainsIgnoreCase(description.manufacturer, value)
|| ContainsIgnoreCase(device.displayName, value)
|| ContainsIgnoreCase(device.name, value);
}
private static bool TryParseVendorProductIds(string capabilities, out int vendorId, out int productId)
{
vendorId = 0;
productId = 0;
if (string.IsNullOrWhiteSpace(capabilities))
{
return false;
}
try
{
DeviceCapabilityInfo info = JsonUtility.FromJson<DeviceCapabilityInfo>(capabilities);
vendorId = info.vendorId;
productId = info.productId;
return vendorId != 0 || productId != 0;
}
catch
{
return false;
}
}
private static bool ContainsIgnoreCase(string source, string value)
{
return !string.IsNullOrEmpty(source) && source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e78f6224467e13742a70115f1942d941
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -1,359 +0,0 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
[Serializable]
public sealed class GlyphEntry
{
public Sprite Sprite;
public InputAction action;
}
[Serializable]
public sealed class DeviceGlyphTable
{
public string deviceName;
public Texture2D spriteSheetTexture;
public Sprite platformIcons;
public List<GlyphEntry> entries = new List<GlyphEntry>();
}
[CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "GameplaySystem/Input/InputGlyphDatabase", order = 400)]
public sealed class InputGlyphDatabase : ScriptableObject
{
private const string DeviceKeyboard = "Keyboard";
private const string DeviceXbox = "Xbox";
private const string DevicePlayStation = "PlayStation";
private const string DeviceOther = "Other";
private static readonly InputDeviceWatcher.InputDeviceCategory[] KeyboardLookupOrder = { InputDeviceWatcher.InputDeviceCategory.Keyboard };
private static readonly InputDeviceWatcher.InputDeviceCategory[] XboxLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
private static readonly InputDeviceWatcher.InputDeviceCategory[] PlayStationLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.PlayStation,
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
private static readonly InputDeviceWatcher.InputDeviceCategory[] OtherLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
private static readonly Dictionary<string, string> NormalizedPathCache = new(StringComparer.Ordinal);
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>();
public Sprite placeholderSprite;
private Dictionary<string, DeviceGlyphTable> _tableCache;
private Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>> _pathLookup;
private void OnEnable()
{
BuildCache();
}
#if UNITY_EDITOR
private void OnValidate()
{
BuildCache();
}
#endif
public DeviceGlyphTable GetTable(string deviceName)
{
if (string.IsNullOrWhiteSpace(deviceName) || tables == null)
{
return null;
}
EnsureCache();
_tableCache.TryGetValue(deviceName, out DeviceGlyphTable table);
return table;
}
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
{
switch (device)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return GetTable(DeviceKeyboard);
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return GetTable(DeviceXbox);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return GetTable(DevicePlayStation);
default:
return GetTable(DeviceOther) ?? GetTable(DeviceXbox);
}
}
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{
DeviceGlyphTable table = GetTable(device);
return table != null ? table.platformIcons : null;
}
public bool TryGetSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite)
{
EnsureCache();
string key = NormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(key))
{
sprite = placeholderSprite;
return sprite != null;
}
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
for (int i = 0; i < lookupOrder.Length; i++)
{
InputDeviceWatcher.InputDeviceCategory category = lookupOrder[i];
if (_pathLookup.TryGetValue(category, out Dictionary<string, Sprite> map) && map.TryGetValue(key, out sprite) && sprite != null)
{
return true;
}
}
sprite = placeholderSprite;
return sprite != null;
}
public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{
return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite;
}
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{
if (!TryGetSprite(controlPath, device, out Sprite sprite) || sprite == null)
{
return null;
}
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
for (int i = 0; i < lookupOrder.Length; i++)
{
DeviceGlyphTable table = GetTable(lookupOrder[i]);
if (table == null || table.entries == null)
{
continue;
}
for (int j = 0; j < table.entries.Count; j++)
{
GlyphEntry entry = table.entries[j];
if (entry != null && entry.Sprite == sprite)
{
return entry;
}
}
}
return null;
}
private void EnsureCache()
{
if (_tableCache == null || _pathLookup == null)
{
BuildCache();
}
}
private void BuildCache()
{
_tableCache ??= new Dictionary<string, DeviceGlyphTable>(StringComparer.OrdinalIgnoreCase);
_tableCache.Clear();
_pathLookup ??= new Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>>();
_pathLookup.Clear();
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Keyboard);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Xbox);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.PlayStation);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Other);
if (tables == null)
{
return;
}
for (int i = 0; i < tables.Count; i++)
{
DeviceGlyphTable table = tables[i];
if (table == null || string.IsNullOrWhiteSpace(table.deviceName))
{
continue;
}
_tableCache[table.deviceName] = table;
InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName);
Dictionary<string, Sprite> map = _pathLookup[category];
RegisterEntries(table, map);
}
}
#if UNITY_EDITOR
public void EditorRefreshCache()
{
BuildCache();
}
public static string EditorNormalizeControlPath(string controlPath)
{
return NormalizeControlPath(controlPath);
}
#endif
private void InitializeLookup(InputDeviceWatcher.InputDeviceCategory category)
{
_pathLookup[category] = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
}
private void RegisterEntries(DeviceGlyphTable table, Dictionary<string, Sprite> map)
{
if (table.entries == null)
{
return;
}
for (int i = 0; i < table.entries.Count; i++)
{
GlyphEntry entry = table.entries[i];
if (entry == null || entry.Sprite == null || entry.action == null)
{
continue;
}
for (int j = 0; j < entry.action.bindings.Count; j++)
{
RegisterBinding(map, entry.action.bindings[j].path, entry.Sprite);
RegisterBinding(map, entry.action.bindings[j].effectivePath, entry.Sprite);
}
}
}
private void RegisterBinding(Dictionary<string, Sprite> map, string controlPath, Sprite sprite)
{
string key = NormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(key) || map.ContainsKey(key))
{
return;
}
map[key] = sprite;
}
private static string NormalizeControlPath(string controlPath)
{
if (string.IsNullOrWhiteSpace(controlPath))
{
return string.Empty;
}
if (NormalizedPathCache.TryGetValue(controlPath, out string normalizedPath))
{
return normalizedPath;
}
normalizedPath = CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant());
NormalizedPathCache[controlPath] = normalizedPath;
return normalizedPath;
}
private static string CanonicalizeDeviceLayout(string controlPath)
{
int start = controlPath.IndexOf('<');
int end = controlPath.IndexOf('>');
if (start < 0 || end <= start + 1)
{
return controlPath;
}
string layout = controlPath.Substring(start + 1, end - start - 1);
string canonicalLayout = GetCanonicalLayout(layout);
if (string.Equals(layout, canonicalLayout, StringComparison.Ordinal))
{
return controlPath;
}
return controlPath.Substring(0, start + 1) + canonicalLayout + controlPath.Substring(end);
}
private static string GetCanonicalLayout(string layout)
{
if (string.IsNullOrEmpty(layout))
{
return string.Empty;
}
if (layout.IndexOf("keyboard", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "keyboard";
}
if (layout.IndexOf("mouse", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "mouse";
}
if (layout.IndexOf("joystick", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "joystick";
}
if (layout.IndexOf("gamepad", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("controller", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("xinput", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("dualshock", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("dualsense", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "gamepad";
}
return layout;
}
private static InputDeviceWatcher.InputDeviceCategory ParseCategory(string deviceName)
{
if (string.IsNullOrWhiteSpace(deviceName))
{
return InputDeviceWatcher.InputDeviceCategory.Other;
}
if (deviceName.Equals(DeviceKeyboard, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.Keyboard;
}
if (deviceName.Equals(DeviceXbox, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.Xbox;
}
if (deviceName.Equals(DevicePlayStation, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.PlayStation;
}
return InputDeviceWatcher.InputDeviceCategory.Other;
}
private static InputDeviceWatcher.InputDeviceCategory[] GetLookupOrder(InputDeviceWatcher.InputDeviceCategory device)
{
switch (device)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardLookupOrder;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxLookupOrder;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationLookupOrder;
default:
return OtherLookupOrder;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 53ed017cef844d11842ba16553c6391d
timeCreated: 1764917621

View File

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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 96b54c2be64d4891a50c4db8b36bf839
timeCreated: 1764923573

View File

@ -1,303 +0,0 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.InputSystem;
[CustomEditor(typeof(InputGlyph))]
[CanEditMultipleObjects]
public sealed class InputGlyphEditor : Editor
{
private SerializedProperty _actionSourceMode;
private SerializedProperty _actionReference;
private SerializedProperty _hotkeyTrigger;
private SerializedProperty _actionName;
private SerializedProperty _compositePartName;
private SerializedProperty _outputMode;
private SerializedProperty _targetImage;
private SerializedProperty _targetText;
private SerializedProperty _categoryEvents;
private GUIStyle _titleStyle;
private GUIStyle _sectionStyle;
private GUIStyle _hintStyle;
private void OnEnable()
{
_actionSourceMode = serializedObject.FindProperty("actionSourceMode");
_actionReference = serializedObject.FindProperty("actionReference");
_hotkeyTrigger = serializedObject.FindProperty("hotkeyTrigger");
_actionName = serializedObject.FindProperty("actionName");
_compositePartName = serializedObject.FindProperty("compositePartName");
_outputMode = serializedObject.FindProperty("outputMode");
_targetImage = serializedObject.FindProperty("targetImage");
_targetText = serializedObject.FindProperty("targetText");
_categoryEvents = serializedObject.FindProperty("categoryEvents");
BuildStyles();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawSourceSection();
DrawOutputSection();
DrawEventsSection();
serializedObject.ApplyModifiedProperties();
}
private void DrawSourceSection()
{
InputAction resolvedAction = ResolveSelectedAction();
EditorGUILayout.BeginVertical(_sectionStyle);
EditorGUILayout.PropertyField(_actionSourceMode, new GUIContent("Reference Mode"));
DrawSourceFields();
DrawResolvedActionInfo(resolvedAction);
DrawCompositePartField(resolvedAction);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(6f);
}
private void DrawSourceFields()
{
InputGlyph.ActionSourceMode mode = (InputGlyph.ActionSourceMode)_actionSourceMode.enumValueIndex;
switch (mode)
{
case InputGlyph.ActionSourceMode.ActionReference:
EditorGUILayout.PropertyField(_actionReference, new GUIContent("Action Reference"));
EditorGUILayout.LabelField("Use a direct InputActionReference.", _hintStyle);
break;
case InputGlyph.ActionSourceMode.HotkeyTrigger:
EditorGUILayout.PropertyField(_hotkeyTrigger, new GUIContent("Hotkey Trigger"));
Component component = _hotkeyTrigger.objectReferenceValue as Component;
if (component != null && !(component is UnityEngine.UI.IHotkeyTrigger))
{
EditorGUILayout.HelpBox("Hotkey Trigger must implement IHotkeyTrigger.", MessageType.Warning);
}
else
{
EditorGUILayout.LabelField("Reads the action from an external IHotkeyTrigger component.", _hintStyle);
}
break;
case InputGlyph.ActionSourceMode.ActionName:
EditorGUILayout.PropertyField(_actionName, new GUIContent("Action Name"));
EditorGUILayout.LabelField("Supports ActionName or MapName/ActionName.", _hintStyle);
break;
}
}
private void DrawOutputSection()
{
EditorGUILayout.BeginVertical(_sectionStyle);
EditorGUILayout.PropertyField(_outputMode, new GUIContent("Render Mode"));
DrawOutputFields();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(6f);
}
private void DrawOutputFields()
{
InputGlyph.OutputMode mode = (InputGlyph.OutputMode)_outputMode.enumValueIndex;
switch (mode)
{
case InputGlyph.OutputMode.Image:
EditorGUILayout.PropertyField(_targetImage, new GUIContent("Target Image"));
EditorGUILayout.LabelField("Shows the resolved sprite on a Unity UI Image.", _hintStyle);
break;
case InputGlyph.OutputMode.Text:
EditorGUILayout.PropertyField(_targetText, new GUIContent("Target TMP Text"));
EditorGUILayout.LabelField("Uses the current TMP text as a template and replaces {0}.", _hintStyle);
TMP_Text text = _targetText.objectReferenceValue as TMP_Text;
if (text == null)
{
EditorGUILayout.HelpBox("If TMP_Text is empty, the component tries GetComponent<TMP_Text>().", MessageType.None);
}
break;
}
}
private void DrawEventsSection()
{
EditorGUILayout.BeginVertical(_sectionStyle);
EditorGUILayout.PropertyField(_categoryEvents, new GUIContent("Category Events"), true);
EditorGUILayout.EndVertical();
}
private void DrawResolvedActionInfo(InputAction action)
{
if (action == null)
{
return;
}
string mapName = action.actionMap != null ? action.actionMap.name : "<No Map>";
EditorGUILayout.LabelField($"Resolved Action: {mapName}/{action.name}", _hintStyle);
}
private void DrawCompositePartField(InputAction action)
{
List<string> compositeParts = CollectCompositePartNames(action);
if (compositeParts.Count == 0)
{
if (!string.IsNullOrEmpty(_compositePartName.stringValue))
{
_compositePartName.stringValue = string.Empty;
}
return;
}
string[] options = new string[compositeParts.Count + 1];
options[0] = "<None>";
for (int i = 0; i < compositeParts.Count; i++)
{
options[i + 1] = compositeParts[i];
}
int selectedIndex = 0;
for (int i = 0; i < compositeParts.Count; i++)
{
if (string.Equals(compositeParts[i], _compositePartName.stringValue, StringComparison.OrdinalIgnoreCase))
{
selectedIndex = i + 1;
break;
}
}
int newIndex = EditorGUILayout.Popup(new GUIContent("Composite Part"), selectedIndex, options);
_compositePartName.stringValue = newIndex <= 0 ? string.Empty : compositeParts[newIndex - 1];
EditorGUILayout.LabelField("Shown only when the resolved action contains composite bindings.", _hintStyle);
}
private InputAction ResolveSelectedAction()
{
InputGlyph.ActionSourceMode mode = (InputGlyph.ActionSourceMode)_actionSourceMode.enumValueIndex;
switch (mode)
{
case InputGlyph.ActionSourceMode.ActionReference:
InputActionReference actionReference = _actionReference.objectReferenceValue as InputActionReference;
return actionReference != null ? actionReference.action : null;
case InputGlyph.ActionSourceMode.HotkeyTrigger:
Component component = _hotkeyTrigger.objectReferenceValue as Component;
if (component is UnityEngine.UI.IHotkeyTrigger trigger && trigger.HotkeyAction != null)
{
return trigger.HotkeyAction.action;
}
return null;
case InputGlyph.ActionSourceMode.ActionName:
return ResolveActionByName(_actionName.stringValue);
default:
return null;
}
}
private InputAction ResolveActionByName(string actionName)
{
if (string.IsNullOrWhiteSpace(actionName))
{
return null;
}
foreach (InputActionAsset asset in EnumerateInputActionAssets())
{
if (asset == null)
{
continue;
}
InputAction action = asset.FindAction(actionName, false);
if (action != null)
{
return action;
}
}
return null;
}
private IEnumerable<InputActionAsset> EnumerateInputActionAssets()
{
HashSet<InputActionAsset> visited = new HashSet<InputActionAsset>();
InputBindingManager[] managers = Resources.FindObjectsOfTypeAll<InputBindingManager>();
for (int i = 0; i < managers.Length; i++)
{
InputActionAsset asset = managers[i] != null ? managers[i].actions : null;
if (asset != null && visited.Add(asset))
{
yield return asset;
}
}
string[] guids = AssetDatabase.FindAssets("t:InputActionAsset");
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
InputActionAsset asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(path);
if (asset != null && visited.Add(asset))
{
yield return asset;
}
}
}
private static List<string> CollectCompositePartNames(InputAction action)
{
List<string> parts = new List<string>();
if (action == null)
{
return parts;
}
HashSet<string> uniqueParts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < action.bindings.Count; i++)
{
InputBinding binding = action.bindings[i];
if (!binding.isPartOfComposite || string.IsNullOrWhiteSpace(binding.name))
{
continue;
}
if (uniqueParts.Add(binding.name))
{
parts.Add(binding.name);
}
}
return parts;
}
private void BuildStyles()
{
if (_titleStyle == null)
{
_titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14
};
}
if (_sectionStyle == null)
{
_sectionStyle = new GUIStyle(EditorStyles.helpBox)
{
padding = new RectOffset(12, 12, 10, 10)
};
}
if (_hintStyle == null)
{
_hintStyle = new GUIStyle(EditorStyles.miniLabel)
{
wordWrap = true
};
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 72d9df70bb4f43f6a73be92b5f332871
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,296 +0,0 @@
using System;
using System.Collections.Generic;
using AlicizaX;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.UI;
[AddComponentMenu("UI/Input Glyph")]
public sealed class InputGlyph : InputGlyphBehaviourBase
{
public enum ActionSourceMode
{
ActionReference,
HotkeyTrigger,
ActionName
}
public enum OutputMode
{
Image,
Text
}
[Serializable]
public sealed class DeviceCategoryEvent
{
public InputDeviceWatcher.InputDeviceCategory category;
public UnityEvent onMatched;
public UnityEvent onNotMatched;
}
[Header("Source")]
[SerializeField] private ActionSourceMode actionSourceMode = ActionSourceMode.ActionReference;
[SerializeField] private InputActionReference actionReference;
[SerializeField] private Component hotkeyTrigger;
[SerializeField] private string actionName;
[SerializeField] private string compositePartName;
[Header("Output")]
[SerializeField] private OutputMode outputMode = OutputMode.Image;
[SerializeField] private Image targetImage;
[SerializeField] private TMP_Text targetText;
[Header("Platform Events")]
[SerializeField] private List<DeviceCategoryEvent> categoryEvents = new();
private Sprite _cachedSprite;
private string _templateText;
private string _cachedFormattedText;
private string _cachedReplacementToken;
private bool _hasInvokedCategoryEvent;
private InputDeviceWatcher.InputDeviceCategory _lastInvokedCategory;
#if UNITY_EDITOR
private void OnValidate()
{
AutoAssignHotkeyTrigger();
AutoAssignTarget();
}
#endif
protected override void OnEnable()
{
AutoAssignHotkeyTrigger();
AutoAssignTarget();
CacheTemplateText();
base.OnEnable();
InvokeCategoryEvents(true);
}
protected override void OnDeviceCategoryChanged(
InputDeviceWatcher.InputDeviceCategory previousCategory,
InputDeviceWatcher.InputDeviceCategory newCategory)
{
if (previousCategory == newCategory)
{
return;
}
InvokeCategoryEvents(false);
}
protected override void RefreshGlyph()
{
InputAction action = ResolveAction();
switch (outputMode)
{
case OutputMode.Image:
RefreshImage(action);
break;
case OutputMode.Text:
RefreshText(action);
break;
}
}
private void RefreshImage(InputAction action)
{
if (targetImage == null)
{
return;
}
if (action == null)
{
ClearImage();
return;
}
bool hasSprite = GlyphService.TryGetUISpriteForActionPath(action, compositePartName, CurrentCategory, out Sprite sprite);
if (!hasSprite)
{
sprite = null;
}
if (_cachedSprite != sprite || targetImage.sprite != sprite)
{
_cachedSprite = sprite;
targetImage.sprite = sprite;
}
}
private void RefreshText(InputAction action)
{
if (targetText == null)
{
return;
}
CacheTemplateText();
if (action == null)
{
ResetText();
return;
}
string replacementToken;
if (GlyphService.TryGetTMPTagForActionPath(action, compositePartName, CurrentCategory, out string tag, out string displayFallback))
{
replacementToken = tag;
}
else
{
replacementToken = displayFallback;
}
if (string.IsNullOrEmpty(replacementToken))
{
ResetText();
return;
}
string formattedText = Utility.Text.Format(_templateText, replacementToken);
if (_cachedReplacementToken == replacementToken
&& _cachedFormattedText == formattedText
&& targetText.text == formattedText)
{
return;
}
_cachedReplacementToken = replacementToken;
if (_cachedFormattedText != formattedText || targetText.text != formattedText)
{
_cachedFormattedText = formattedText;
targetText.text = formattedText;
}
}
private InputAction ResolveAction()
{
switch (actionSourceMode)
{
case ActionSourceMode.ActionReference:
return actionReference != null ? actionReference.action : null;
case ActionSourceMode.HotkeyTrigger:
return ResolveHotkeyAction();
case ActionSourceMode.ActionName:
return InputBindingManager.TryGetAction(actionName, out InputAction action) ? action : null;
default:
return null;
}
}
private InputAction ResolveHotkeyAction()
{
IHotkeyTrigger trigger = ResolveHotkeyTrigger();
return trigger != null && trigger.HotkeyAction != null ? trigger.HotkeyAction.action : null;
}
private IHotkeyTrigger ResolveHotkeyTrigger()
{
AutoAssignHotkeyTrigger();
return hotkeyTrigger as IHotkeyTrigger;
}
private void AutoAssignHotkeyTrigger()
{
if (actionSourceMode != ActionSourceMode.HotkeyTrigger || hotkeyTrigger != null)
{
return;
}
if (TryGetComponent(typeof(IHotkeyTrigger), out Component component))
{
hotkeyTrigger = component;
}
}
private void AutoAssignTarget()
{
switch (outputMode)
{
case OutputMode.Image:
if (targetImage == null)
{
targetImage = GetComponent<Image>();
}
break;
case OutputMode.Text:
if (targetText == null)
{
targetText = GetComponent<TMP_Text>();
}
break;
}
}
private void CacheTemplateText()
{
if (targetText == null)
{
return;
}
if (string.IsNullOrEmpty(_templateText))
{
_templateText = targetText.text;
}
}
private void ResetText()
{
_cachedReplacementToken = null;
_cachedFormattedText = null;
if (targetText != null && targetText.text != _templateText)
{
targetText.text = _templateText;
}
}
private void ClearImage()
{
_cachedSprite = null;
if (targetImage != null && targetImage.sprite != null)
{
targetImage.sprite = null;
}
}
private void InvokeCategoryEvents(bool force)
{
if (!force && _hasInvokedCategoryEvent && _lastInvokedCategory == CurrentCategory)
{
return;
}
_hasInvokedCategoryEvent = true;
_lastInvokedCategory = CurrentCategory;
if (categoryEvents == null)
{
return;
}
for (int i = 0; i < categoryEvents.Count; i++)
{
DeviceCategoryEvent categoryEvent = categoryEvents[i];
if (categoryEvent == null)
{
continue;
}
if (categoryEvent.category == CurrentCategory)
{
categoryEvent.onMatched?.Invoke();
}
else
{
categoryEvent.onNotMatched?.Invoke();
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 211cfb186fc74ca694ec6f7f4b0fd933
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,41 +0,0 @@
using UnityEngine;
public abstract class InputGlyphBehaviourBase : MonoBehaviour
{
protected InputDeviceWatcher.InputDeviceCategory CurrentCategory { get; private set; }
protected virtual void OnEnable()
{
CurrentCategory = InputDeviceWatcher.CurrentCategory;
InputDeviceWatcher.OnDeviceChanged += HandleDeviceChanged;
InputBindingManager.BindingsChanged += HandleBindingsChanged;
RefreshGlyph();
}
protected virtual void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= HandleDeviceChanged;
InputBindingManager.BindingsChanged -= HandleBindingsChanged;
}
private void HandleDeviceChanged(InputDeviceWatcher.InputDeviceCategory category)
{
InputDeviceWatcher.InputDeviceCategory previousCategory = CurrentCategory;
CurrentCategory = category;
OnDeviceCategoryChanged(previousCategory, category);
RefreshGlyph();
}
private void HandleBindingsChanged()
{
RefreshGlyph();
}
protected virtual void OnDeviceCategoryChanged(
InputDeviceWatcher.InputDeviceCategory previousCategory,
InputDeviceWatcher.InputDeviceCategory newCategory)
{
}
protected abstract void RefreshGlyph();
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a16393b81b47f1844a49d492284be475
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,245 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class TestRebindScript : MonoBehaviour
{
[Header("UI")] public UXButton btn;
public TextMeshProUGUI bindKeyText;
public Image targetImage;
[Tooltip("如果不使用 actionReference则用 name 在全局 manager 查找")]
public string actionName = "movement";
[Header("Optional composite part (WASD style)")] [Tooltip("如果需要绑定 composite 的某一部分(例如 Up/Down/Left/Right填这个留空表示绑定非 composite 或整体 binding")]
public string compositePartName = "";
[Header("Behavior")] [Tooltip("如果 true在 Prepare 后自动调用 ConfirmApply() 并保存;否则等待手动 ConfirmPrepared()/CancelPrepared()")]
public bool autoConfirm = false;
/// <summary>
/// 启动时初始化并订阅事件
/// </summary>
private void Start()
{
if (btn != null) btn.onClick.AddListener(OnBtnClicked);
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
InputBindingManager.BindingsChanged += OnBindingsChanged;
UpdateBindingText();
if (InputBindingManager.Instance != null)
{
// 订阅事件
InputBindingManager.Instance.OnRebindPrepare += OnRebindPrepareHandler;
InputBindingManager.Instance.OnApply += OnApplyHandler;
InputBindingManager.Instance.OnRebindEnd += OnRebindEndHandler;
InputBindingManager.Instance.OnRebindConflict += OnRebindConflictHandler;
}
}
/// <summary>
/// 禁用时取消订阅事件
/// </summary>
private void OnDisable()
{
if (btn != null) btn.onClick.RemoveListener(OnBtnClicked);
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
InputBindingManager.BindingsChanged -= OnBindingsChanged;
if (InputBindingManager.Instance != null)
{
InputBindingManager.Instance.OnRebindPrepare -= OnRebindPrepareHandler;
InputBindingManager.Instance.OnApply -= OnApplyHandler;
InputBindingManager.Instance.OnRebindEnd -= OnRebindEndHandler;
InputBindingManager.Instance.OnRebindConflict -= OnRebindConflictHandler;
}
}
/// <summary>
/// 重新绑定准备完成的处理器
/// </summary>
private void OnRebindPrepareHandler(InputBindingManager.RebindContext ctx)
{
if (IsTargetContext(ctx))
{
var disp = ctx.overridePath == InputBindingManager.NULL_BINDING ? "<Cleared>" : ctx.overridePath;
bindKeyText.text = disp;
if (autoConfirm) _ = ConfirmPreparedAsync();
}
}
/// <summary>
/// 应用重新绑定的处理器
/// </summary>
private void OnApplyHandler(bool success, HashSet<InputBindingManager.RebindContext> appliedContexts)
{
if (appliedContexts != null)
{
// 仅当任何应用/丢弃的上下文与此实例匹配时才更新
foreach (var ctx in appliedContexts)
{
if (IsTargetContext(ctx))
{
UpdateBindingText();
break;
}
}
}
}
/// <summary>
/// 重新绑定结束的处理器
/// </summary>
private void OnRebindEndHandler(bool success, InputBindingManager.RebindContext context)
{
if (IsTargetContext(context))
{
UpdateBindingText();
}
}
/// <summary>
/// 重新绑定冲突的处理器
/// </summary>
private void OnRebindConflictHandler(InputBindingManager.RebindContext prepared, InputBindingManager.RebindContext conflict)
{
// 如果准备的或冲突的上下文匹配此实例,则更新
if (IsTargetContext(prepared) || IsTargetContext(conflict))
{
UpdateBindingText();
}
}
/// <summary>
/// 设备变更的回调
/// </summary>
private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _)
{
UpdateBindingText();
}
private void OnBindingsChanged()
{
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;
// 必须匹配操作
if (ctx.action != action) return false;
// 如果指定了复合部分,需要匹配绑定索引
if (!string.IsNullOrEmpty(compositePartName))
{
// 获取上下文索引处的绑定
if (ctx.bindingIndex < 0 || ctx.bindingIndex >= action.bindings.Count)
return false;
var binding = action.bindings[ctx.bindingIndex];
// 检查绑定的名称是否与我们的复合部分匹配
return string.Equals(binding.name, compositePartName, StringComparison.OrdinalIgnoreCase);
}
// 如果未指定复合部分,仅匹配操作就足够了
return true;
}
/// <summary>
/// 按钮点击的回调
/// </summary>
private void OnBtnClicked()
{
// 使用管理器 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
{
var task = InputBindingManager.ConfirmApply();
return await task;
}
catch (Exception ex)
{
Debug.LogError(ex);
return false;
}
}
/// <summary>
/// 取消准备好的重新绑定
/// </summary>
public void CancelPrepared()
{
InputBindingManager.DiscardPrepared();
// UpdateBindingText 将通过 OnApply 事件自动调用
}
/// <summary>
/// 更新绑定文本和图标显示
/// </summary>
private void UpdateBindingText()
{
var action = GetAction();
var deviceCat = InputDeviceWatcher.CurrentCategory;
if (action == null)
{
bindKeyText.text = "<no action>";
if (targetImage != null) targetImage.sprite = null;
return;
}
bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName, deviceCat);
try
{
if (GlyphService.TryGetUISpriteForActionPath(action, compositePartName, deviceCat, out Sprite sprite))
{
if (targetImage != null) targetImage.sprite = sprite;
}
else
{
if (targetImage != null) targetImage.sprite = null;
}
}
catch
{
if (targetImage != null) targetImage.sprite = null;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 5ec2871cc330674438e5ae0aea9e616b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -10,7 +10,7 @@
"BuildPipeline": "EditorSimulateBuildPipeline",
"PackageName": "DefaultPackage",
"PackageVersion": "Simulate",
"PackageNote": "2026/3/20 11:28:53",
"PackageNote": "2026/3/20 16:23:36",
"AssetList": [
{
"Address": "Click",
@ -151,7 +151,7 @@
"UnityCRC": 0,
"FileHash": "82253a65841b8f1db612149ec8c9a317",
"FileCRC": 0,
"FileSize": 57247,
"FileSize": 56023,
"Encrypted": false,
"Tags": [
"UI"
@ -163,7 +163,7 @@
"UnityCRC": 0,
"FileHash": "cf84038a2a1620e6ff49815bcd8e6a73",
"FileCRC": 0,
"FileSize": 21629,
"FileSize": 22136,
"Encrypted": false,
"Tags": [
"UI"

@ -1 +1 @@
Subproject commit 4364f4673d24c4a27062c46526ca5f6687675f5e
Subproject commit 175a96c230faad04491be0c8234a3a2851dd4d15