2026-04-29 11:04:57 +08:00
|
|
|
using System;
|
2026-03-20 16:50:30 +08:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.InputSystem;
|
|
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
public sealed class GlyphEntry
|
|
|
|
|
{
|
|
|
|
|
public Sprite Sprite;
|
|
|
|
|
public InputAction action;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
public sealed class DeviceGlyphTable
|
|
|
|
|
{
|
|
|
|
|
public string deviceName;
|
|
|
|
|
public Texture2D spriteSheetTexture;
|
|
|
|
|
public Sprite platformIcons;
|
|
|
|
|
public List<GlyphEntry> entries = new List<GlyphEntry>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "GameplaySystem/Input/InputGlyphDatabase", order = 400)]
|
|
|
|
|
public sealed class InputGlyphDatabase : ScriptableObject
|
|
|
|
|
{
|
|
|
|
|
private const string DeviceKeyboard = "Keyboard";
|
|
|
|
|
private const string DeviceXbox = "Xbox";
|
|
|
|
|
private const string DevicePlayStation = "PlayStation";
|
|
|
|
|
private const string DeviceOther = "Other";
|
2026-04-29 11:04:57 +08:00
|
|
|
private const int CategoryCount = 4;
|
|
|
|
|
private const int InitialPathCapacity = 128;
|
2026-03-20 16:50:30 +08:00
|
|
|
private static readonly InputDeviceWatcher.InputDeviceCategory[] KeyboardLookupOrder = { InputDeviceWatcher.InputDeviceCategory.Keyboard };
|
|
|
|
|
private static readonly InputDeviceWatcher.InputDeviceCategory[] XboxLookupOrder =
|
|
|
|
|
{
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Xbox,
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Other,
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Keyboard,
|
|
|
|
|
};
|
|
|
|
|
private static readonly InputDeviceWatcher.InputDeviceCategory[] PlayStationLookupOrder =
|
|
|
|
|
{
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.PlayStation,
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Other,
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Keyboard,
|
|
|
|
|
};
|
|
|
|
|
private static readonly InputDeviceWatcher.InputDeviceCategory[] OtherLookupOrder =
|
|
|
|
|
{
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Other,
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Xbox,
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory.Keyboard,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>();
|
|
|
|
|
public Sprite placeholderSprite;
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
private DeviceGlyphTable[] _tableByCategory = new DeviceGlyphTable[CategoryCount];
|
|
|
|
|
private PathLookup[] _pathLookupByCategory = new PathLookup[CategoryCount];
|
|
|
|
|
private bool _cacheBuilt;
|
|
|
|
|
|
|
|
|
|
private struct PathLookup
|
|
|
|
|
{
|
|
|
|
|
public string[] Keys;
|
|
|
|
|
public Sprite[] Sprites;
|
|
|
|
|
public int Count;
|
|
|
|
|
}
|
2026-03-20 16:50:30 +08:00
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
{
|
|
|
|
|
BuildCache();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
private void OnValidate()
|
|
|
|
|
{
|
|
|
|
|
BuildCache();
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
public DeviceGlyphTable GetTable(string deviceName)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(deviceName) || tables == null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EnsureCache();
|
2026-04-29 11:04:57 +08:00
|
|
|
return GetTable(ParseCategory(deviceName));
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
|
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
EnsureCache();
|
|
|
|
|
int index = CategoryIndex(device);
|
|
|
|
|
DeviceGlyphTable table = _tableByCategory[index];
|
|
|
|
|
if (table != null)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
return table;
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
2026-04-29 11:04:57 +08:00
|
|
|
|
|
|
|
|
return device == InputDeviceWatcher.InputDeviceCategory.Other ? _tableByCategory[CategoryIndex(InputDeviceWatcher.InputDeviceCategory.Xbox)] : null;
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
|
|
|
|
|
{
|
|
|
|
|
DeviceGlyphTable table = GetTable(device);
|
|
|
|
|
return table != null ? table.platformIcons : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool TryGetSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite)
|
|
|
|
|
{
|
|
|
|
|
EnsureCache();
|
|
|
|
|
string key = NormalizeControlPath(controlPath);
|
|
|
|
|
if (string.IsNullOrEmpty(key))
|
|
|
|
|
{
|
|
|
|
|
sprite = placeholderSprite;
|
|
|
|
|
return sprite != null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
|
|
|
|
|
for (int i = 0; i < lookupOrder.Length; i++)
|
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
if (TryGetSpriteInCategory(lookupOrder[i], key, out sprite))
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sprite = placeholderSprite;
|
|
|
|
|
return sprite != null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
public Sprite GetSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
|
|
|
|
return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
|
|
|
|
|
{
|
|
|
|
|
if (!TryGetSprite(controlPath, device, out Sprite sprite) || sprite == null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
|
|
|
|
|
for (int i = 0; i < lookupOrder.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
DeviceGlyphTable table = GetTable(lookupOrder[i]);
|
|
|
|
|
if (table == null || table.entries == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int j = 0; j < table.entries.Count; j++)
|
|
|
|
|
{
|
|
|
|
|
GlyphEntry entry = table.entries[j];
|
|
|
|
|
if (entry != null && entry.Sprite == sprite)
|
|
|
|
|
{
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureCache()
|
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
if (!_cacheBuilt)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
|
|
|
|
BuildCache();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BuildCache()
|
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
if (_tableByCategory == null || _tableByCategory.Length != CategoryCount)
|
|
|
|
|
{
|
|
|
|
|
_tableByCategory = new DeviceGlyphTable[CategoryCount];
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Array.Clear(_tableByCategory, 0, _tableByCategory.Length);
|
|
|
|
|
}
|
2026-03-20 16:50:30 +08:00
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
if (_pathLookupByCategory == null || _pathLookupByCategory.Length != CategoryCount)
|
|
|
|
|
{
|
|
|
|
|
_pathLookupByCategory = new PathLookup[CategoryCount];
|
|
|
|
|
}
|
2026-03-20 16:50:30 +08:00
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
for (int i = 0; i < CategoryCount; i++)
|
|
|
|
|
{
|
|
|
|
|
ResetLookup(ref _pathLookupByCategory[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_cacheBuilt = true;
|
2026-03-20 16:50:30 +08:00
|
|
|
if (tables == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < tables.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
DeviceGlyphTable table = tables[i];
|
|
|
|
|
if (table == null || string.IsNullOrWhiteSpace(table.deviceName))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName);
|
2026-04-29 11:04:57 +08:00
|
|
|
int categoryIndex = CategoryIndex(category);
|
|
|
|
|
_tableByCategory[categoryIndex] = table;
|
|
|
|
|
RegisterEntries(table, ref _pathLookupByCategory[categoryIndex]);
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
public void EditorRefreshCache()
|
|
|
|
|
{
|
|
|
|
|
BuildCache();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string EditorNormalizeControlPath(string controlPath)
|
|
|
|
|
{
|
|
|
|
|
return NormalizeControlPath(controlPath);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
private static void ResetLookup(ref PathLookup lookup)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
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;
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
private void RegisterEntries(DeviceGlyphTable table, ref PathLookup lookup)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
|
|
|
|
if (table.entries == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < table.entries.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
GlyphEntry entry = table.entries[i];
|
|
|
|
|
if (entry == null || entry.Sprite == null || entry.action == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int j = 0; j < entry.action.bindings.Count; j++)
|
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
RegisterBinding(ref lookup, entry.action.bindings[j].path, entry.Sprite);
|
|
|
|
|
RegisterBinding(ref lookup, entry.action.bindings[j].effectivePath, entry.Sprite);
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
private static void RegisterBinding(ref PathLookup lookup, string controlPath, Sprite sprite)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
|
|
|
|
string key = NormalizeControlPath(controlPath);
|
2026-04-29 11:04:57 +08:00
|
|
|
if (string.IsNullOrEmpty(key) || IndexOf(lookup.Keys, lookup.Count, key) >= 0)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
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++;
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
private bool TryGetSpriteInCategory(InputDeviceWatcher.InputDeviceCategory category, string key, out Sprite sprite)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
PathLookup lookup = _pathLookupByCategory[CategoryIndex(category)];
|
|
|
|
|
int index = IndexOf(lookup.Keys, lookup.Count, key);
|
|
|
|
|
if (index >= 0)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
sprite = lookup.Sprites[index];
|
|
|
|
|
return sprite != null;
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
sprite = null;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int IndexOf(string[] keys, int count, string key)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < count; i++)
|
2026-03-20 16:50:30 +08:00
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
if (string.Equals(keys[i], key, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return i;
|
|
|
|
|
}
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string NormalizeControlPath(string controlPath)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(controlPath))
|
|
|
|
|
{
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant());
|
2026-03-20 16:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string CanonicalizeDeviceLayout(string controlPath)
|
|
|
|
|
{
|
|
|
|
|
int start = controlPath.IndexOf('<');
|
|
|
|
|
int end = controlPath.IndexOf('>');
|
|
|
|
|
if (start < 0 || end <= start + 1)
|
|
|
|
|
{
|
|
|
|
|
return controlPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string layout = controlPath.Substring(start + 1, end - start - 1);
|
|
|
|
|
string canonicalLayout = GetCanonicalLayout(layout);
|
|
|
|
|
if (string.Equals(layout, canonicalLayout, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
return controlPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return controlPath.Substring(0, start + 1) + canonicalLayout + controlPath.Substring(end);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetCanonicalLayout(string layout)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(layout))
|
|
|
|
|
{
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layout.IndexOf("keyboard", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
|
|
|
{
|
|
|
|
|
return "keyboard";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layout.IndexOf("mouse", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
|
|
|
{
|
|
|
|
|
return "mouse";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layout.IndexOf("joystick", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
|
|
|
{
|
|
|
|
|
return "joystick";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layout.IndexOf("gamepad", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|
|
|
|| layout.IndexOf("controller", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|
|
|
|| layout.IndexOf("xinput", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|
|
|
|| layout.IndexOf("dualshock", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|
|
|
|| layout.IndexOf("dualsense", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
|
|
|
{
|
|
|
|
|
return "gamepad";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return layout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static InputDeviceWatcher.InputDeviceCategory ParseCategory(string deviceName)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(deviceName))
|
|
|
|
|
{
|
|
|
|
|
return InputDeviceWatcher.InputDeviceCategory.Other;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (deviceName.Equals(DeviceKeyboard, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return InputDeviceWatcher.InputDeviceCategory.Keyboard;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (deviceName.Equals(DeviceXbox, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return InputDeviceWatcher.InputDeviceCategory.Xbox;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (deviceName.Equals(DevicePlayStation, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return InputDeviceWatcher.InputDeviceCategory.PlayStation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return InputDeviceWatcher.InputDeviceCategory.Other;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 16:50:30 +08:00
|
|
|
private static InputDeviceWatcher.InputDeviceCategory[] GetLookupOrder(InputDeviceWatcher.InputDeviceCategory device)
|
|
|
|
|
{
|
|
|
|
|
switch (device)
|
|
|
|
|
{
|
|
|
|
|
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
|
|
|
|
|
return KeyboardLookupOrder;
|
|
|
|
|
case InputDeviceWatcher.InputDeviceCategory.Xbox:
|
|
|
|
|
return XboxLookupOrder;
|
|
|
|
|
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
|
|
|
|
|
return PlayStationLookupOrder;
|
|
|
|
|
default:
|
|
|
|
|
return OtherLookupOrder;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-29 11:04:57 +08:00
|
|
|
}
|