using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.EnhancedTouch; using UnityEngine.InputSystem.Utilities; using EnhancedTouchSupport = UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport; using EnhancedTouchTouch = UnityEngine.InputSystem.EnhancedTouch.Touch; using EnhancedTouchPhase = UnityEngine.InputSystem.TouchPhase; using UnityTouch = UnityEngine.Touch; using TouchPhaseLegacy = UnityEngine.TouchPhase; /// /// Aggregates look input from mouse, gamepad sticks, and mobile touch into a single delta value. /// Works with either the legacy Input Manager or the newer Input System. /// [DisallowMultipleComponent] public sealed class LookInputRouter : MonoBehaviour { private const float TinyThreshold = 0.0001f; [Header("General")] [SerializeField] private bool autoDetectInputSystem = true; [SerializeField] private bool preferNewInputSystem = true; [Header("Input System (New)")] [Tooltip("Optional PlayerInput component containing the action map with a Look action.")] [SerializeField] private PlayerInput playerInput; [Tooltip("Action name used for look deltas when using the new Input System.")] [SerializeField] private string lookActionName = "Look"; [Header("Legacy Input Manager")] [SerializeField] private string mouseXAxis = "Mouse X"; [SerializeField] private string mouseYAxis = "Mouse Y"; [SerializeField] private string rightStickXAxis = "Joystick X"; [SerializeField] private string rightStickYAxis = "Joystick Y"; [Header("Sensitivities")] [SerializeField] private float mouseSensitivity = 1.0f; [SerializeField] private float stickSensitivity = 120.0f; [SerializeField] private float touchSensitivity = 0.15f; [Header("Filtering")] [Tooltip("Inputs below this magnitude will be ignored and reset to zero.")] [SerializeField] private float deadzone = 0.05f; [Tooltip("Optional smoothing duration in seconds. Set to zero for raw input.")] [SerializeField] private float smoothingTime = 0.08f; [Tooltip("Maximum magnitude allowed for a single look delta.")] [SerializeField] private float maxMagnitude = 5.0f; private InputAction cachedLookAction; private bool usingNewInputSystem; private Vector2 currentDelta; private Vector2 smoothVelocity; // Touch state private int activeTouchId = -1; private Vector2 lastTouchPosition; private void Awake() { DetectInputSystem(); } private void OnEnable() { // Enable Enhanced Touch so we can use the high-level Touch struct API. EnhancedTouchSupport.Enable(); if (usingNewInputSystem) { EnableLookAction(); } } private void OnDisable() { if (cachedLookAction != null) { cachedLookAction.Disable(); } cachedLookAction = null; // Pair with Enable() to keep the global reference count balanced. EnhancedTouchSupport.Disable(); } private void Update() { Vector2 rawDelta = usingNewInputSystem ? ReadNewSystemDelta() : ReadLegacyDelta(); if (!usingNewInputSystem && preferNewInputSystem) { // If no PlayerInput was supplied but the Input System package is present, // fall back to gathering from Gamepad/Touchscreen directly. rawDelta += ReadDirectDeviceDelta(); } rawDelta = Vector2.ClampMagnitude(rawDelta, maxMagnitude); if (smoothingTime > TinyThreshold) { currentDelta = Vector2.SmoothDamp(currentDelta, rawDelta, ref smoothVelocity, smoothingTime, Mathf.Infinity, Time.unscaledDeltaTime); } else { currentDelta = rawDelta; } if (currentDelta.magnitude < deadzone) { currentDelta = Vector2.zero; } } public bool TryGetLookDelta(out Vector2 delta) { delta = currentDelta; return delta.sqrMagnitude > TinyThreshold; } private void DetectInputSystem() { if (!autoDetectInputSystem) { usingNewInputSystem = preferNewInputSystem && playerInput != null; CacheLookAction(); return; } // If PlayerInput is explicitly assigned, use it. if (playerInput == null) { playerInput = GetComponent(); } if (playerInput != null && preferNewInputSystem) { usingNewInputSystem = true; CacheLookAction(); } else { usingNewInputSystem = false; } } private void CacheLookAction() { if (playerInput == null) return; var actions = playerInput.actions; if (actions == null) return; cachedLookAction = actions.FindAction(lookActionName, false); if (cachedLookAction != null && !cachedLookAction.enabled && isActiveAndEnabled) { cachedLookAction.SafeEnable(); } } private void EnableLookAction() { if (cachedLookAction == null && playerInput != null) { CacheLookAction(); } cachedLookAction?.SafeEnable(); } private Vector2 ReadNewSystemDelta() { if (cachedLookAction == null) return ReadDirectDeviceDelta(); Vector2 value = cachedLookAction.ReadValue(); return value * Time.unscaledDeltaTime; } private Vector2 ReadLegacyDelta() { // Replace legacy Input.GetAxis usage with InputSystem device reads where possible. float x = 0f, y = 0f; if (UnityEngine.InputSystem.Mouse.current != null) { Vector2 mouseDelta = UnityEngine.InputSystem.Mouse.current.delta.ReadValue(); x = mouseDelta.x * mouseSensitivity; y = mouseDelta.y * mouseSensitivity; } float stickX = 0f, stickY = 0f; if (UnityEngine.InputSystem.Gamepad.current != null) { Vector2 right = UnityEngine.InputSystem.Gamepad.current.rightStick.ReadValue(); stickX = right.x * stickSensitivity * Time.unscaledDeltaTime; stickY = right.y * stickSensitivity * Time.unscaledDeltaTime; } return new Vector2(x + stickX, y + stickY); } private Vector2 ReadDirectDeviceDelta() { Vector2 aggregate = Vector2.zero; if (Mouse.current != null) { Vector2 mouseDelta = Mouse.current.delta.ReadValue(); aggregate += mouseDelta * mouseSensitivity * Time.unscaledDeltaTime; } if (Gamepad.current != null) { Vector2 stick = Gamepad.current.rightStick.ReadValue(); aggregate += stick * stickSensitivity * Time.unscaledDeltaTime; } aggregate += ReadTouchDelta(); return aggregate; } private Vector2 ReadTouchDelta() { if (EnhancedTouchSupport.enabled) { var enhancedDelta = ReadEnhancedTouchDelta(); if (enhancedDelta != Vector2.zero || EnhancedTouchTouch.activeTouches.Count > 0) return enhancedDelta; } if (Touchscreen.current != null) { var touchscreenDelta = ReadTouchscreenDelta(); if (touchscreenDelta != Vector2.zero) return touchscreenDelta; } return ReadLegacyTouch(); } private Vector2 ReadEnhancedTouchDelta() { var touches = EnhancedTouchTouch.activeTouches; if (touches.Count <= 0) { ResetTouchTracking(); return Vector2.zero; } for (int i = 0; i < touches.Count; ++i) { var touch = touches[i]; if (!touch.valid) continue; if (activeTouchId == -1) { activeTouchId = touch.touchId; lastTouchPosition = touch.screenPosition; } if (touch.touchId != activeTouchId) continue; if (touch.phase == EnhancedTouchPhase.Ended || touch.phase == EnhancedTouchPhase.Canceled) { ResetTouchTracking(); return Vector2.zero; } if (touch.phase == EnhancedTouchPhase.Began) { lastTouchPosition = touch.screenPosition; return Vector2.zero; } Vector2 delta = (touch.screenPosition - lastTouchPosition) * touchSensitivity * Time.unscaledDeltaTime; lastTouchPosition = touch.screenPosition; return delta; } return Vector2.zero; } private Vector2 ReadTouchscreenDelta() { if (Touchscreen.current == null) { return Vector2.zero; } if (!AnyTouchPressed(Touchscreen.current.touches)) { ResetTouchTracking(); return Vector2.zero; } foreach (var touchControl in Touchscreen.current.touches) { if (!touchControl.press.isPressed) continue; int id = touchControl.touchId.ReadValue(); Vector2 position = touchControl.position.ReadValue(); if (activeTouchId == -1) { activeTouchId = id; lastTouchPosition = position; continue; } if (activeTouchId != id) continue; Vector2 delta = (position - lastTouchPosition) * touchSensitivity; lastTouchPosition = position; return delta * Time.unscaledDeltaTime; } return Vector2.zero; } private Vector2 ReadLegacyTouch() { // Prefer the Input System touchscreen if available. Fall back to legacy Input.touches only when necessary. if (UnityEngine.InputSystem.Touchscreen.current != null) { var touches = UnityEngine.InputSystem.Touchscreen.current.touches; if (touches.Count <= 0) { ResetTouchTracking(); return Vector2.zero; } for (int i = 0; i < touches.Count; ++i) { var tc = touches[i]; if (!tc.press.isPressed) continue; int id = tc.touchId.ReadValue(); Vector2 pos = tc.position.ReadValue(); // We already check tc.press.isPressed above. No additional phase check is required // here for the Input System touch path. This avoids ambiguous TouchPhase enum // comparisons between the legacy and new Input System types. if (activeTouchId == -1) { activeTouchId = id; lastTouchPosition = pos; continue; } if (activeTouchId != id) continue; Vector2 delta = (pos - lastTouchPosition) * touchSensitivity * Time.unscaledDeltaTime; lastTouchPosition = pos; return delta; } return Vector2.zero; } // Legacy fallback if (Input.touchCount <= 0) { ResetTouchTracking(); return Vector2.zero; } for (int i = 0; i < Input.touchCount; i++) { UnityTouch touch = Input.GetTouch(i); if (touch.phase == TouchPhaseLegacy.Ended || touch.phase == TouchPhaseLegacy.Canceled) continue; if (activeTouchId == -1) { activeTouchId = touch.fingerId; lastTouchPosition = touch.position; continue; } if (activeTouchId != touch.fingerId) continue; Vector2 delta = touch.deltaPosition * touchSensitivity * Time.unscaledDeltaTime; lastTouchPosition = touch.position; return delta; } return Vector2.zero; } private void ResetTouchTracking() { activeTouchId = -1; lastTouchPosition = Vector2.zero; } public void ResetLookDelta() { currentDelta = Vector2.zero; smoothVelocity = Vector2.zero; } private static bool AnyTouchPressed(ReadOnlyArray touches) { for (int i = 0; i < touches.Count; ++i) { if (touches[i].press.isPressed) return true; } return false; } }