using System; using System.Collections; using System.Collections.Generic; using Unity.XR.CoreUtils; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Interactions; using UnityEngine.XR; using CommonUsages = UnityEngine.XR.CommonUsages; using InputDevice = UnityEngine.XR.InputDevice; namespace Unity.XR.XREAL.Samples { public enum ControllerHandEnum { None = -1, RightHand, LeftHand, } public enum ControllerButton { TRIGGER = 0x01, APP = 0x02, HOME = 0x04, } public enum ButtonEventType { Down, Pressing, LongPressDown, Up, Click, } /// /// A utility class for managing input compatibility with legacy versions of the NRSdk. /// This class ensures backward compatibility with older SDK versions. /// /// Note: This class only supports InputSource types of Controller and ControllerAndHands. /// [DefaultExecutionOrder(-150)] public class XREALInput : SingletonMonoBehaviour { /// The time interval (in seconds) within which a click is considered valid. public const float ClickInterval = 0.3f; /// The time interval (in seconds) required for a press to be considered a long press. public const float LongPressInterval = 1.0f; /// The current input source type. [ReadOnly] [SerializeField] private InputSource mInputSourceType = InputSource.Controller; public static InputSource CurrentInputSourceType { get { if (s_Singleton != null) return Singleton.mInputSourceType; return XREALPlugin.GetInputSource(); } } /// The InputActionProperty for the TRIGGER button action. [SerializeField] private InputActionProperty m_TriggerActionProperty; /// The InputActionProperty for the HOME button action. [SerializeField] private InputActionProperty m_HomeActionProperty; /// The InputActionProperty for the APP button action. [SerializeField] private InputActionProperty m_AppActionProperty; /// The InputActionProperty for the touchpad scroll action. [SerializeField] private InputActionProperty m_TouchActionProperty; /// The main camera used in the XROrigin setup. public Camera MainCamera => XREALUtility.MainCamera; /// Triggered before the controller is recentered. public static Action OnBeforeControllerRecenter; /// Triggered after the controller has been recentered. public static Action OnControllerRecentered; /// Triggered when an InputSource starts. public static Action OnInputSourceStarted; /// Triggered when an InputSource stops public static Action OnInputSourceStop; private Dictionary[] mRightListenerArr = new Dictionary[Enum.GetValues(typeof(ButtonEventType)).Length]; private Dictionary mRightDownTimeDic = new Dictionary(); private int mControllerButtonValue = 0; private int mControllerLongButtonValue = 0; private int mControllerLastButtonValue = 0; private int mControllerLastLongButtonValue = 0; private bool mControllerTouchpadPressed = false; private bool mControllerTouchpadLastPressed = false; private Vector2 mControllerTouchPos = Vector2.zero; private Vector2 mControllerLastTouchPos = Vector2.zero; private Vector2 mControllerTouchDelta = Vector2.zero; private Coroutine mAfterUpdateCoroutine = null; protected override void Awake() { base.Awake(); mInputSourceType = XREALPlugin.GetInputSource(); #if UNITY_EDITOR && UNITY_ANDROID mInputSourceType = XREALSettings.GetSettings().InitialInputSource; #endif if (m_TriggerActionProperty.action != null) { m_TriggerActionProperty.action.performed += TriggerActionPerformed; m_TriggerActionProperty.action.canceled += TriggerActionCanceled; } if (m_HomeActionProperty.action != null) { m_HomeActionProperty.action.performed += HomeActionPerformed; m_HomeActionProperty.action.performed += HomeActionCanceled; } if (m_AppActionProperty.action != null) { m_AppActionProperty.action.performed += AppActionPerformed; m_AppActionProperty.action.canceled += AppActionCanceled; } if (m_TouchActionProperty.action != null) { m_TouchActionProperty.action.performed += TouchActionPerformed; m_TouchActionProperty.action.canceled += TouchActionCanceled; } } private void OnEnable() { if (mAfterUpdateCoroutine != null) StopCoroutine(mAfterUpdateCoroutine); mAfterUpdateCoroutine = StartCoroutine(SyncAfterUpdate()); } private void Update() { if (mControllerTouchpadPressed || !mControllerTouchpadLastPressed) { mControllerTouchDelta = mControllerTouchPos - mControllerLastTouchPos; } CheckButtonEvent(); } private void OnDisable() { if (mAfterUpdateCoroutine != null) { StopCoroutine(mAfterUpdateCoroutine); mAfterUpdateCoroutine = null; } } protected override void OnDestroy() { base.OnDestroy(); if (m_TriggerActionProperty.action != null) { m_TriggerActionProperty.action.performed -= TriggerActionPerformed; m_TriggerActionProperty.action.canceled -= TriggerActionCanceled; } if (m_HomeActionProperty.action != null) { m_HomeActionProperty.action.performed -= HomeActionPerformed; m_HomeActionProperty.action.performed -= HomeActionCanceled; } if (m_AppActionProperty.action != null) { m_AppActionProperty.action.performed -= AppActionPerformed; m_AppActionProperty.action.canceled -= AppActionCanceled; } if (m_TouchActionProperty.action != null) { m_TouchActionProperty.action.performed -= TouchActionPerformed; m_TouchActionProperty.action.canceled -= TouchActionCanceled; } } private IEnumerator SyncAfterUpdate() { WaitForEndOfFrame frameEnd = new WaitForEndOfFrame(); while (true) { yield return frameEnd; mControllerLastButtonValue = mControllerButtonValue; mControllerLastLongButtonValue = mControllerLongButtonValue; mControllerLastTouchPos = mControllerTouchPos; mControllerTouchpadLastPressed = mControllerTouchpadPressed; } } /// /// Sets the current input source type. /// This method allows you to specify which type of input source is being used, /// /// /// public static bool SetInputSource(InputSource inputSourceType) { if (s_Singleton != null) { Debug.Log(string.Format("[input] xrealInput SetInputSource currentInput = {0} targetInput = {1}", Singleton.mInputSourceType, inputSourceType)); if (Singleton.mInputSourceType == inputSourceType) return false; OnInputSourceStop?.Invoke(Singleton.mInputSourceType); XREALPlugin.SetInputSource(inputSourceType); Singleton.mInputSourceType = inputSourceType; OnInputSourceStarted?.Invoke(inputSourceType); return true; } return false; } /// /// Checks if the touchpad is currently being touched. /// /// public static bool IsTouching() { if (s_Singleton != null) return (s_Singleton.mControllerButtonValue & (int)ControllerButton.TRIGGER) != 0; return false; } /// /// Checks if the touchpad has started a scroll interaction. /// /// public static bool IsTouchScrollStart() { if (s_Singleton != null) return s_Singleton.mControllerTouchpadPressed && !s_Singleton.mControllerTouchpadLastPressed; return false; } /// /// Checks if the touchpad is currently scrolling. /// /// public static bool IsTouchScrolling() { if (s_Singleton != null) return s_Singleton.mControllerTouchpadPressed; return false; } /// /// Checks if the touchpad has stopped scrolling. /// /// public static bool IsTouchScrollStop() { if (s_Singleton != null) return s_Singleton.mControllerTouchpadLastPressed && !s_Singleton.mControllerTouchpadPressed; return false; } /// /// Gets the current touch position on the touchpad. /// /// /// values ranging from -1 to 1 for both X and Y axes public static Vector2 GetTouch() { if (s_Singleton != null) { if (s_Singleton.mControllerTouchpadPressed || !s_Singleton.mControllerTouchpadLastPressed) return s_Singleton.mControllerTouchPos; else if (s_Singleton.mControllerTouchpadLastPressed) return s_Singleton.mControllerLastTouchPos; } return Vector2.zero; } /// /// Gets the change in touch position on the touchpad since the last frame. /// /// public static Vector2 GetDeltaTouch() { if (s_Singleton != null) return s_Singleton.mControllerTouchDelta; return Vector2.zero; } /// /// Gets the current position of the controller in 3D space. /// /// public static Vector3 GetPosition() { if (CurrentInputSourceType == InputSource.Controller || CurrentInputSourceType == InputSource.ControllerAndHands) { Vector3 position = Vector3.zero; InputDevice inputDevice = InputDevices.GetDeviceAtXRNode(XRNode.RightHand); inputDevice.TryGetFeatureValue(CommonUsages.devicePosition, out position); return position; } return Vector3.one; } /// /// Gets the current rotation of the controller in 3D space. /// /// public static Quaternion GetRotation() { if (CurrentInputSourceType == InputSource.Controller || CurrentInputSourceType == InputSource.ControllerAndHands) { Quaternion rotation = Quaternion.identity; InputDevice inputDevice = InputDevices.GetDeviceAtXRNode(XRNode.RightHand); inputDevice.TryGetFeatureValue(CommonUsages.deviceRotation, out rotation); return rotation; } return Quaternion.identity; } /// /// Checks if the controller button was pressed down in the current frame. /// /// /// public static bool GetButtonDown(ControllerButton button) { if (s_Singleton != null) return ((s_Singleton.mControllerButtonValue & (int)button) != 0) && ((s_Singleton.mControllerLastButtonValue & (int)button) == 0); return false; } /// /// Checks if the controller button was was released in the current frame. /// /// /// public static bool GetButtonUp(ControllerButton button) { if (s_Singleton != null) return ((s_Singleton.mControllerLastButtonValue & (int)button) != 0) && ((s_Singleton.mControllerButtonValue & (int)button) == 0); return false; } /// /// Checks if the controller button is currently pressed. /// /// /// public static bool GetButton(ControllerButton button) { if (s_Singleton != null) return (s_Singleton.mControllerButtonValue & (int)button) != 0; return false; } /// /// Triggers a haptic vibration on the controller. /// /// /// /// public static bool TriggerHapticVibration(float durationSeconds = 0.1f, float amplitude = 0.8f) { #if !UNITY_EDITOR return XREALVirtualController.Singleton.Controller.SendHapticImpulse(0, amplitude, durationSeconds); #endif return false; } /// /// Triggers a haptic vibration on the controller. /// /// /// /// public static bool TriggerHapticVibration(ControllerHandEnum hand, float durationSeconds = 0.1f, float amplitude = 0.8f) { #if !UNITY_EDITOR return XREALVirtualController.Singleton.Controller.SendHapticImpulse(0, amplitude, durationSeconds); #endif return false; } /// /// Adds a listener for a specific controller button event. /// /// The type of button event to listen for, represented by the /// The controller button to listen for, represented by the /// The to execute when the specified button event occurs. public static void AddButtonListener(ButtonEventType buttonEventType, ControllerButton button, Action action) { if (s_Singleton != null) { Debug.Log("[input] AddButtonListener " + buttonEventType + " " + button); int btnEventID = (int)buttonEventType; if (Singleton.mRightListenerArr[btnEventID] == null) Singleton.mRightListenerArr[btnEventID] = new Dictionary(); var buttonListernerArr = Singleton.mRightListenerArr; if (buttonListernerArr[btnEventID].ContainsKey(button)) { buttonListernerArr[btnEventID][button] += action; } else { buttonListernerArr[btnEventID].Add(button, action); } } } /// /// Removes a listener for a specific controller button event. /// /// /// /// public static void RemoveButtonListener(ButtonEventType buttonEventType, ControllerButton button, Action action) { if (s_Singleton != null) { int btnEventID = (int)buttonEventType; if (Singleton.mRightListenerArr[btnEventID] == null) Singleton.mRightListenerArr[btnEventID] = new Dictionary(); var buttonListernerArr = Singleton.mRightListenerArr; if (buttonListernerArr[btnEventID].ContainsKey(button) && buttonListernerArr[btnEventID][button] != null) { buttonListernerArr[btnEventID][button] -= action; if (buttonListernerArr[btnEventID][button] == null) { buttonListernerArr[btnEventID].Remove(button); } } } } /// /// Re-centers the controller's orientation /// public static void RecenterController() { if (CurrentInputSourceType == InputSource.Controller || CurrentInputSourceType == InputSource.ControllerAndHands) { OnBeforeControllerRecenter?.Invoke(); XREALPlugin.RecenterController(); OnControllerRecentered?.Invoke(); } } private void CheckButtonEvent() { if (mControllerButtonValue == 0 && mControllerLastButtonValue == 0) return; foreach (ControllerButton button in Enum.GetValues(typeof(ControllerButton))) { //down event int buttonNum = (int)button; if ((mControllerButtonValue & buttonNum) != 0 && !mRightDownTimeDic.ContainsKey(button)) { TryInvokeListener(ButtonEventType.Down, button); mRightDownTimeDic[button] = Time.unscaledTime; } //press event if ((mControllerButtonValue & buttonNum) != 0) { TryInvokeListener(ButtonEventType.Pressing, button); if (Time.unscaledTime - mRightDownTimeDic[button] >= LongPressInterval) { mControllerLongButtonValue |= buttonNum; if ((mControllerLastLongButtonValue & buttonNum) == 0) { TryInvokeListener(ButtonEventType.LongPressDown, button); } } } //up event if ((mControllerLastButtonValue & buttonNum) != 0 && (mControllerButtonValue & buttonNum) == 0) { mControllerLongButtonValue &= ~buttonNum; TryInvokeListener(ButtonEventType.Up, button); if (Time.unscaledTime - mRightDownTimeDic[button] <= ClickInterval) TryInvokeListener(ButtonEventType.Click, button); mRightDownTimeDic.Remove(button); } } } private void TryInvokeListener(ButtonEventType buttonEventType, ControllerButton button) { var buttonListernerArr = mRightListenerArr; int btnEventID = (int)buttonEventType; if (buttonListernerArr == null || buttonListernerArr[btnEventID] == null) return; if (buttonListernerArr[btnEventID].ContainsKey(button) && buttonListernerArr[btnEventID][button] != null) buttonListernerArr[btnEventID][button].Invoke(); } private void AppActionPerformed(InputAction.CallbackContext context) { mControllerButtonValue |= (int)ControllerButton.APP; } private void AppActionCanceled(InputAction.CallbackContext context) { mControllerButtonValue &= ~(int)ControllerButton.APP; } private void HomeActionPerformed(InputAction.CallbackContext context) { if (context.interaction is PressInteraction) mControllerButtonValue |= (int)ControllerButton.HOME; } private void HomeActionCanceled(InputAction.CallbackContext context) { mControllerButtonValue &= ~(int)ControllerButton.HOME; } private void TriggerActionPerformed(InputAction.CallbackContext context) { mControllerButtonValue |= (int)ControllerButton.TRIGGER; } private void TriggerActionCanceled(InputAction.CallbackContext context) { mControllerButtonValue &= ~(int)ControllerButton.TRIGGER; } private void TouchActionPerformed(InputAction.CallbackContext context) { mControllerTouchPos = context.ReadValue(); mControllerTouchpadPressed = true; } private void TouchActionCanceled(InputAction.CallbackContext context) { mControllerTouchPos = Vector2.zero; mControllerTouchpadPressed = false; } } }