[Opt]优化输入系统
This commit is contained in:
parent
526341579a
commit
7d5ae32361
@ -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"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 241d5382b2b4f274596c73f14a40cb8d
|
guid: 43899fd2849604a49acbdbd7b3450fb6
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
476
Editor/InputGlyph/InputGlyphDatabaseEditor.cs
Normal file
476
Editor/InputGlyph/InputGlyphDatabaseEditor.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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": [],
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user