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