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);
}
}
}