using System; using System.Diagnostics; using UnityEngine.XR.Interaction.Toolkit.Inputs.Haptics; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Utilities; using UnityEngine.XR.Interaction.Toolkit.Utilities.Internal; namespace UnityEngine.XR.Interaction.Toolkit.Feedback { /// /// Data needed to describe a haptic impulse. /// [Serializable] public class HapticImpulseData { [SerializeField, Range(0f, 1f)] float m_Amplitude; /// /// The desired motor amplitude that should be within a [0-1] range. /// public float amplitude { get => m_Amplitude; set => m_Amplitude = value; } [SerializeField] float m_Duration; /// /// The desired duration of the impulse in seconds. /// public float duration { get => m_Duration; set => m_Duration = value; } [SerializeField] float m_Frequency; /// /// The desired frequency of the impulse in Hz. /// The default value of 0 means to use the default frequency of the device. /// Not all devices or XR Plug-ins support specifying a frequency. /// public float frequency { get => m_Frequency; set => m_Frequency = value; } } /// /// Component that responds to select and hover events by playing haptic impulses /// (in other words, vibrating the controller). /// /// [AddComponentMenu("XR/Feedback/Simple Haptic Feedback", 11)] [HelpURL(XRHelpURLConstants.k_SimpleHapticFeedback)] public class SimpleHapticFeedback : MonoBehaviour { [SerializeField] [RequireInterface(typeof(IXRInteractor))] Object m_InteractorSourceObject; [SerializeField] HapticImpulsePlayer m_HapticImpulsePlayer; /// /// The Haptic Impulse Player component to use to play haptic impulses. /// public HapticImpulsePlayer hapticImpulsePlayer { get => m_HapticImpulsePlayer; set => m_HapticImpulsePlayer = value; } [SerializeField] bool m_PlaySelectEntered; /// /// Whether to play a haptic impulse when the interactor starts selecting an interactable. /// public bool playSelectEntered { get => m_PlaySelectEntered; set => m_PlaySelectEntered = value; } [SerializeField] HapticImpulseData m_SelectEnteredData = new HapticImpulseData { amplitude = 0.5f, duration = 0.1f, }; /// /// The haptic impulse to play when the interactor starts selecting an interactable. /// public HapticImpulseData selectEnteredData { get => m_SelectEnteredData; set => m_SelectEnteredData = value; } [SerializeField] bool m_PlaySelectExited; /// /// Whether to play a haptic impulse when the interactor stops selecting an interactable without being canceled. /// public bool playSelectExited { get => m_PlaySelectExited; set => m_PlaySelectExited = value; } [SerializeField] HapticImpulseData m_SelectExitedData = new HapticImpulseData { amplitude = 0.5f, duration = 0.1f, }; /// /// The haptic impulse to play when the interactor stops selecting an interactable without being canceled. /// public HapticImpulseData selectExitedData { get => m_SelectExitedData; set => m_SelectExitedData = value; } [SerializeField] bool m_PlaySelectCanceled; /// /// Whether to play a haptic impulse when the interactor stops selecting an interactable due to being canceled. /// public bool playSelectCanceled { get => m_PlaySelectCanceled; set => m_PlaySelectCanceled = value; } [SerializeField] HapticImpulseData m_SelectCanceledData = new HapticImpulseData { amplitude = 0.5f, duration = 0.1f, }; /// /// The haptic impulse to play when the interactor stops selecting an interactable due to being canceled. /// public HapticImpulseData selectCanceledData { get => m_SelectCanceledData; set => m_SelectCanceledData = value; } [SerializeField] bool m_PlayHoverEntered; /// /// Whether to play a haptic impulse when the interactor starts hovering over an interactable. /// public bool playHoverEntered { get => m_PlayHoverEntered; set => m_PlayHoverEntered = value; } [SerializeField] HapticImpulseData m_HoverEnteredData = new HapticImpulseData { amplitude = 0.25f, duration = 0.1f, }; /// /// The haptic impulse to play when the interactor starts hovering over an interactable. /// public HapticImpulseData hoverEnteredData { get => m_HoverEnteredData; set => m_HoverEnteredData = value; } [SerializeField] bool m_PlayHoverExited; /// /// Whether to play a haptic impulse when the interactor stops hovering over an interactable without being canceled. /// public bool playHoverExited { get => m_PlayHoverExited; set => m_PlayHoverExited = value; } [SerializeField] HapticImpulseData m_HoverExitedData = new HapticImpulseData { amplitude = 0.25f, duration = 0.1f, }; /// /// The haptic impulse to play when the interactor stops hovering over an interactable without being canceled. /// public HapticImpulseData hoverExitedData { get => m_HoverExitedData; set => m_HoverExitedData = value; } [SerializeField] bool m_PlayHoverCanceled; /// /// Whether to play a haptic impulse when the interactor stops hovering over an interactable due to being canceled. /// public bool playHoverCanceled { get => m_PlayHoverCanceled; set => m_PlayHoverCanceled = value; } [SerializeField] HapticImpulseData m_HoverCanceledData = new HapticImpulseData { amplitude = 0.25f, duration = 0.1f, }; /// /// The haptic impulse to play when the interactor stops hovering over an interactable due to being canceled. /// public HapticImpulseData hoverCanceledData { get => m_HoverCanceledData; set => m_HoverCanceledData = value; } [SerializeField] bool m_AllowHoverHapticsWhileSelecting; /// /// Whether to allow hover haptics to play while the interactor is selecting an interactable. /// public bool allowHoverHapticsWhileSelecting { get => m_AllowHoverHapticsWhileSelecting; set => m_AllowHoverHapticsWhileSelecting = value; } readonly UnityObjectReferenceCache m_InteractorSource = new UnityObjectReferenceCache(); /// /// See . /// [Conditional("UNITY_EDITOR")] protected void Reset() { #if UNITY_EDITOR m_InteractorSourceObject = GetComponentInParent(true) as Object; m_HapticImpulsePlayer = GetComponentInParent(true); #endif } /// /// See . /// protected void Awake() { if (m_InteractorSourceObject == null) m_InteractorSourceObject = GetComponentInParent(true) as Object; if (m_PlaySelectEntered || m_PlaySelectExited || m_PlaySelectCanceled || m_PlayHoverEntered || m_PlayHoverExited || m_PlayHoverCanceled) { if (m_HapticImpulsePlayer == null) CreateHapticImpulsePlayer(); } } /// /// See . /// protected void OnEnable() { Subscribe(GetInteractorSource()); } /// /// See . /// protected void OnDisable() { Unsubscribe(GetInteractorSource()); } /// /// Gets the interactor this behavior should subscribe to for events. /// /// Returns the interactor this behavior should subscribe to for events. /// public IXRInteractor GetInteractorSource() { return m_InteractorSource.Get(m_InteractorSourceObject); } /// /// Sets the interactor this behavior should subscribe to for events. /// /// The interactor this behavior should subscribe to for events. /// /// This also sets the serialized field to the given interactor as a Unity Object. /// /// public void SetInteractorSource(IXRInteractor interactor) { if (Application.isPlaying && isActiveAndEnabled) Unsubscribe(m_InteractorSource.Get(m_InteractorSourceObject)); m_InteractorSource.Set(ref m_InteractorSourceObject, interactor); if (Application.isPlaying && isActiveAndEnabled) Subscribe(interactor); } /// /// Sends a haptic impulse to the referenced haptic impulse player component. /// /// The parameters of the haptic impulse. /// Returns if successful. Otherwise, returns . /// protected bool SendHapticImpulse(HapticImpulseData data) { return data != null && SendHapticImpulse(data.amplitude, data.duration, data.frequency); } /// /// Sends a haptic impulse to the referenced haptic impulse player component. /// /// The desired motor amplitude that should be within a [0-1] range. /// The desired duration of the impulse in seconds. /// The desired frequency of the impulse in Hz. A value of 0 means to use the default frequency of the device. /// Returns if successful. Otherwise, returns . /// protected bool SendHapticImpulse(float amplitude, float duration, float frequency) { if (m_HapticImpulsePlayer == null) CreateHapticImpulsePlayer(); return m_HapticImpulsePlayer.SendHapticImpulse(amplitude, duration, frequency); } void CreateHapticImpulsePlayer() { m_HapticImpulsePlayer = HapticImpulsePlayer.GetOrCreateInHierarchy(gameObject); } void Subscribe(IXRInteractor interactor) { if (interactor == null || (interactor is Object interactorObject && interactorObject == null)) return; if (interactor is IXRSelectInteractor selectInteractor) { selectInteractor.selectEntered.AddListener(OnSelectEntered); selectInteractor.selectExited.AddListener(OnSelectExited); } if (interactor is IXRHoverInteractor hoverInteractor) { hoverInteractor.hoverEntered.AddListener(OnHoverEntered); hoverInteractor.hoverExited.AddListener(OnHoverExited); } } void Unsubscribe(IXRInteractor interactor) { if (interactor == null || (interactor is Object interactorObject && interactorObject == null)) return; if (interactor is IXRSelectInteractor selectInteractor) { selectInteractor.selectEntered.RemoveListener(OnSelectEntered); selectInteractor.selectExited.RemoveListener(OnSelectExited); } if (interactor is IXRHoverInteractor hoverInteractor) { hoverInteractor.hoverEntered.RemoveListener(OnHoverEntered); hoverInteractor.hoverExited.RemoveListener(OnHoverExited); } } void OnSelectEntered(SelectEnterEventArgs args) { if (m_PlaySelectEntered) SendHapticImpulse(m_SelectEnteredData); } void OnSelectExited(SelectExitEventArgs args) { if (m_PlaySelectCanceled && args.isCanceled) SendHapticImpulse(m_SelectCanceledData); if (m_PlaySelectExited && !args.isCanceled) SendHapticImpulse(m_SelectExitedData); } void OnHoverEntered(HoverEnterEventArgs args) { if (m_PlayHoverEntered && IsHoverHapticsAllowed(args.interactorObject, args.interactableObject)) SendHapticImpulse(m_HoverEnteredData); } void OnHoverExited(HoverExitEventArgs args) { if (!IsHoverHapticsAllowed(args.interactorObject, args.interactableObject)) return; if (m_PlayHoverCanceled && args.isCanceled) SendHapticImpulse(m_HoverCanceledData); if (m_PlayHoverExited && !args.isCanceled) SendHapticImpulse(m_HoverExitedData); } bool IsHoverHapticsAllowed(IXRInteractor interactor, IXRInteractable interactable) { return m_AllowHoverHapticsWhileSelecting || !IsSelecting(interactor, interactable); } static bool IsSelecting(IXRInteractor interactor, IXRInteractable interactable) { return interactor is IXRSelectInteractor selectInteractor && interactable is IXRSelectInteractable selectable && selectInteractor.IsSelecting(selectable); } } }