com.alicizax.unity.ui.exten.../Runtime/Input/InputDeviceWatcher.cs
2025-12-17 20:31:54 +08:00

246 lines
8.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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<InputDeviceCategory> 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("<Keyboard>/anyKey");
_anyInputAction.AddBinding("<Gamepad>/*");
_anyInputAction.AddBinding("<Joystick>/*");
_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