更新详见各模块Package
This commit is contained in:
parent
93023a995d
commit
a3e1cf0407
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2a23416e80d6df41a157dc645840eb1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22b092f05744baa4383a9dc15a488e22
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb7e653c16f0005419e1eb9226655176
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cc7ec0447a544ac984f3aac7a7b71d4
|
||||
timeCreated: 1764917633
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9368556ed4729ae618e0a19d3a7925b
|
||||
timeCreated: 1773811724
|
||||
@ -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;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c25ae9d04ef4e03a723135aa298de16
|
||||
timeCreated: 1765271070
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e78f6224467e13742a70115f1942d941
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 096043edb2be8224f8564b40992f588b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53ed017cef844d11842ba16553c6391d
|
||||
timeCreated: 1764917621
|
||||
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 241d5382b2b4f274596c73f14a40cb8d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96b54c2be64d4891a50c4db8b36bf839
|
||||
timeCreated: 1764923573
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72d9df70bb4f43f6a73be92b5f332871
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 211cfb186fc74ca694ec6f7f4b0fd933
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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();
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a16393b81b47f1844a49d492284be475
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ec2871cc330674438e5ae0aea9e616b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@ -1 +1 @@
|
||||
1c55ee5a
|
||||
1a5adabe
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user