#if INPUTSYSTEM_SUPPORT using System; using System.Globalization; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using UnityEngine.InputSystem; public static class InputDeviceWatcher { public enum InputDeviceCategory { Keyboard, Xbox, PlayStation, Other } static readonly float DebounceWindow = 1f; public static InputDeviceCategory CurrentCategory = InputDeviceCategory.Keyboard; public static string CurrentDeviceName = ""; private static InputAction _anyInputAction; private static int _lastDeviceId = -1; private static float _lastInputTime = -Mathf.Infinity; private static InputDeviceCategory _lastEmittedCategory = InputDeviceCategory.Keyboard; public static event Action OnDeviceChanged; private static bool initialized = false; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] public static void Initialize() { if (initialized) return; initialized = true; CurrentCategory = InputDeviceCategory.Keyboard; CurrentDeviceName = ""; _lastEmittedCategory = CurrentCategory; // 初始化同步 _anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough); _anyInputAction.AddBinding("/anyKey"); _anyInputAction.AddBinding("/*"); _anyInputAction.AddBinding("/*"); _anyInputAction.performed += OnAnyInputPerformed; _anyInputAction.Enable(); InputSystem.onDeviceChange += OnDeviceChange; #if UNITY_EDITOR EditorApplication.playModeStateChanged += OnPlayModeStateChanged; #endif } #if UNITY_EDITOR static void OnPlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.ExitingPlayMode) { Dispose(); } EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } #endif public static void Dispose() { if (!initialized) return; CurrentCategory = InputDeviceCategory.Keyboard; _anyInputAction.performed -= OnAnyInputPerformed; _anyInputAction.Disable(); _anyInputAction.Dispose(); InputSystem.onDeviceChange -= OnDeviceChange; OnDeviceChanged = null; initialized = false; _lastEmittedCategory = InputDeviceCategory.Keyboard; } private static void OnAnyInputPerformed(InputAction.CallbackContext ctx) { if (ctx.control == null || ctx.control.device == null) return; var device = ctx.control.device; if (!IsRelevantDevice(device)) return; int curId = device.deviceId; float now = Time.realtimeSinceStartup; if (curId == _lastDeviceId) return; if (DebounceWindow > 0f && (now - _lastInputTime) < DebounceWindow) return; _lastInputTime = now; _lastDeviceId = curId; CurrentCategory = DetermineCategoryFromDevice(device); CurrentDeviceName = device.displayName ?? $"Device_{curId}"; EmitChange(); } // ------------------ 监听设备变更 -------------------- private static void OnDeviceChange(InputDevice device, InputDeviceChange change) { if (change == InputDeviceChange.Removed || change == InputDeviceChange.Disconnected) { if (device.deviceId == _lastDeviceId) { _lastDeviceId = -1; _lastInputTime = -Mathf.Infinity; CurrentDeviceName = ""; CurrentCategory = InputDeviceCategory.Keyboard; EmitChange(); } } } // ------------------ 分类逻辑 -------------------- private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device) { if (device == null) return InputDeviceCategory.Keyboard; // 重要:鼠标不再被视为键盘类(避免鼠标触发时回退到 Keyboard) if (device is Keyboard) return InputDeviceCategory.Keyboard; if (device is Mouse) return InputDeviceCategory.Other; // 明确忽略鼠标 if (IsGamepadLike(device)) return GetGamepadCategory(device); string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower(); if (combined.Contains("xbox") || combined.Contains("xinput")) return InputDeviceCategory.Xbox; if (combined.Contains("dualshock") || combined.Contains("dualsense") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation; return InputDeviceCategory.Other; } private static bool IsGamepadLike(InputDevice device) { if (device is Gamepad) return true; if (device is Joystick) return true; var layout = (device.layout ?? "").ToLower(); // 这里保留 controller/gamepad/joystick 的识别,但忽略 mouse/touch 等 if (layout.Contains("mouse") || layout.Contains("touch") || layout.Contains("pen")) return false; return layout.Contains("gamepad") || layout.Contains("controller") || layout.Contains("joystick"); } private static bool IsRelevantDevice(InputDevice device) { if (device == null) return false; if (device is Keyboard) return true; if (IsGamepadLike(device)) return true; return false; } private static InputDeviceCategory GetGamepadCategory(InputDevice device) { if (device == null) return InputDeviceCategory.Other; var iface = (device.description.interfaceName ?? "").ToLower(); if (iface.Contains("xinput")) return InputDeviceCategory.Xbox; if (TryParseVidPidFromCapabilities(device.description.capabilities, out int vendorId, out int _)) { if (vendorId == 0x045E || vendorId == 1118) return InputDeviceCategory.Xbox; if (vendorId == 0x054C || vendorId == 1356) return InputDeviceCategory.PlayStation; } string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower(); if (combined.Contains("xbox")) return InputDeviceCategory.Xbox; if (combined.Contains("dualshock") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation; return InputDeviceCategory.Other; } // ------------------ VID/PID 解析 -------------------- private static bool TryParseVidPidFromCapabilities(string capabilities, out int vendorId, out int productId) { vendorId = 0; productId = 0; if (string.IsNullOrEmpty(capabilities)) return false; try { var decVendor = Regex.Match(capabilities, "\"vendorId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase); var decProduct = Regex.Match(capabilities, "\"productId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase); if (decVendor.Success) int.TryParse(decVendor.Groups[1].Value, out vendorId); if (decProduct.Success) int.TryParse(decProduct.Groups[1].Value, out productId); return vendorId != 0 || productId != 0; } catch { return false; } } private static void EmitChange() { if (CurrentCategory == _lastEmittedCategory) { return; } int vid = GetVendorId(); int pid = GetProductId(); #if UNITY_EDITOR Debug.Log($"输入设备变更 -> {CurrentCategory} 触发设备: {CurrentDeviceName} vid=0x{vid:X} pid=0x{pid:X}"); #endif OnDeviceChanged?.Invoke(CurrentCategory); _lastEmittedCategory = CurrentCategory; } private static int GetVendorId() { foreach (var d in InputSystem.devices) { if ((d.displayName ?? "") == CurrentDeviceName && TryParseVidPidFromCapabilities(d.description.capabilities, out int v, out int _)) return v; } return 0; } private static int GetProductId() { foreach (var d in InputSystem.devices) { if ((d.displayName ?? "") == CurrentDeviceName && TryParseVidPidFromCapabilities(d.description.capabilities, out int _, out int p)) return p; } return 0; } } #endif