[Opt]优化输入系统

This commit is contained in:
陈思海 2026-04-29 11:04:57 +08:00
parent 526341579a
commit 7d5ae32361
16 changed files with 1088 additions and 1504 deletions

View File

@ -6,7 +6,8 @@
"GUID:6055be8ebefd69e48b49212b09b47b2f", "GUID:6055be8ebefd69e48b49212b09b47b2f",
"GUID:760f1778adc613f49a4394fb41ff0bbc", "GUID:760f1778adc613f49a4394fb41ff0bbc",
"GUID:1619e00706139ce488ff80c0daeea8e7", "GUID:1619e00706139ce488ff80c0daeea8e7",
"GUID:fb064c8bf96bac94e90d2f39090daa94" "GUID:fb064c8bf96bac94e90d2f39090daa94",
"GUID:75469ad4d38634e559750d17036d5f7c"
], ],
"includePlatforms": [ "includePlatforms": [
"Editor" "Editor"

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 241d5382b2b4f274596c73f14a40cb8d guid: 43899fd2849604a49acbdbd7b3450fb6
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@ -0,0 +1,476 @@
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
using UnityEngine.InputSystem;
[CustomEditor(typeof(InputGlyphDatabase))]
public sealed class InputGlyphDatabaseEditor : Editor
{
public override void OnInspectorGUI()
{
}
[OnOpenAsset(0)]
private static bool OpenAsset(int instanceID, int line)
{
InputGlyphDatabase database = EditorUtility.InstanceIDToObject(instanceID) as InputGlyphDatabase;
if (database == null)
{
return false;
}
InputGlyphDatabaseWindow.OpenFromAsset(database);
return true;
}
}
internal sealed class InputGlyphDatabaseWindow : EditorWindow
{
private const string TablesPropertyName = "tables";
private const string PlaceholderSpritePropertyName = "placeholderSprite";
private const string DeviceNamePropertyName = "deviceName";
private const string SpriteSheetPropertyName = "spriteSheetTexture";
private const string PlatformIconPropertyName = "platformIcons";
private const string EntriesPropertyName = "entries";
private const string EntrySpritePropertyName = "Sprite";
private const string EntryActionPropertyName = "action";
private const float SidebarWidth = 220f;
private const float EntryMinHeight = 76f;
private const float PreviewSize = 42f;
private static readonly string[] DefaultTableNames = { "Keyboard", "Xbox", "PlayStation", "Other" };
private InputGlyphDatabase _database;
private SerializedObject _serializedDatabase;
private SerializedProperty _tablesProperty;
private SerializedProperty _placeholderSpriteProperty;
private Vector2 _tableScroll;
private Vector2 _entryScroll;
private int _selectedTable;
private string _search = string.Empty;
private GUIStyle _sidebarStyle;
private GUIStyle _selectedTableStyle;
private GUIStyle _tableStyle;
private GUIStyle _entryCardStyle;
private GUIStyle _headerStyle;
private GUIContent _addIcon;
private GUIContent _removeIcon;
private GUIContent _saveIcon;
private GUIContent _refreshIcon;
private GUIContent _searchIcon;
private GUIContent _settingsIcon;
internal static void OpenFromAsset(InputGlyphDatabase database)
{
InputGlyphDatabaseWindow window = GetWindow<InputGlyphDatabaseWindow>(true, "Input Glyph Database", true);
window.SetDatabase(database);
window.minSize = new Vector2(940f, 560f);
window.Show();
}
private void OnEnable()
{
BuildStyles();
}
private void SetDatabase(InputGlyphDatabase database)
{
_database = database;
_serializedDatabase = new SerializedObject(_database);
_tablesProperty = _serializedDatabase.FindProperty(TablesPropertyName);
_placeholderSpriteProperty = _serializedDatabase.FindProperty(PlaceholderSpritePropertyName);
_selectedTable = Mathf.Clamp(_selectedTable, 0, Mathf.Max(0, _tablesProperty.arraySize - 1));
titleContent = new GUIContent(database.name, EditorGUIUtility.IconContent("d_ScriptableObject Icon").image);
}
private void OnGUI()
{
if (_database == null || _serializedDatabase == null)
{
EditorApplication.delayCall += CloseIfDatabaseMissing;
return;
}
BuildStyles();
_serializedDatabase.Update();
DrawToolbar();
Rect contentRect = new Rect(0f, EditorGUIUtility.singleLineHeight + 8f, position.width, position.height - EditorGUIUtility.singleLineHeight - 8f);
Rect sidebarRect = new Rect(contentRect.x, contentRect.y, SidebarWidth, contentRect.height);
Rect mainRect = new Rect(sidebarRect.xMax + 1f, contentRect.y, contentRect.width - SidebarWidth - 1f, contentRect.height);
DrawSidebar(sidebarRect);
DrawMainPanel(mainRect);
if (_serializedDatabase.ApplyModifiedProperties())
{
MarkDirty();
}
}
private void CloseIfDatabaseMissing()
{
if (this != null && _database == null)
{
Close();
}
}
private void DrawToolbar()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
if (GUILayout.Button(_saveIcon, EditorStyles.toolbarButton, GUILayout.Width(28f)))
{
Save();
}
if (GUILayout.Button(_refreshIcon, EditorStyles.toolbarButton, GUILayout.Width(28f)))
{
_database.EditorRefreshCache();
Repaint();
}
GUILayout.Space(8f);
GUILayout.Label(_searchIcon, GUILayout.Width(20f));
_search = GUILayout.TextField(_search, EditorStyles.toolbarSearchField, GUILayout.Width(240f));
if (!string.IsNullOrEmpty(_search) && GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(22f)))
{
_search = string.Empty;
GUI.FocusControl(null);
}
GUILayout.FlexibleSpace();
DrawObjectName();
}
}
private void DrawSidebar(Rect rect)
{
GUI.Box(rect, GUIContent.none, _sidebarStyle);
GUILayout.BeginArea(new Rect(rect.x + 8f, rect.y + 8f, rect.width - 16f, rect.height - 16f));
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label("Tables", _headerStyle);
GUILayout.FlexibleSpace();
if (GUILayout.Button(_addIcon, EditorStyles.iconButton, GUILayout.Width(24f), GUILayout.Height(22f)))
{
AddTable(NextTableName());
}
}
_tableScroll = EditorGUILayout.BeginScrollView(_tableScroll);
for (int i = 0; i < _tablesProperty.arraySize; i++)
{
SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(i);
DrawTableButton(i, table);
}
EditorGUILayout.EndScrollView();
GUILayout.Space(6f);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button(_settingsIcon, EditorStyles.iconButton, GUILayout.Width(28f), GUILayout.Height(24f)))
{
_selectedTable = -1;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(_addIcon, EditorStyles.iconButton, GUILayout.Width(28f), GUILayout.Height(24f)))
{
CreateMissingDefaultTables();
}
}
GUILayout.EndArea();
}
private void DrawTableButton(int index, SerializedProperty table)
{
SerializedProperty nameProperty = table.FindPropertyRelative(DeviceNamePropertyName);
SerializedProperty iconProperty = table.FindPropertyRelative(PlatformIconPropertyName);
string tableName = string.IsNullOrWhiteSpace(nameProperty.stringValue) ? "Table " + (index + 1) : nameProperty.stringValue;
bool selected = _selectedTable == index;
GUIStyle style = selected ? _selectedTableStyle : _tableStyle;
Rect rect = GUILayoutUtility.GetRect(1f, 44f, GUILayout.ExpandWidth(true));
if (GUI.Button(rect, GUIContent.none, style))
{
_selectedTable = index;
GUI.FocusControl(null);
}
Rect iconRect = new Rect(rect.x + 8f, rect.y + 6f, 32f, 32f);
DrawSprite(iconRect, iconProperty.objectReferenceValue as Sprite);
GUI.Label(new Rect(iconRect.xMax + 8f, rect.y + 6f, rect.width - 76f, 18f), tableName, EditorStyles.boldLabel);
GUI.Label(new Rect(iconRect.xMax + 8f, rect.y + 24f, rect.width - 76f, 16f), EntryCountLabel(table), EditorStyles.miniLabel);
Rect deleteRect = new Rect(rect.xMax - 28f, rect.y + 10f, 22f, 22f);
if (GUI.Button(deleteRect, _removeIcon, EditorStyles.iconButton))
{
RemoveTable(index);
GUIUtility.ExitGUI();
}
}
private void DrawMainPanel(Rect rect)
{
GUILayout.BeginArea(new Rect(rect.x + 18f, rect.y + 14f, rect.width - 36f, rect.height - 22f));
if (_selectedTable < 0 || _selectedTable >= _tablesProperty.arraySize)
{
DrawSettings();
}
else
{
DrawTable(_selectedTable);
}
GUILayout.EndArea();
}
private void DrawSettings()
{
GUILayout.Label("Database", _headerStyle);
EditorGUILayout.Space(8f);
EditorGUILayout.PropertyField(_placeholderSpriteProperty, GUIContent.none);
Rect previewRect = GUILayoutUtility.GetRect(PreviewSize, PreviewSize, GUILayout.Width(PreviewSize), GUILayout.Height(PreviewSize));
DrawSprite(previewRect, _placeholderSpriteProperty.objectReferenceValue as Sprite);
}
private void DrawTable(int tableIndex)
{
SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(tableIndex);
SerializedProperty nameProperty = table.FindPropertyRelative(DeviceNamePropertyName);
SerializedProperty sheetProperty = table.FindPropertyRelative(SpriteSheetPropertyName);
SerializedProperty iconProperty = table.FindPropertyRelative(PlatformIconPropertyName);
SerializedProperty entriesProperty = table.FindPropertyRelative(EntriesPropertyName);
using (new EditorGUILayout.HorizontalScope())
{
DrawSprite(GUILayoutUtility.GetRect(52f, 52f, GUILayout.Width(52f), GUILayout.Height(52f)), iconProperty.objectReferenceValue as Sprite);
using (new EditorGUILayout.VerticalScope())
{
nameProperty.stringValue = EditorGUILayout.TextField(nameProperty.stringValue, _headerStyle);
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PropertyField(iconProperty, GUIContent.none, GUILayout.MinWidth(160f));
EditorGUILayout.PropertyField(sheetProperty, GUIContent.none, GUILayout.MinWidth(160f));
if (GUILayout.Button(_addIcon, EditorStyles.iconButton, GUILayout.Width(28f), GUILayout.Height(22f)))
{
AddEntry(entriesProperty);
}
}
}
}
EditorGUILayout.Space(12f);
_entryScroll = EditorGUILayout.BeginScrollView(_entryScroll);
for (int i = 0; i < entriesProperty.arraySize; i++)
{
SerializedProperty entry = entriesProperty.GetArrayElementAtIndex(i);
if (!EntryMatches(entry))
{
continue;
}
DrawEntry(entriesProperty, i, entry);
}
EditorGUILayout.EndScrollView();
}
private void DrawEntry(SerializedProperty entriesProperty, int index, SerializedProperty entry)
{
SerializedProperty spriteProperty = entry.FindPropertyRelative(EntrySpritePropertyName);
SerializedProperty actionProperty = entry.FindPropertyRelative(EntryActionPropertyName);
float spriteHeight = EditorGUI.GetPropertyHeight(spriteProperty, GUIContent.none, true);
float actionHeight = EditorGUI.GetPropertyHeight(actionProperty, GUIContent.none, true);
float contentHeight = Mathf.Max(PreviewSize, spriteHeight + actionHeight + 8f);
Rect rect = GUILayoutUtility.GetRect(1f, Mathf.Max(EntryMinHeight, contentHeight + 16f), GUILayout.ExpandWidth(true));
GUI.Box(rect, GUIContent.none, _entryCardStyle);
Rect previewRect = new Rect(rect.x + 8f, rect.y + 8f, PreviewSize, PreviewSize);
DrawSprite(previewRect, spriteProperty.objectReferenceValue as Sprite);
Rect removeRect = new Rect(rect.xMax - 32f, rect.y + 8f, 24f, 24f);
float fieldX = previewRect.xMax + 12f;
float fieldWidth = Mathf.Max(120f, removeRect.x - fieldX - 10f);
Rect spriteRect = new Rect(fieldX, rect.y + 8f, fieldWidth, spriteHeight);
Rect actionRect = new Rect(fieldX, spriteRect.yMax + 8f, fieldWidth, actionHeight);
EditorGUI.PropertyField(spriteRect, spriteProperty, GUIContent.none, true);
EditorGUI.PropertyField(actionRect, actionProperty, GUIContent.none, true);
if (GUI.Button(removeRect, _removeIcon, EditorStyles.iconButton))
{
entriesProperty.DeleteArrayElementAtIndex(index);
MarkDirty();
GUIUtility.ExitGUI();
}
}
private bool EntryMatches(SerializedProperty entry)
{
if (string.IsNullOrWhiteSpace(_search))
{
return true;
}
SerializedProperty spriteProperty = entry.FindPropertyRelative(EntrySpritePropertyName);
SerializedProperty actionProperty = entry.FindPropertyRelative(EntryActionPropertyName);
string search = _search.Trim();
Sprite sprite = spriteProperty.objectReferenceValue as Sprite;
Object actionObject = actionProperty.objectReferenceValue;
return Contains(sprite != null ? sprite.name : string.Empty, search)
|| Contains(actionObject != null ? actionObject.name : string.Empty, search);
}
private static bool Contains(string value, string search)
{
return !string.IsNullOrEmpty(value) && value.IndexOf(search, System.StringComparison.OrdinalIgnoreCase) >= 0;
}
private void AddEntry(SerializedProperty entriesProperty)
{
int index = entriesProperty.arraySize;
entriesProperty.InsertArrayElementAtIndex(index);
SerializedProperty entry = entriesProperty.GetArrayElementAtIndex(index);
entry.FindPropertyRelative(EntrySpritePropertyName).objectReferenceValue = null;
entry.FindPropertyRelative(EntryActionPropertyName).objectReferenceValue = null;
MarkDirty();
}
private void AddTable(string tableName)
{
int index = _tablesProperty.arraySize;
_tablesProperty.InsertArrayElementAtIndex(index);
SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(index);
table.FindPropertyRelative(DeviceNamePropertyName).stringValue = tableName;
table.FindPropertyRelative(SpriteSheetPropertyName).objectReferenceValue = null;
table.FindPropertyRelative(PlatformIconPropertyName).objectReferenceValue = null;
table.FindPropertyRelative(EntriesPropertyName).ClearArray();
_selectedTable = index;
MarkDirty();
}
private void RemoveTable(int index)
{
_tablesProperty.DeleteArrayElementAtIndex(index);
_selectedTable = Mathf.Clamp(_selectedTable, -1, _tablesProperty.arraySize - 1);
MarkDirty();
}
private void CreateMissingDefaultTables()
{
for (int i = 0; i < DefaultTableNames.Length; i++)
{
if (!HasTable(DefaultTableNames[i]))
{
AddTable(DefaultTableNames[i]);
}
}
}
private bool HasTable(string tableName)
{
for (int i = 0; i < _tablesProperty.arraySize; i++)
{
SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(i);
if (string.Equals(table.FindPropertyRelative(DeviceNamePropertyName).stringValue, tableName, System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private string NextTableName()
{
for (int i = 0; i < DefaultTableNames.Length; i++)
{
if (!HasTable(DefaultTableNames[i]))
{
return DefaultTableNames[i];
}
}
return "Table " + (_tablesProperty.arraySize + 1);
}
private static string EntryCountLabel(SerializedProperty table)
{
SerializedProperty entries = table.FindPropertyRelative(EntriesPropertyName);
return entries.arraySize + " glyphs";
}
private void DrawObjectName()
{
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.ObjectField(_database, typeof(InputGlyphDatabase), false, GUILayout.Width(260f));
}
}
private static void DrawSprite(Rect rect, Sprite sprite)
{
if (sprite == null)
{
GUI.Box(rect, GUIContent.none, EditorStyles.helpBox);
return;
}
Texture2D texture = AssetPreview.GetAssetPreview(sprite) ?? AssetPreview.GetMiniThumbnail(sprite);
if (texture != null)
{
GUI.DrawTexture(rect, texture, ScaleMode.ScaleToFit, true);
}
}
private void Save()
{
_serializedDatabase.ApplyModifiedProperties();
MarkDirty();
AssetDatabase.SaveAssetIfDirty(_database);
}
private void MarkDirty()
{
_database.EditorRefreshCache();
EditorUtility.SetDirty(_database);
}
private void BuildStyles()
{
_addIcon ??= EditorGUIUtility.IconContent("Toolbar Plus");
_removeIcon ??= EditorGUIUtility.IconContent("TreeEditor.Trash");
_saveIcon ??= EditorGUIUtility.IconContent("SaveActive");
_refreshIcon ??= EditorGUIUtility.IconContent("Refresh");
_searchIcon ??= EditorGUIUtility.IconContent("Search Icon");
_settingsIcon ??= EditorGUIUtility.IconContent("d__Popup");
_sidebarStyle ??= new GUIStyle(EditorStyles.helpBox)
{
padding = new RectOffset(8, 8, 8, 8)
};
_tableStyle ??= new GUIStyle(EditorStyles.toolbarButton)
{
alignment = TextAnchor.MiddleLeft,
fixedHeight = 44f
};
_selectedTableStyle ??= new GUIStyle(_tableStyle)
{
normal = { background = Texture2D.grayTexture },
fontStyle = FontStyle.Bold
};
_entryCardStyle ??= new GUIStyle(EditorStyles.helpBox)
{
padding = new RectOffset(8, 8, 8, 8)
};
_headerStyle ??= new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 15
};
}
}

View File

@ -9,6 +9,9 @@ using UnityEngine.InputSystem;
[CanEditMultipleObjects] [CanEditMultipleObjects]
public sealed class InputGlyphEditor : Editor public sealed class InputGlyphEditor : Editor
{ {
private static readonly List<InputActionAsset> CachedActionAssets = new(16);
private static bool _actionAssetCacheDirty = true;
private SerializedProperty _actionSourceMode; private SerializedProperty _actionSourceMode;
private SerializedProperty _actionReference; private SerializedProperty _actionReference;
private SerializedProperty _hotkeyTrigger; private SerializedProperty _hotkeyTrigger;
@ -86,6 +89,11 @@ public sealed class InputGlyphEditor : Editor
break; break;
case InputGlyph.ActionSourceMode.ActionName: case InputGlyph.ActionSourceMode.ActionName:
EditorGUILayout.PropertyField(_actionName, new GUIContent("Action Name")); EditorGUILayout.PropertyField(_actionName, new GUIContent("Action Name"));
if (GUILayout.Button("Refresh Action Asset Cache", EditorStyles.miniButton))
{
_actionAssetCacheDirty = true;
}
EditorGUILayout.LabelField("Supports ActionName or MapName/ActionName.", _hintStyle); EditorGUILayout.LabelField("Supports ActionName or MapName/ActionName.", _hintStyle);
break; break;
} }
@ -225,14 +233,29 @@ public sealed class InputGlyphEditor : Editor
private IEnumerable<InputActionAsset> EnumerateInputActionAssets() private IEnumerable<InputActionAsset> EnumerateInputActionAssets()
{ {
HashSet<InputActionAsset> visited = new HashSet<InputActionAsset>(); EnsureActionAssetCache();
for (int i = 0; i < CachedActionAssets.Count; i++)
{
yield return CachedActionAssets[i];
}
}
private static void EnsureActionAssetCache()
{
if (!_actionAssetCacheDirty)
{
return;
}
_actionAssetCacheDirty = false;
CachedActionAssets.Clear();
InputBindingManager[] managers = Resources.FindObjectsOfTypeAll<InputBindingManager>(); InputBindingManager[] managers = Resources.FindObjectsOfTypeAll<InputBindingManager>();
for (int i = 0; i < managers.Length; i++) for (int i = 0; i < managers.Length; i++)
{ {
InputActionAsset asset = managers[i] != null ? managers[i].actions : null; InputActionAsset asset = managers[i] != null ? managers[i].actions : null;
if (asset != null && visited.Add(asset)) if (asset != null && !CachedActionAssets.Contains(asset))
{ {
yield return asset; CachedActionAssets.Add(asset);
} }
} }
@ -241,9 +264,9 @@ public sealed class InputGlyphEditor : Editor
{ {
string path = AssetDatabase.GUIDToAssetPath(guids[i]); string path = AssetDatabase.GUIDToAssetPath(guids[i]);
InputActionAsset asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(path); InputActionAsset asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(path);
if (asset != null && visited.Add(asset)) if (asset != null && !CachedActionAssets.Contains(asset))
{ {
yield return asset; CachedActionAssets.Add(asset);
} }
} }
} }

View File

@ -5,7 +5,8 @@
"GUID:6055be8ebefd69e48b49212b09b47b2f", "GUID:6055be8ebefd69e48b49212b09b47b2f",
"GUID:80ecb87cae9c44d19824e70ea7229748", "GUID:80ecb87cae9c44d19824e70ea7229748",
"GUID:75469ad4d38634e559750d17036d5f7c", "GUID:75469ad4d38634e559750d17036d5f7c",
"GUID:1619e00706139ce488ff80c0daeea8e7" "GUID:1619e00706139ce488ff80c0daeea8e7",
"GUID:33661e06c33d31b4c9223810bf503247"
], ],
"includePlatforms": [], "includePlatforms": [],
"excludePlatforms": [], "excludePlatforms": [],

