AlicizaX/Client/Assets/Scripts/CustomeModule/InputGlyph/InputDeviceWatcher.cs

473 lines
15 KiB
C#
Raw Normal View History

2026-03-19 14:24:12 +08:00
using System;
using System.Collections.Generic;
#if UNITY_EDITOR
2026-03-11 14:18:25 +08:00
using UnityEditor;
#endif
2026-03-11 14:18:25 +08:00
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
2026-03-11 14:18:25 +08:00
public static class InputDeviceWatcher
{
public enum InputDeviceCategory
{
Keyboard,
Xbox,
PlayStation,
Other
}
public readonly struct DeviceContext : IEquatable<DeviceContext>
{
public readonly InputDeviceCategory Category;
public readonly int DeviceId;
public readonly int VendorId;
public readonly int ProductId;
public readonly string DeviceName;
public readonly string Layout;
public DeviceContext(
InputDeviceCategory category,
int deviceId,
int vendorId,
int productId,
string deviceName,
string layout)
{
Category = category;
DeviceId = deviceId;
VendorId = vendorId;
ProductId = productId;
DeviceName = deviceName ?? string.Empty;
Layout = layout ?? string.Empty;
}
2026-03-11 14:18:25 +08:00
public bool Equals(DeviceContext other)
{
return Category == other.Category
&& DeviceId == other.DeviceId
&& VendorId == other.VendorId
&& ProductId == other.ProductId
&& string.Equals(DeviceName, other.DeviceName, StringComparison.Ordinal)
&& string.Equals(Layout, other.Layout, StringComparison.Ordinal);
}
2026-03-11 14:18:25 +08:00
public override bool Equals(object obj)
{
return obj is DeviceContext other && Equals(other);
}
2026-03-11 14:18:25 +08:00
public override int GetHashCode()
{
unchecked
{
int hashCode = (int)Category;
hashCode = (hashCode * 397) ^ DeviceId;
hashCode = (hashCode * 397) ^ VendorId;
hashCode = (hashCode * 397) ^ ProductId;
hashCode = (hashCode * 397) ^ (DeviceName != null ? DeviceName.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (Layout != null ? Layout.GetHashCode() : 0);
return hashCode;
}
}
}
2026-03-11 14:18:25 +08:00
[Serializable]
private struct DeviceCapabilityInfo
{
public int vendorId;
public int productId;
}
private const float SameCategoryDebounceWindow = 0.15f;
private const float AxisActivationThreshold = 0.5f;
private const float StickActivationThreshold = 0.25f;
private const string DefaultKeyboardDeviceName = "Keyboard&Mouse";
public static InputDeviceCategory CurrentCategory { get; private set; } = InputDeviceCategory.Keyboard;
public static string CurrentDeviceName { get; private set; } = DefaultKeyboardDeviceName;
public static int CurrentDeviceId { get; private set; } = -1;
public static int CurrentVendorId { get; private set; }
public static int CurrentProductId { get; private set; }
public static DeviceContext CurrentContext { get; private set; } = CreateDefaultContext();
private static InputAction _anyInputAction;
private static float _lastSwitchTime = -Mathf.Infinity;
private static DeviceContext _lastEmittedContext = CreateDefaultContext();
2026-03-19 14:24:12 +08:00
private static readonly Dictionary<int, DeviceContext> DeviceContextCache = new();
private static bool _initialized;
public static event Action<InputDeviceCategory> OnDeviceChanged;
public static event Action<DeviceContext> OnDeviceContextChanged;
2026-03-11 14:18:25 +08:00
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void Initialize()
{
if (_initialized)
{
return;
}
2026-03-11 14:18:25 +08:00
_initialized = true;
ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext;
2026-03-11 14:18:25 +08:00
_anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough);
_anyInputAction.AddBinding("<Keyboard>/anyKey");
2026-03-19 20:17:04 +08:00
// _anyInputAction.AddBinding("<Mouse>/leftButton");
// _anyInputAction.AddBinding("<Mouse>/rightButton");
// _anyInputAction.AddBinding("<Mouse>/middleButton");
// _anyInputAction.AddBinding("<Mouse>/scroll");
2026-03-11 14:18:25 +08:00
_anyInputAction.AddBinding("<Gamepad>/*");
_anyInputAction.AddBinding("<Joystick>/*");
_anyInputAction.performed += OnAnyInputPerformed;
_anyInputAction.Enable();
InputSystem.onDeviceChange += OnDeviceChange;
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#endif
}
#if UNITY_EDITOR
private static void OnPlayModeStateChanged(PlayModeStateChange state)
2026-03-11 14:18:25 +08:00
{
if (state == PlayModeStateChange.ExitingPlayMode)
{
Dispose();
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
2026-03-11 14:18:25 +08:00
}
}
#endif
public static void Dispose()
{
if (!_initialized)
{
return;
}
if (_anyInputAction != null)
{
_anyInputAction.performed -= OnAnyInputPerformed;
_anyInputAction.Disable();
_anyInputAction.Dispose();
_anyInputAction = null;
}
2026-03-11 14:18:25 +08:00
InputSystem.onDeviceChange -= OnDeviceChange;
2026-03-19 14:24:12 +08:00
DeviceContextCache.Clear();
2026-03-11 14:18:25 +08:00
ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext;
_lastSwitchTime = -Mathf.Infinity;
2026-03-11 14:18:25 +08:00
OnDeviceChanged = null;
OnDeviceContextChanged = null;
_initialized = false;
2026-03-11 14:18:25 +08:00
}
private static void OnAnyInputPerformed(InputAction.CallbackContext context)
2026-03-11 14:18:25 +08:00
{
InputControl control = context.control;
if (!IsRelevantControl(control))
{
return;
}
2026-03-11 14:18:25 +08:00
2026-03-19 14:24:12 +08:00
InputDevice device = control.device;
if (device == null || device.deviceId == CurrentDeviceId)
{
return;
}
DeviceContext deviceContext = BuildContext(device);
if (deviceContext.DeviceId == CurrentDeviceId)
{
return;
}
2026-03-11 14:18:25 +08:00
float now = Time.realtimeSinceStartup;
if (deviceContext.Category == CurrentCategory && now - _lastSwitchTime < SameCategoryDebounceWindow)
{
return;
}
2026-03-11 14:18:25 +08:00
_lastSwitchTime = now;
SetCurrentContext(deviceContext);
}
2026-03-11 14:18:25 +08:00
private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
if (device == null)
{
return;
}
2026-03-11 14:18:25 +08:00
switch (change)
{
case InputDeviceChange.Removed:
case InputDeviceChange.Disconnected:
2026-03-19 14:24:12 +08:00
DeviceContextCache.Remove(device.deviceId);
if (device.deviceId == CurrentDeviceId)
{
PromoteFallbackDevice(device.deviceId);
}
break;
case InputDeviceChange.Reconnected:
case InputDeviceChange.Added:
2026-03-19 14:24:12 +08:00
DeviceContextCache.Remove(device.deviceId);
if (CurrentDeviceId < 0 && IsRelevantDevice(device))
{
SetCurrentContext(BuildContext(device));
}
break;
}
2026-03-11 14:18:25 +08:00
}
private static void PromoteFallbackDevice(int removedDeviceId)
2026-03-11 14:18:25 +08:00
{
for (int i = InputSystem.devices.Count - 1; i >= 0; i--)
2026-03-11 14:18:25 +08:00
{
InputDevice device = InputSystem.devices[i];
if (device == null || device.deviceId == removedDeviceId || !device.added || !IsRelevantDevice(device))
2026-03-11 14:18:25 +08:00
{
continue;
2026-03-11 14:18:25 +08:00
}
SetCurrentContext(BuildContext(device));
return;
2026-03-11 14:18:25 +08:00
}
SetCurrentContext(CreateDefaultContext());
2026-03-11 14:18:25 +08:00
}
private static void SetCurrentContext(DeviceContext context)
2026-03-11 14:18:25 +08:00
{
bool categoryChanged = CurrentCategory != context.Category;
ApplyContext(context, true);
2026-03-11 14:18:25 +08:00
if (!_lastEmittedContext.Equals(context))
{
OnDeviceContextChanged?.Invoke(context);
if (categoryChanged)
{
OnDeviceChanged?.Invoke(context.Category);
}
2026-03-11 14:18:25 +08:00
_lastEmittedContext = context;
}
}
2026-03-11 14:18:25 +08:00
private static void ApplyContext(DeviceContext context, bool log)
{
CurrentContext = context;
CurrentCategory = context.Category;
CurrentDeviceId = context.DeviceId;
CurrentVendorId = context.VendorId;
CurrentProductId = context.ProductId;
CurrentDeviceName = context.DeviceName;
#if UNITY_EDITOR
if (log)
{
AlicizaX.Log.Info($"Input device -> {CurrentCategory} name={CurrentDeviceName} vid=0x{CurrentVendorId:X} pid=0x{CurrentProductId:X} id={CurrentDeviceId}");
}
#endif
2026-03-11 14:18:25 +08:00
}
private static DeviceContext BuildContext(InputDevice device)
2026-03-11 14:18:25 +08:00
{
if (device == null)
{
return CreateDefaultContext();
}
2026-03-11 14:18:25 +08:00
2026-03-19 14:24:12 +08:00
if (DeviceContextCache.TryGetValue(device.deviceId, out DeviceContext cachedContext))
{
return cachedContext;
}
TryParseVendorProductIds(device.description.capabilities, out int vendorId, out int productId);
string deviceName = string.IsNullOrWhiteSpace(device.displayName) ? device.name : device.displayName;
2026-03-19 14:24:12 +08:00
DeviceContext context = new DeviceContext(
DetermineCategoryFromDevice(device, vendorId),
device.deviceId,
vendorId,
productId,
deviceName,
device.layout);
2026-03-19 14:24:12 +08:00
DeviceContextCache[device.deviceId] = context;
return context;
2026-03-11 14:18:25 +08:00
}
private static DeviceContext CreateDefaultContext()
2026-03-11 14:18:25 +08:00
{
return new DeviceContext(InputDeviceCategory.Keyboard, -1, 0, 0, DefaultKeyboardDeviceName, Keyboard.current != null ? Keyboard.current.layout : string.Empty);
2026-03-11 14:18:25 +08:00
}
2026-03-19 14:24:12 +08:00
private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device, int vendorId = 0)
2026-03-11 14:18:25 +08:00
{
if (device == null)
{
return InputDeviceCategory.Keyboard;
}
2026-03-11 14:18:25 +08:00
if (device is Keyboard || device is Mouse)
{
return InputDeviceCategory.Keyboard;
}
2026-03-11 14:18:25 +08:00
if (IsGamepadLike(device))
2026-03-11 14:18:25 +08:00
{
2026-03-19 14:24:12 +08:00
return GetGamepadCategory(device, vendorId);
2026-03-11 14:18:25 +08:00
}
2026-03-19 14:24:12 +08:00
if (DescriptionContains(device, "xbox") || DescriptionContains(device, "xinput"))
{
return InputDeviceCategory.Xbox;
}
2026-03-19 14:24:12 +08:00
if (DescriptionContains(device, "dualshock")
|| DescriptionContains(device, "dualsense")
|| DescriptionContains(device, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
2026-03-11 14:18:25 +08:00
return InputDeviceCategory.Other;
}
private static bool IsRelevantDevice(InputDevice device)
2026-03-11 14:18:25 +08:00
{
return device is Keyboard || device is Mouse || IsGamepadLike(device);
}
2026-03-11 14:18:25 +08:00
private static bool IsRelevantControl(InputControl control)
{
if (control == null || control.device == null || !IsRelevantDevice(control.device) || control.synthetic)
2026-03-11 14:18:25 +08:00
{
return false;
}
2026-03-11 14:18:25 +08:00
switch (control)
{
case ButtonControl button:
return button.IsPressed();
case StickControl stick:
return stick.ReadValue().sqrMagnitude >= StickActivationThreshold;
case Vector2Control vector2:
return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold;
case AxisControl axis:
return Mathf.Abs(axis.ReadValue()) >= AxisActivationThreshold;
default:
return !control.noisy;
}
}
2026-03-11 14:18:25 +08:00
private static bool IsGamepadLike(InputDevice device)
{
if (device is Gamepad || device is Joystick)
{
return true;
2026-03-11 14:18:25 +08:00
}
string layout = device.layout ?? string.Empty;
if (ContainsIgnoreCase(layout, "Mouse")
|| ContainsIgnoreCase(layout, "Touch")
|| ContainsIgnoreCase(layout, "Pen"))
2026-03-11 14:18:25 +08:00
{
return false;
}
return ContainsIgnoreCase(layout, "Gamepad")
|| ContainsIgnoreCase(layout, "Controller")
|| ContainsIgnoreCase(layout, "Joystick");
2026-03-11 14:18:25 +08:00
}
2026-03-19 14:24:12 +08:00
private static InputDeviceCategory GetGamepadCategory(InputDevice device, int vendorId = 0)
2026-03-11 14:18:25 +08:00
{
if (device == null)
2026-03-11 14:18:25 +08:00
{
return InputDeviceCategory.Other;
2026-03-11 14:18:25 +08:00
}
string interfaceName = device.description.interfaceName ?? string.Empty;
if (ContainsIgnoreCase(interfaceName, "xinput"))
{
return InputDeviceCategory.Xbox;
}
2026-03-11 14:18:25 +08:00
2026-03-19 14:24:12 +08:00
if (vendorId == 0 && TryParseVendorProductIds(device.description.capabilities, out int parsedVendorId, out _))
{
2026-03-19 14:24:12 +08:00
vendorId = parsedVendorId;
}
2026-03-11 14:18:25 +08:00
2026-03-19 14:24:12 +08:00
if (vendorId == 0x045E || vendorId == 1118)
{
return InputDeviceCategory.Xbox;
}
if (vendorId == 0x054C || vendorId == 1356)
{
return InputDeviceCategory.PlayStation;
}
2026-03-11 14:18:25 +08:00
2026-03-19 14:24:12 +08:00
if (DescriptionContains(device, "xbox"))
2026-03-11 14:18:25 +08:00
{
return InputDeviceCategory.Xbox;
2026-03-11 14:18:25 +08:00
}
2026-03-19 14:24:12 +08:00
if (DescriptionContains(device, "dualshock")
|| DescriptionContains(device, "dualsense")
|| DescriptionContains(device, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
return InputDeviceCategory.Other;
2026-03-11 14:18:25 +08:00
}
2026-03-19 14:24:12 +08:00
private static bool DescriptionContains(InputDevice device, string value)
2026-03-11 14:18:25 +08:00
{
2026-03-19 14:24:12 +08:00
if (device == null)
{
return false;
}
var description = device.description;
return ContainsIgnoreCase(description.interfaceName, value)
|| ContainsIgnoreCase(device.layout, value)
|| ContainsIgnoreCase(description.product, value)
|| ContainsIgnoreCase(description.manufacturer, value)
|| ContainsIgnoreCase(device.displayName, value)
|| ContainsIgnoreCase(device.name, value);
}
private static bool TryParseVendorProductIds(string capabilities, out int vendorId, out int productId)
{
vendorId = 0;
productId = 0;
if (string.IsNullOrWhiteSpace(capabilities))
2026-03-11 14:18:25 +08:00
{
return false;
2026-03-11 14:18:25 +08:00
}
try
{
DeviceCapabilityInfo info = JsonUtility.FromJson<DeviceCapabilityInfo>(capabilities);
vendorId = info.vendorId;
productId = info.productId;
return vendorId != 0 || productId != 0;
}
catch
{
return false;
}
}
private static bool ContainsIgnoreCase(string source, string value)
{
return !string.IsNullOrEmpty(source) && source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
2026-03-11 14:18:25 +08:00
}
}