View File

@ -1,8 +1,12 @@
using System; using System;
using System.Collections.Generic; using Cysharp.Text;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
/// <summary>
/// ? Input System ????? UI Sprite?TMP Sprite Tag ????????
/// ?????????????????????? SetDatabase ???
/// </summary>
public static class GlyphService public static class GlyphService
{ {
private static readonly string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" }; private static readonly string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" };
@ -10,24 +14,44 @@ public static class GlyphService
private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" }; private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" };
private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" }; private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" };
private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
private static readonly Dictionary<string, string> DisplayNameCache = new(StringComparer.Ordinal); private const int InitialCacheCapacity = 64;
private static readonly Dictionary<int, string> SpriteTagCache = new(); private static string[] DisplayNameKeys = new string[InitialCacheCapacity];
private static string[] DisplayNameValues = new string[InitialCacheCapacity];
private static int DisplayNameCount;
private static int[] SpriteTagKeys = new int[InitialCacheCapacity];
private static string[] SpriteTagValues = new string[InitialCacheCapacity];
private static int SpriteTagCount;
private static InputGlyphDatabase _database; private static InputGlyphDatabase _database;
/// <summary>
/// ???? Glyph ????????????????????
/// ??????????????? Resources.Load?
/// </summary>
public static void SetDatabase(InputGlyphDatabase database)
{
_database = database;
}
/// <summary>
/// ????????????
/// </summary>
public static void ClearDatabase()
{
_database = null;
}
static InputGlyphDatabase Database static InputGlyphDatabase Database
{ {
get get
{ {
if (_database == null)
{
_database = Resources.Load<InputGlyphDatabase>("InputGlyphDatabase");
}
return _database; return _database;
} }
} }
/// <summary>
/// ??????????????????????????
/// </summary>
public static string GetBindingControlPath( public static string GetBindingControlPath(
InputAction action, InputAction action,
string compositePartName = null, string compositePartName = null,
@ -46,6 +70,9 @@ public static class GlyphService
return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride); return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride);
} }
/// <summary>
/// ? Action ?????????? TMP Sprite Tag?????????????
/// </summary>
public static bool TryGetTMPTagForActionPath( public static bool TryGetTMPTagForActionPath(
InputAction action, InputAction action,
string compositePartName, string compositePartName,
@ -69,6 +96,9 @@ public static class GlyphService
return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db); return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db);
} }
/// <summary>
/// ? Action ?????????????????? UI Sprite?
/// </summary>
public static bool TryGetUISpriteForActionPath( public static bool TryGetUISpriteForActionPath(
InputAction action, InputAction action,
string compositePartName, string compositePartName,
@ -90,6 +120,9 @@ public static class GlyphService
return TryGetUISpriteForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out sprite, db); return TryGetUISpriteForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out sprite, db);
} }
/// <summary>
/// 将原始控制路径解析为 TMP Sprite Tag失败时回退为可读控制名。
/// </summary>
public static bool TryGetTMPTagForActionPath( public static bool TryGetTMPTagForActionPath(
string controlPath, string controlPath,
InputDeviceWatcher.InputDeviceCategory device, InputDeviceWatcher.InputDeviceCategory device,
@ -109,6 +142,9 @@ public static class GlyphService
return true; return true;
} }
/// <summary>
/// 通过 Glyph 数据库查找表将原始控制路径解析为 UI Sprite。
/// </summary>
public static bool TryGetUISpriteForActionPath( public static bool TryGetUISpriteForActionPath(
string controlPath, string controlPath,
InputDeviceWatcher.InputDeviceCategory device, InputDeviceWatcher.InputDeviceCategory device,
@ -120,6 +156,9 @@ public static class GlyphService
return db != null && db.TryGetSprite(controlPath, device, out sprite); return db != null && db.TryGetSprite(controlPath, device, out sprite);
} }
/// <summary>
/// ?? Action ?????????????
/// </summary>
public static string GetDisplayNameFromInputAction( public static string GetDisplayNameFromInputAction(
InputAction action, InputAction action,
string compositePartName = null, string compositePartName = null,
@ -134,6 +173,10 @@ public static class GlyphService
return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display; return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display;
} }
/// <summary>
/// ?????????????????
/// ??????????????????????????
/// </summary>
public static string GetDisplayNameFromControlPath(string controlPath) public static string GetDisplayNameFromControlPath(string controlPath)
{ {
if (string.IsNullOrWhiteSpace(controlPath)) if (string.IsNullOrWhiteSpace(controlPath))
@ -141,24 +184,28 @@ public static class GlyphService
return string.Empty; return string.Empty;
} }
if (DisplayNameCache.TryGetValue(controlPath, out string cachedDisplayName)) int cacheIndex = IndexOf(DisplayNameKeys, DisplayNameCount, controlPath);
if (cacheIndex >= 0)
{ {
return cachedDisplayName; return DisplayNameValues[cacheIndex];
} }
string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice); string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice);
if (!string.IsNullOrWhiteSpace(humanReadable)) if (!string.IsNullOrWhiteSpace(humanReadable))
{ {
DisplayNameCache[controlPath] = humanReadable; AddDisplayNameCache(controlPath, humanReadable);
return humanReadable; return humanReadable;
} }
int separatorIndex = controlPath.LastIndexOf('/'); int separatorIndex = controlPath.LastIndexOf('/');
string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars); string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars);
DisplayNameCache[controlPath] = last; AddDisplayNameCache(controlPath, last);
return last; return last;
} }
/// <summary>
/// ?? binding group ????????????????????
/// </summary>
public static bool TryGetBindingControl( public static bool TryGetBindingControl(
InputAction action, InputAction action,
string compositePartName, string compositePartName,
@ -269,8 +316,7 @@ public static class GlyphService
if (tokenLength > 0) if (tokenLength > 0)
{ {
string token = groups.Substring(tokenStart, tokenLength); if (ContainsAny(groups, tokenStart, tokenLength, hints))
if (ContainsAny(token, hints))
{ {
return true; return true;
} }
@ -290,16 +336,112 @@ public static class GlyphService
} }
int instanceId = sprite.GetInstanceID(); int instanceId = sprite.GetInstanceID();
if (SpriteTagCache.TryGetValue(instanceId, out string cachedTag)) int cacheIndex = IndexOf(SpriteTagKeys, SpriteTagCount, instanceId);
if (cacheIndex >= 0)
{ {
return cachedTag; return SpriteTagValues[cacheIndex];
} }
cachedTag = $"<sprite name=\"{sprite.name}\">"; string cachedTag = ZString.Concat("<sprite name=\"", sprite.name, "\">");
SpriteTagCache[instanceId] = cachedTag; AddSpriteTagCache(instanceId, cachedTag);
return cachedTag; return cachedTag;
} }
private static void AddDisplayNameCache(string key, string value)
{
if (DisplayNameCount == DisplayNameKeys.Length)
{
Array.Resize(ref DisplayNameKeys, DisplayNameKeys.Length << 1);
Array.Resize(ref DisplayNameValues, DisplayNameValues.Length << 1);
}
DisplayNameKeys[DisplayNameCount] = key;
DisplayNameValues[DisplayNameCount] = value;
DisplayNameCount++;
}
private static void AddSpriteTagCache(int key, string value)
{
if (SpriteTagCount == SpriteTagKeys.Length)
{
Array.Resize(ref SpriteTagKeys, SpriteTagKeys.Length << 1);
Array.Resize(ref SpriteTagValues, SpriteTagValues.Length << 1);
}
SpriteTagKeys[SpriteTagCount] = key;
SpriteTagValues[SpriteTagCount] = value;
SpriteTagCount++;
}
private static int IndexOf(string[] values, int count, string value)
{
for (int i = 0; i < count; i++)
{
if (string.Equals(values[i], value, StringComparison.Ordinal))
{
return i;
}
}
return -1;
}
private static int IndexOf(int[] values, int count, int value)
{
for (int i = 0; i < count; i++)
{
if (values[i] == value)
{
return i;
}
}
return -1;
}
private static bool ContainsAny(string source, int startIndex, int length, string[] hints)
{
if (string.IsNullOrEmpty(source) || hints == null || length <= 0)
{
return false;
}
for (int i = 0; i < hints.Length; i++)
{
if (IndexOfIgnoreCase(source, startIndex, length, hints[i]) >= 0)
{
return true;
}
}
return false;
}
private static int IndexOfIgnoreCase(string source, int startIndex, int length, string value)
{
if (string.IsNullOrEmpty(value) || value.Length > length)
{
return -1;
}
int end = startIndex + length - value.Length;
for (int i = startIndex; i <= end; i++)
{
int valueIndex = 0;
while (valueIndex < value.Length && char.ToUpperInvariant(source[i + valueIndex]) == char.ToUpperInvariant(value[valueIndex]))
{
valueIndex++;
}
if (valueIndex == value.Length)
{
return i;
}
}
return -1;
}
private static bool ContainsAny(string source, string[] hints) private static bool ContainsAny(string source, string[] hints)
{ {
if (string.IsNullOrWhiteSpace(source) || hints == null) if (string.IsNullOrWhiteSpace(source) || hints == null)

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
@ -64,25 +63,19 @@ public static class InputActionReader
} }
} }
// 记录“本次按下已消费”的键,用于 Once 语义。 private const int InitialKeyCapacity = 64;
private static readonly HashSet<InputReadKey> PressedKeys = new(); private static InputReadKey[] PressedKeys = new InputReadKey[InitialKeyCapacity];
// 记录当前处于开启状态的切换键。 private static int PressedKeyCount;
private static readonly HashSet<InputReadKey> ToggledKeys = new(); private static InputReadKey[] ToggledKeys = new InputReadKey[InitialKeyCapacity];
private static int ToggledKeyCount;
/// <summary> /// <summary>
/// 直接读取指定 Action 的值。 /// 直接读取指定 Action 的值。
/// </summary> /// </summary>
public static T ReadValue<T>(string actionName) where T : struct public static T ReadValue<T>(string actionName) where T : struct
{ {
return ResolveAction(actionName).ReadValue<T>(); InputAction inputAction = ResolveAction(actionName);
} return inputAction != null ? inputAction.ReadValue<T>() : default;
/// <summary>
/// 以 object 形式读取指定 Action 的值。
/// </summary>
public static object ReadValue(string actionName)
{
return ResolveAction(actionName).ReadValueAsObject();
} }
/// <summary> /// <summary>
@ -91,7 +84,7 @@ public static class InputActionReader
public static bool TryReadValue<T>(string actionName, out T value) where T : struct public static bool TryReadValue<T>(string actionName, out T value) where T : struct
{ {
InputAction inputAction = ResolveAction(actionName); InputAction inputAction = ResolveAction(actionName);
if (inputAction.IsPressed()) if (inputAction != null && inputAction.IsPressed())
{ {
value = inputAction.ReadValue<T>(); value = inputAction.ReadValue<T>();
return true; return true;
@ -101,22 +94,6 @@ public static class InputActionReader
return false; 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> /// <summary>
/// 只在本次按下的第一帧返回 true并输出当前值。 /// 只在本次按下的第一帧返回 true并输出当前值。
/// owner 用来隔离不同对象的读取状态。 /// owner 用来隔离不同对象的读取状态。
@ -139,9 +116,14 @@ public static class InputActionReader
public static bool ReadButton(string actionName) public static bool ReadButton(string actionName)
{ {
InputAction inputAction = ResolveAction(actionName); InputAction inputAction = ResolveAction(actionName);
if (inputAction == null)
{
return false;
}
if (inputAction.type == InputActionType.Button) if (inputAction.type == InputActionType.Button)
{ {
return Convert.ToBoolean(inputAction.ReadValueAsObject()); return inputAction.IsPressed();
} }
throw new NotSupportedException("[InputActionReader] The Input Action must be a button type."); throw new NotSupportedException("[InputActionReader] The Input Action must be a button type.");
@ -201,7 +183,7 @@ public static class InputActionReader
/// </summary> /// </summary>
public static void ResetToggledButton(string key, string actionName) public static void ResetToggledButton(string key, string actionName)
{ {
ToggledKeys.Remove(new InputReadKey(actionName, key)); RemoveKey(ToggledKeys, ref ToggledKeyCount, new InputReadKey(actionName, key));
} }
/// <summary> /// <summary>
@ -209,18 +191,16 @@ public static class InputActionReader
/// </summary> /// </summary>
public static void ResetToggledButton(string actionName) public static void ResetToggledButton(string actionName)
{ {
if (string.IsNullOrEmpty(actionName) || ToggledKeys.Count == 0) if (string.IsNullOrEmpty(actionName) || ToggledKeyCount == 0)
{ {
return; return;
} }
InputReadKey[] snapshot = new InputReadKey[ToggledKeys.Count]; for (int i = ToggledKeyCount - 1; i >= 0; i--)
ToggledKeys.CopyTo(snapshot);
for (int i = 0; i < snapshot.Length; i++)
{ {
if (string.Equals(snapshot[i].ActionName, actionName, StringComparison.Ordinal)) if (string.Equals(ToggledKeys[i].ActionName, actionName, StringComparison.Ordinal))
{ {
ToggledKeys.Remove(snapshot[i]); RemoveAt(ToggledKeys, ref ToggledKeyCount, i);
} }
} }
} }
@ -230,7 +210,8 @@ public static class InputActionReader
/// </summary> /// </summary>
public static void ResetToggledButtons() public static void ResetToggledButtons()
{ {
ToggledKeys.Clear(); Array.Clear(ToggledKeys, 0, ToggledKeyCount);
ToggledKeyCount = 0;
} }
/// <summary> /// <summary>
@ -238,8 +219,7 @@ public static class InputActionReader
/// </summary> /// </summary>
private static InputAction ResolveAction(string actionName) private static InputAction ResolveAction(string actionName)
{ {
return InputBindingManager.Action(actionName) return InputBindingManager.Action(actionName);
?? throw new InvalidOperationException($"[InputActionReader] Action '{actionName}' is not available.");
} }
/// <summary> /// <summary>
@ -249,9 +229,9 @@ public static class InputActionReader
private static bool TryReadValueOnceInternal<T>(InputReadKey readKey, string actionName, out T value) where T : struct private static bool TryReadValueOnceInternal<T>(InputReadKey readKey, string actionName, out T value) where T : struct
{ {
InputAction inputAction = ResolveAction(actionName); InputAction inputAction = ResolveAction(actionName);
if (inputAction.IsPressed()) if (inputAction != null && inputAction.IsPressed())
{ {
if (PressedKeys.Add(readKey)) if (AddKey(ref PressedKeys, ref PressedKeyCount, readKey))
{ {
value = inputAction.ReadValue<T>(); value = inputAction.ReadValue<T>();
return true; return true;
@ -259,7 +239,7 @@ public static class InputActionReader
} }
else else
{ {
PressedKeys.Remove(readKey); RemoveKey(PressedKeys, ref PressedKeyCount, readKey);
} }
value = default; value = default;
@ -274,10 +254,10 @@ public static class InputActionReader
{ {
if (ReadButton(actionName)) if (ReadButton(actionName))
{ {
return PressedKeys.Add(readKey); return AddKey(ref PressedKeys, ref PressedKeyCount, readKey);
} }
PressedKeys.Remove(readKey); RemoveKey(PressedKeys, ref PressedKeyCount, readKey);
return false; return false;
} }
@ -289,12 +269,64 @@ public static class InputActionReader
{ {
if (ReadButtonOnceInternal(readKey, actionName)) if (ReadButtonOnceInternal(readKey, actionName))
{ {
if (!ToggledKeys.Add(readKey)) if (!AddKey(ref ToggledKeys, ref ToggledKeyCount, readKey))
{ {
ToggledKeys.Remove(readKey); RemoveKey(ToggledKeys, ref ToggledKeyCount, readKey);
} }
} }
return ToggledKeys.Contains(readKey); return IndexOf(ToggledKeys, ToggledKeyCount, readKey) >= 0;
}
private static bool AddKey(ref InputReadKey[] keys, ref int count, InputReadKey key)
{
if (IndexOf(keys, count, key) >= 0)
{
return false;
}
if (count == keys.Length)
{
Array.Resize(ref keys, keys.Length << 1);
}
keys[count++] = key;
return true;
}
private static bool RemoveKey(InputReadKey[] keys, ref int count, InputReadKey key)
{
int index = IndexOf(keys, count, key);
if (index < 0)
{
return false;
}
RemoveAt(keys, ref count, index);
return true;
}
private static void RemoveAt(InputReadKey[] keys, ref int count, int index)
{
count--;
if (index < count)
{
keys[index] = keys[count];
}
keys[count] = default;
}
private static int IndexOf(InputReadKey[] keys, int count, InputReadKey key)
{
for (int i = 0; i < count; i++)
{
if (keys[i].Equals(key))
{
return i;
}
}
return -1;
} }
} }

View File

@ -23,6 +23,8 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
public bool debugMode = false; public bool debugMode = false;
private InputActionRebindingExtensions.RebindingOperation rebindOperation; private InputActionRebindingExtensions.RebindingOperation rebindOperation;
private InputAction rebindAction;
private int rebindBindingIndex = -1;
private bool isApplyPending = false; private bool isApplyPending = false;
private string defaultBindingsJson = string.Empty; private string defaultBindingsJson = string.Empty;
private string cachedSavePath; private string cachedSavePath;
@ -32,7 +34,7 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
private readonly Dictionary<Guid, (ActionMap map, ActionMap.Action action)> actionLookupById = new(); private readonly Dictionary<Guid, (ActionMap map, ActionMap.Action action)> actionLookupById = new();
private readonly HashSet<string> ambiguousActionNames = new(StringComparer.Ordinal); private readonly HashSet<string> ambiguousActionNames = new(StringComparer.Ordinal);
public event Action<bool, HashSet<RebindContext>> OnApply; public event Action<bool, RebindContext[]> OnApply;
public event Action<RebindContext> OnRebindPrepare; public event Action<RebindContext> OnRebindPrepare;
public event Action OnRebindStart; public event Action OnRebindStart;
public event Action<bool, RebindContext> OnRebindEnd; public event Action<bool, RebindContext> OnRebindEnd;
@ -78,36 +80,21 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
BuildActionMap(); BuildActionMap();
try defaultBindingsJson = actions.SaveBindingOverridesAsJson();
{
defaultBindingsJson = actions.SaveBindingOverridesAsJson();
}
catch (Exception ex)
{
Log.Warning($"[InputBindingManager] Failed to save default bindings: {ex.Message}");
defaultBindingsJson = string.Empty;
}
if (File.Exists(SavePath)) if (File.Exists(SavePath))
{ {
try var json = File.ReadAllText(SavePath);
if (!string.IsNullOrEmpty(json))
{ {
var json = File.ReadAllText(SavePath); actions.LoadBindingOverridesFromJson(json);
if (!string.IsNullOrEmpty(json)) RefreshBindingPathsFromActions();
BindingsChanged?.Invoke();
if (debugMode)
{ {
actions.LoadBindingOverridesFromJson(json); Log.Info($"Loaded overrides from {SavePath}");
RefreshBindingPathsFromActions();
BindingsChanged?.Invoke();
if (debugMode)
{
Log.Info($"Loaded overrides from {SavePath}");
}
} }
} }
catch (Exception ex)
{
Log.Error("Failed to load overrides: " + ex);
}
} }
actions.Enable(); actions.Enable();
@ -350,6 +337,8 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true) private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true)
{ {
rebindAction = action;
rebindBindingIndex = bindingIndex;
var op = action.PerformInteractiveRebinding(bindingIndex); var op = action.PerformInteractiveRebinding(bindingIndex);
if (!string.IsNullOrEmpty(deviceMatchPath)) if (!string.IsNullOrEmpty(deviceMatchPath))
@ -366,57 +355,75 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
} }
rebindOperation = op rebindOperation = op
.OnApplyBinding((o, path) => .OnApplyBinding(HandleApplyBinding)
{ .OnComplete(HandleRebindComplete)
RebindContext preparedContext = new RebindContext(action, bindingIndex, path); .OnCancel(HandleRebindCancel)
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) .WithCancelingThrough(KEYBOARD_ESCAPE)
.Start(); .Start();
} }
private void HandleApplyBinding(InputActionRebindingExtensions.RebindingOperation operation, string path)
{
RebindContext preparedContext = new RebindContext(rebindAction, rebindBindingIndex, path);
if (AnyPreparedRebind(path, rebindAction, rebindBindingIndex, out var existing))
{
PrepareRebind(preparedContext);
PrepareRebind(new RebindContext(existing.action, existing.bindingIndex, NULL_BINDING));
OnRebindConflict?.Invoke(preparedContext, existing);
}
else if (AnyBindingPath(path, rebindAction, rebindBindingIndex, out var duplicate))
{
RebindContext conflictingContext = new RebindContext(duplicate.action, duplicate.bindingIndex, duplicate.action.bindings[duplicate.bindingIndex].path);
PrepareRebind(preparedContext);
PrepareRebind(new RebindContext(duplicate.action, duplicate.bindingIndex, NULL_BINDING));
OnRebindConflict?.Invoke(preparedContext, conflictingContext);
}
else
{
PrepareRebind(preparedContext);
}
}
private void HandleRebindComplete(InputActionRebindingExtensions.RebindingOperation operation)
{
if (debugMode)
{
Log.Info("[InputBindingManager] Rebind completed");
}
actions.Enable();
OnRebindEnd?.Invoke(true, CreateCurrentRebindContext());
CleanRebindOperation();
}
private void HandleRebindCancel(InputActionRebindingExtensions.RebindingOperation operation)
{
if (debugMode)
{
Log.Info("[InputBindingManager] Rebind cancelled");
}
actions.Enable();
OnRebindEnd?.Invoke(false, CreateCurrentRebindContext());
CleanRebindOperation();
}
private RebindContext CreateCurrentRebindContext()
{
if (rebindAction == null || rebindBindingIndex < 0)
{
return null;
}
return new RebindContext(rebindAction, rebindBindingIndex, rebindAction.bindings[rebindBindingIndex].effectivePath);
}
private void CleanRebindOperation() private void CleanRebindOperation()
{ {
rebindOperation?.Dispose(); rebindOperation?.Dispose();
rebindOperation = null; rebindOperation = null;
rebindAction = null;
rebindBindingIndex = -1;
} }
private bool AnyPreparedRebind(string bindingPath, InputAction currentAction, int currentIndex, out RebindContext duplicate) private bool AnyPreparedRebind(string bindingPath, InputAction currentAction, int currentIndex, out RebindContext duplicate)
@ -444,7 +451,7 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
foreach (var bindingPair in actionPair.bindings) foreach (var bindingPair in actionPair.bindings)
{ {
// Skip if it's the same action and same binding index // 跳过当前正在重绑定的同一个 action/binding。
if (isSameAction && bindingPair.Key == currentIndex) if (isSameAction && bindingPair.Key == currentIndex)
continue; continue;
@ -463,7 +470,7 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
private void PrepareRebind(RebindContext context) private void PrepareRebind(RebindContext context)
{ {
// Remove any existing prepared state for the same action/binding pair. // 移除同一个 action/binding 已暂存的重绑定状态。
preparedRebinds.Remove(context); preparedRebinds.Remove(context);
BindingPath bindingPath = GetBindingPath(context.action, context.bindingIndex); BindingPath bindingPath = GetBindingPath(context.action, context.bindingIndex);
@ -488,20 +495,12 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
private async Task WriteOverridesToDiskAsync() private async Task WriteOverridesToDiskAsync()
{ {
try var json = actions.SaveBindingOverridesAsJson();
EnsureSaveDirectoryExists();
using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json);
if (debugMode)
{ {
var json = actions.SaveBindingOverridesAsJson(); Log.Info($"Overrides saved to {SavePath}");
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;
} }
} }
@ -522,14 +521,9 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
} }
#region Public API #region Public API
// 为键盘选择最佳绑定索引;如果 compositePartName != null 则查找部分
/// <summary> /// <summary>
/// 为键盘查找最佳的绑定索引 /// ?? Action ???????????
/// </summary> /// </summary>
/// <param name="action">输入操作</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <returns>绑定索引,未找到则返回 -1</returns>
public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null) public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null)
{ {
if (action == null) return -1; if (action == null) return -1;
@ -541,15 +535,11 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
for (int i = 0; i < action.bindings.Count; i++) for (int i = 0; i < action.bindings.Count; i++)
{ {
var b = action.bindings[i]; var b = action.bindings[i];
// 如果搜索特定的复合部分,跳过不匹配的绑定
if (searchingForCompositePart) if (searchingForCompositePart)
{ {
if (!b.isPartOfComposite) continue; if (!b.isPartOfComposite) continue;
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
} }
// 检查此绑定是否用于键盘
bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) || bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) ||
(!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)); (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase));
@ -567,12 +557,9 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart; return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart;
} }
/// <summary> /// <summary>
/// 根据操作名称获取输入操作 /// ?? ActionName ? MapName/ActionName ?? Action?
/// </summary> /// </summary>
/// <param name="actionName">操作名称</param>
/// <returns>输入操作,未找到则返回 null</returns>
public static InputAction Action(string actionName) public static InputAction Action(string actionName)
{ {
var instance= AppServices.Require<InputBindingManager>(); var instance= AppServices.Require<InputBindingManager>();
@ -608,18 +595,13 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
action = null; action = null;
return false; return false;
} }
/// <summary> /// <summary>
/// 开始重新绑定指定的输入操作 /// ??? Action ???????????
/// </summary> /// </summary>
/// <param name="actionName">操作名称</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
public void StartRebind(string actionName, string compositePartName = null) public void StartRebind(string actionName, string compositePartName = null)
{ {
var action = Action(actionName); var action = Action(actionName);
if (action == null) return; if (action == null) return;
// 自动决定 bindingIndex 和 deviceMatch
int bindingIndex = FindBestBindingIndexForKeyboard(action, compositePartName); int bindingIndex = FindBestBindingIndexForKeyboard(action, compositePartName);
if (bindingIndex < 0) if (bindingIndex < 0)
{ {
@ -635,81 +617,77 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
Log.Info("[InputBindingManager] Rebind started"); Log.Info("[InputBindingManager] Rebind started");
} }
} }
/// <summary> /// <summary>
/// 取消当前的重新绑定操作 /// ???????????????
/// </summary> /// </summary>
public void CancelRebind() => rebindOperation?.Cancel(); public void CancelRebind() => rebindOperation?.Cancel();
/// <summary> /// <summary>
/// 确认并应用准备好的重新绑定 /// ????????????????????
/// </summary> /// </summary>
/// <param name="clearConflicts">是否清除冲突</param>
/// <returns>是否成功应用</returns>
public async Task<bool> ConfirmApply(bool clearConflicts = true) public async Task<bool> ConfirmApply(bool clearConflicts = true)
{ {
if (!isApplyPending) return false; if (!isApplyPending) return false;
try RebindContext[] appliedContexts = OnApply != null ? BuildPreparedSnapshot() : null;
foreach (var ctx in preparedRebinds)
{ {
// 在清除之前创建准备好的重绑定的副本 if (ctx.overridePath == NULL_BINDING && !clearConflicts)
HashSet<RebindContext> appliedContexts = OnApply != null
? new HashSet<RebindContext>(preparedRebinds)
: null;
foreach (var ctx in preparedRebinds)
{ {
if (!string.IsNullOrEmpty(ctx.overridePath)) continue;
{ }
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 (!string.IsNullOrEmpty(ctx.overridePath))
if (bp != null) {
if (ctx.overridePath == NULL_BINDING)
{ {
bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath; ctx.action.RemoveBindingOverride(ctx.bindingIndex);
}
else
{
ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath);
} }
} }
preparedRebinds.Clear(); var bp = GetBindingPath(ctx.action, ctx.bindingIndex);
await WriteOverridesToDiskAsync(); if (bp != null)
BindingsChanged?.Invoke();
OnApply?.Invoke(true, appliedContexts);
isApplyPending = false;
if (debugMode)
{ {
Log.Info("[InputBindingManager] Apply confirmed and saved."); bp.EffectivePath = ctx.action.bindings[ctx.bindingIndex].effectivePath;
} }
}
return true; preparedRebinds.Clear();
} await WriteOverridesToDiskAsync();
catch (Exception ex) BindingsChanged?.Invoke();
OnApply?.Invoke(true, appliedContexts);
isApplyPending = false;
if (debugMode)
{ {
Log.Error("[InputBindingManager] Failed to apply binds: " + ex); Log.Info("[InputBindingManager] Apply confirmed and saved.");
OnApply?.Invoke(false, null);
return false;
} }
return true;
}
private RebindContext[] BuildPreparedSnapshot()
{
if (preparedRebinds.Count == 0)
{
return Array.Empty<RebindContext>();
}
RebindContext[] snapshot = new RebindContext[preparedRebinds.Count];
preparedRebinds.CopyTo(snapshot);
return snapshot;
} }
/// <summary> /// <summary>
/// 丢弃准备好的重新绑定 /// ???????????????
/// </summary> /// </summary>
public void DiscardPrepared() public void DiscardPrepared()
{ {
if (!isApplyPending) return; if (!isApplyPending) return;
// 在清除之前创建准备好的重绑定的副本(用于事件通知) RebindContext[] discardedContexts = OnApply != null ? BuildPreparedSnapshot() : null;
HashSet<RebindContext> discardedContexts = OnApply != null
? new HashSet<RebindContext>(preparedRebinds)
: null;
preparedRebinds.Clear(); preparedRebinds.Clear();
isApplyPending = false; isApplyPending = false;
OnApply?.Invoke(false, discardedContexts); OnApply?.Invoke(false, discardedContexts);
@ -718,52 +696,40 @@ public sealed class InputBindingManager : MonoServiceBehaviour<AppScope>
Log.Info("[InputBindingManager] Prepared rebinds discarded."); Log.Info("[InputBindingManager] Prepared rebinds discarded.");
} }
} }
/// <summary> /// <summary>
/// 重置所有绑定到默认值 /// ?????????????????????
/// </summary> /// </summary>
public async Task ResetToDefaultAsync() public async Task ResetToDefaultAsync()
{ {
try if (!string.IsNullOrEmpty(defaultBindingsJson))
{ {
if (!string.IsNullOrEmpty(defaultBindingsJson)) actions.LoadBindingOverridesFromJson(defaultBindingsJson);
}
else
{
foreach (var map in actionMap.Values)
{ {
actions.LoadBindingOverridesFromJson(defaultBindingsJson); foreach (var a in map.actions.Values)
}
else
{
foreach (var map in actionMap.Values)
{ {
foreach (var a in map.actions.Values) for (int b = 0; b < a.action.bindings.Count; b++)
{ {
for (int b = 0; b < a.action.bindings.Count; b++) a.action.RemoveBindingOverride(b);
{
a.action.RemoveBindingOverride(b);
}
} }
} }
} }
RefreshBindingPathsFromActions();
await WriteOverridesToDiskAsync();
BindingsChanged?.Invoke();
if (debugMode)
{
Log.Info("Reset to default and saved.");
}
} }
catch (Exception ex)
RefreshBindingPathsFromActions();
await WriteOverridesToDiskAsync();
BindingsChanged?.Invoke();
if (debugMode)
{ {
Log.Error("Failed to reset defaults: " + ex); Log.Info("Reset to default and saved.");
} }
} }
/// <summary> /// <summary>
/// 获取指定操作的绑定路径 /// ???? Action ?????????????????
/// </summary> /// </summary>
/// <param name="actionName">操作名称</param>
/// <param name="bindingIndex">绑定索引</param>
/// <returns>绑定路径,未找到则返回 null</returns>
public BindingPath GetBindingPath(string actionName, int bindingIndex = 0) public BindingPath GetBindingPath(string actionName, int bindingIndex = 0)
{ {
if (TryGetActionRecord(actionName, out var result) if (TryGetActionRecord(actionName, out var result)

View File

@ -91,7 +91,9 @@ public static class InputDeviceWatcher
private static InputAction _anyInputAction; private static InputAction _anyInputAction;
private static float _lastSwitchTime = -Mathf.Infinity; private static float _lastSwitchTime = -Mathf.Infinity;
private static DeviceContext _lastEmittedContext = CreateDefaultContext(); private static DeviceContext _lastEmittedContext = CreateDefaultContext();
private static readonly Dictionary<int, DeviceContext> DeviceContextCache = new(); private const int InitialDeviceCacheCapacity = 16;
private static DeviceContext[] DeviceContextCache = new DeviceContext[InitialDeviceCacheCapacity];
private static int DeviceContextCacheCount;
private static bool _initialized; private static bool _initialized;
public static event Action<InputDeviceCategory> OnDeviceChanged; public static event Action<InputDeviceCategory> OnDeviceChanged;
@ -112,13 +114,22 @@ public static class InputDeviceWatcher
_anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough); _anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough);
_anyInputAction.AddBinding("<Keyboard>/anyKey"); _anyInputAction.AddBinding("<Keyboard>/anyKey");
//为防止误触 暂时屏蔽鼠标检测 //为防止误触 暂时屏蔽鼠标检测
_anyInputAction.AddBinding("<Mouse>/delta");
_anyInputAction.AddBinding("<Mouse>/leftButton"); _anyInputAction.AddBinding("<Mouse>/leftButton");
_anyInputAction.AddBinding("<Mouse>/rightButton"); _anyInputAction.AddBinding("<Mouse>/rightButton");
_anyInputAction.AddBinding("<Mouse>/middleButton"); _anyInputAction.AddBinding("<Mouse>/middleButton");
// _anyInputAction.AddBinding("<Mouse>/scroll"); _anyInputAction.AddBinding("<Gamepad>/buttonSouth");
_anyInputAction.AddBinding("<Gamepad>/*"); _anyInputAction.AddBinding("<Gamepad>/buttonNorth");
_anyInputAction.AddBinding("<Joystick>/*"); _anyInputAction.AddBinding("<Gamepad>/buttonEast");
_anyInputAction.AddBinding("<Gamepad>/buttonWest");
_anyInputAction.AddBinding("<Gamepad>/start");
_anyInputAction.AddBinding("<Gamepad>/select");
_anyInputAction.AddBinding("<Gamepad>/leftStick");
_anyInputAction.AddBinding("<Gamepad>/rightStick");
_anyInputAction.AddBinding("<Gamepad>/dpad");
_anyInputAction.AddBinding("<Gamepad>/leftTrigger");
_anyInputAction.AddBinding("<Gamepad>/rightTrigger");
_anyInputAction.AddBinding("<Joystick>/trigger");
_anyInputAction.AddBinding("<Joystick>/stick");
_anyInputAction.performed += OnAnyInputPerformed; _anyInputAction.performed += OnAnyInputPerformed;
_anyInputAction.Enable(); _anyInputAction.Enable();
@ -155,7 +166,8 @@ public static class InputDeviceWatcher
} }
InputSystem.onDeviceChange -= OnDeviceChange; InputSystem.onDeviceChange -= OnDeviceChange;
DeviceContextCache.Clear(); Array.Clear(DeviceContextCache, 0, DeviceContextCacheCount);
DeviceContextCacheCount = 0;
ApplyContext(CreateDefaultContext(), false); ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext; _lastEmittedContext = CurrentContext;
@ -206,7 +218,7 @@ public static class InputDeviceWatcher
{ {
case InputDeviceChange.Removed: case InputDeviceChange.Removed:
case InputDeviceChange.Disconnected: case InputDeviceChange.Disconnected:
DeviceContextCache.Remove(device.deviceId); RemoveCachedContext(device.deviceId);
if (device.deviceId == CurrentDeviceId) if (device.deviceId == CurrentDeviceId)
{ {
PromoteFallbackDevice(device.deviceId); PromoteFallbackDevice(device.deviceId);
@ -215,7 +227,7 @@ public static class InputDeviceWatcher
break; break;
case InputDeviceChange.Reconnected: case InputDeviceChange.Reconnected:
case InputDeviceChange.Added: case InputDeviceChange.Added:
DeviceContextCache.Remove(device.deviceId); RemoveCachedContext(device.deviceId);
if (CurrentDeviceId < 0 && IsRelevantDevice(device)) if (CurrentDeviceId < 0 && IsRelevantDevice(device))
{ {
SetCurrentContext(BuildContext(device)); SetCurrentContext(BuildContext(device));
@ -283,7 +295,7 @@ public static class InputDeviceWatcher
return CreateDefaultContext(); return CreateDefaultContext();
} }
if (DeviceContextCache.TryGetValue(device.deviceId, out DeviceContext cachedContext)) if (TryGetCachedContext(device.deviceId, out DeviceContext cachedContext))
{ {
return cachedContext; return cachedContext;
} }
@ -297,10 +309,55 @@ public static class InputDeviceWatcher
productId, productId,
deviceName, deviceName,
device.layout); device.layout);
DeviceContextCache[device.deviceId] = context; AddCachedContext(context);
return context; return context;
} }
private static bool TryGetCachedContext(int deviceId, out DeviceContext context)
{
for (int i = 0; i < DeviceContextCacheCount; i++)
{
if (DeviceContextCache[i].DeviceId == deviceId)
{
context = DeviceContextCache[i];
return true;
}
}
context = default;
return false;
}
private static void AddCachedContext(DeviceContext context)
{
if (DeviceContextCacheCount == DeviceContextCache.Length)
{
Array.Resize(ref DeviceContextCache, DeviceContextCache.Length << 1);
}
DeviceContextCache[DeviceContextCacheCount++] = context;
}
private static void RemoveCachedContext(int deviceId)
{
for (int i = 0; i < DeviceContextCacheCount; i++)
{
if (DeviceContextCache[i].DeviceId != deviceId)
{
continue;
}
DeviceContextCacheCount--;
if (i < DeviceContextCacheCount)
{
DeviceContextCache[i] = DeviceContextCache[DeviceContextCacheCount];
}
DeviceContextCache[DeviceContextCacheCount] = default;
return;
}
}
private static DeviceContext CreateDefaultContext() private static DeviceContext CreateDefaultContext()
{ {
return new DeviceContext(InputDeviceCategory.Keyboard, -1, 0, 0, DefaultKeyboardDeviceName, Keyboard.current != null ? Keyboard.current.layout : string.Empty); return new DeviceContext(InputDeviceCategory.Keyboard, -1, 0, 0, DefaultKeyboardDeviceName, Keyboard.current != null ? Keyboard.current.layout : string.Empty);
@ -355,9 +412,9 @@ public static class InputDeviceWatcher
case ButtonControl button: case ButtonControl button:
return button.IsPressed(); return button.IsPressed();
case StickControl stick: case StickControl stick:
return stick.ReadValue().sqrMagnitude >= StickActivationThreshold; return stick.ReadValue().sqrMagnitude >= StickActivationThreshold * StickActivationThreshold;
case Vector2Control vector2: case Vector2Control vector2:
return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold; return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold * StickActivationThreshold;
case AxisControl axis: case AxisControl axis:
return Mathf.Abs(axis.ReadValue()) >= AxisActivationThreshold; return Mathf.Abs(axis.ReadValue()) >= AxisActivationThreshold;
default: default:

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
@ -26,6 +26,8 @@ public sealed class InputGlyphDatabase : ScriptableObject
private const string DeviceXbox = "Xbox"; private const string DeviceXbox = "Xbox";
private const string DevicePlayStation = "PlayStation"; private const string DevicePlayStation = "PlayStation";
private const string DeviceOther = "Other"; private const string DeviceOther = "Other";
private const int CategoryCount = 4;
private const int InitialPathCapacity = 128;
private static readonly InputDeviceWatcher.InputDeviceCategory[] KeyboardLookupOrder = { InputDeviceWatcher.InputDeviceCategory.Keyboard }; private static readonly InputDeviceWatcher.InputDeviceCategory[] KeyboardLookupOrder = { InputDeviceWatcher.InputDeviceCategory.Keyboard };
private static readonly InputDeviceWatcher.InputDeviceCategory[] XboxLookupOrder = private static readonly InputDeviceWatcher.InputDeviceCategory[] XboxLookupOrder =
{ {
@ -45,13 +47,20 @@ public sealed class InputGlyphDatabase : ScriptableObject
InputDeviceWatcher.InputDeviceCategory.Xbox, InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Keyboard, InputDeviceWatcher.InputDeviceCategory.Keyboard,
}; };
private static readonly Dictionary<string, string> NormalizedPathCache = new(StringComparer.Ordinal);
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>(); public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>();
public Sprite placeholderSprite; public Sprite placeholderSprite;
private Dictionary<string, DeviceGlyphTable> _tableCache; private DeviceGlyphTable[] _tableByCategory = new DeviceGlyphTable[CategoryCount];
private Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>> _pathLookup; private PathLookup[] _pathLookupByCategory = new PathLookup[CategoryCount];
private bool _cacheBuilt;
private struct PathLookup
{
public string[] Keys;
public Sprite[] Sprites;
public int Count;
}
private void OnEnable() private void OnEnable()
{ {
@ -73,23 +82,20 @@ public sealed class InputGlyphDatabase : ScriptableObject
} }
EnsureCache(); EnsureCache();
_tableCache.TryGetValue(deviceName, out DeviceGlyphTable table); return GetTable(ParseCategory(deviceName));
return table;
} }
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
{ {
switch (device) EnsureCache();
int index = CategoryIndex(device);
DeviceGlyphTable table = _tableByCategory[index];
if (table != null)
{ {
case InputDeviceWatcher.InputDeviceCategory.Keyboard: return table;
return GetTable(DeviceKeyboard);
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return GetTable(DeviceXbox);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return GetTable(DevicePlayStation);
default:
return GetTable(DeviceOther) ?? GetTable(DeviceXbox);
} }
return device == InputDeviceWatcher.InputDeviceCategory.Other ? _tableByCategory[CategoryIndex(InputDeviceWatcher.InputDeviceCategory.Xbox)] : null;
} }
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device) public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
@ -111,8 +117,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device); InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
for (int i = 0; i < lookupOrder.Length; i++) for (int i = 0; i < lookupOrder.Length; i++)
{ {
InputDeviceWatcher.InputDeviceCategory category = lookupOrder[i]; if (TryGetSpriteInCategory(lookupOrder[i], key, out sprite))
if (_pathLookup.TryGetValue(category, out Dictionary<string, Sprite> map) && map.TryGetValue(key, out sprite) && sprite != null)
{ {
return true; return true;
} }
@ -122,7 +127,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
return sprite != null; return sprite != null;
} }
public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) public Sprite GetSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{ {
return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite; return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite;
} }
@ -158,7 +163,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
private void EnsureCache() private void EnsureCache()
{ {
if (_tableCache == null || _pathLookup == null) if (!_cacheBuilt)
{ {
BuildCache(); BuildCache();
} }
@ -166,16 +171,26 @@ public sealed class InputGlyphDatabase : ScriptableObject
private void BuildCache() private void BuildCache()
{ {
_tableCache ??= new Dictionary<string, DeviceGlyphTable>(StringComparer.OrdinalIgnoreCase); if (_tableByCategory == null || _tableByCategory.Length != CategoryCount)
_tableCache.Clear(); {
_tableByCategory = new DeviceGlyphTable[CategoryCount];
}
else
{
Array.Clear(_tableByCategory, 0, _tableByCategory.Length);
}
_pathLookup ??= new Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>>(); if (_pathLookupByCategory == null || _pathLookupByCategory.Length != CategoryCount)
_pathLookup.Clear(); {
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Keyboard); _pathLookupByCategory = new PathLookup[CategoryCount];
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Xbox); }
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.PlayStation);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Other);
for (int i = 0; i < CategoryCount; i++)
{
ResetLookup(ref _pathLookupByCategory[i]);
}
_cacheBuilt = true;
if (tables == null) if (tables == null)
{ {
return; return;
@ -189,10 +204,10 @@ public sealed class InputGlyphDatabase : ScriptableObject
continue; continue;
} }
_tableCache[table.deviceName] = table;
InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName); InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName);
Dictionary<string, Sprite> map = _pathLookup[category]; int categoryIndex = CategoryIndex(category);
RegisterEntries(table, map); _tableByCategory[categoryIndex] = table;
RegisterEntries(table, ref _pathLookupByCategory[categoryIndex]);
} }
} }
@ -208,12 +223,23 @@ public sealed class InputGlyphDatabase : ScriptableObject
} }
#endif #endif
private void InitializeLookup(InputDeviceWatcher.InputDeviceCategory category) private static void ResetLookup(ref PathLookup lookup)
{ {
_pathLookup[category] = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase); if (lookup.Keys == null || lookup.Keys.Length == 0)
{
lookup.Keys = new string[InitialPathCapacity];
lookup.Sprites = new Sprite[InitialPathCapacity];
}
else
{
Array.Clear(lookup.Keys, 0, lookup.Count);
Array.Clear(lookup.Sprites, 0, lookup.Count);
}
lookup.Count = 0;
} }
private void RegisterEntries(DeviceGlyphTable table, Dictionary<string, Sprite> map) private void RegisterEntries(DeviceGlyphTable table, ref PathLookup lookup)
{ {
if (table.entries == null) if (table.entries == null)
{ {
@ -230,21 +256,56 @@ public sealed class InputGlyphDatabase : ScriptableObject
for (int j = 0; j < entry.action.bindings.Count; j++) for (int j = 0; j < entry.action.bindings.Count; j++)
{ {
RegisterBinding(map, entry.action.bindings[j].path, entry.Sprite); RegisterBinding(ref lookup, entry.action.bindings[j].path, entry.Sprite);
RegisterBinding(map, entry.action.bindings[j].effectivePath, entry.Sprite); RegisterBinding(ref lookup, entry.action.bindings[j].effectivePath, entry.Sprite);
} }
} }
} }
private void RegisterBinding(Dictionary<string, Sprite> map, string controlPath, Sprite sprite) private static void RegisterBinding(ref PathLookup lookup, string controlPath, Sprite sprite)
{ {
string key = NormalizeControlPath(controlPath); string key = NormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(key) || map.ContainsKey(key)) if (string.IsNullOrEmpty(key) || IndexOf(lookup.Keys, lookup.Count, key) >= 0)
{ {
return; return;
} }
map[key] = sprite; if (lookup.Count == lookup.Keys.Length)
{
Array.Resize(ref lookup.Keys, lookup.Keys.Length << 1);
Array.Resize(ref lookup.Sprites, lookup.Sprites.Length << 1);
}
lookup.Keys[lookup.Count] = key;
lookup.Sprites[lookup.Count] = sprite;
lookup.Count++;
}
private bool TryGetSpriteInCategory(InputDeviceWatcher.InputDeviceCategory category, string key, out Sprite sprite)
{
PathLookup lookup = _pathLookupByCategory[CategoryIndex(category)];
int index = IndexOf(lookup.Keys, lookup.Count, key);
if (index >= 0)
{
sprite = lookup.Sprites[index];
return sprite != null;
}
sprite = null;
return false;
}
private static int IndexOf(string[] keys, int count, string key)
{
for (int i = 0; i < count; i++)
{
if (string.Equals(keys[i], key, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return -1;
} }
private static string NormalizeControlPath(string controlPath) private static string NormalizeControlPath(string controlPath)
@ -254,14 +315,7 @@ public sealed class InputGlyphDatabase : ScriptableObject
return string.Empty; return string.Empty;
} }
if (NormalizedPathCache.TryGetValue(controlPath, out string normalizedPath)) return CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant());
{
return normalizedPath;
}
normalizedPath = CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant());
NormalizedPathCache[controlPath] = normalizedPath;
return normalizedPath;
} }
private static string CanonicalizeDeviceLayout(string controlPath) private static string CanonicalizeDeviceLayout(string controlPath)
@ -342,6 +396,21 @@ public sealed class InputGlyphDatabase : ScriptableObject
return InputDeviceWatcher.InputDeviceCategory.Other; return InputDeviceWatcher.InputDeviceCategory.Other;
} }
private static int CategoryIndex(InputDeviceWatcher.InputDeviceCategory category)
{
switch (category)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return 0;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return 1;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return 2;
default:
return 3;
}
}
private static InputDeviceWatcher.InputDeviceCategory[] GetLookupOrder(InputDeviceWatcher.InputDeviceCategory device) private static InputDeviceWatcher.InputDeviceCategory[] GetLookupOrder(InputDeviceWatcher.InputDeviceCategory device)
{ {
switch (device) switch (device)

File diff suppressed because it is too large Load Diff

View File

@ -150,14 +150,20 @@ public sealed class InputGlyph : InputGlyphBehaviourBase
return; return;
} }
string formattedText = Utility.Text.Format(_templateText, replacementToken);
if (_cachedReplacementToken == replacementToken if (_cachedReplacementToken == replacementToken
&& _cachedFormattedText == formattedText && !string.IsNullOrEmpty(_cachedFormattedText)
&& targetText.text == formattedText) && targetText.text == _cachedFormattedText)
{ {
return; return;
} }
string formattedText = Utility.Text.Format(_templateText, replacementToken);
if (_cachedFormattedText == formattedText && targetText.text == formattedText)
{
_cachedReplacementToken = replacementToken;
return;
}
_cachedReplacementToken = replacementToken; _cachedReplacementToken = replacementToken;
if (_cachedFormattedText != formattedText || targetText.text != formattedText) if (_cachedFormattedText != formattedText || targetText.text != formattedText)
{ {