// Controller Haptics|Interactions|30030 namespace VRTK { using UnityEngine; using System.Collections; using System.Collections.Generic; /// /// A collection of static methods for calling haptic functions on a given controller. /// /// /// **Script Usage:** /// > There is no requirement to add this script to a GameObject as all of the public methods are static and can be called directly e.g. `VRTK_ControllerHaptics.TriggerHapticPulse(ref, 1f)`. /// public class VRTK_ControllerHaptics : MonoBehaviour { protected static VRTK_ControllerHaptics instance; protected Dictionary hapticLoopCoroutines = new Dictionary(); /// /// The TriggerHapticPulse/2 method calls a single haptic pulse call on the controller for a single tick. /// /// The reference to the controller to activate the haptic feedback on. /// The intensity of the rumble of the controller motor. `0` to `1`. public static void TriggerHapticPulse(VRTK_ControllerReference controllerReference, float strength) { SetupInstance(); if (instance != null) { instance.InternalTriggerHapticPulse(controllerReference, strength); } } /// /// The TriggerHapticPulse/4 method calls a haptic pulse for a specified amount of time rather than just a single tick. Each pulse can be separated by providing a `pulseInterval` to pause between each haptic pulse. /// /// The reference to the controller to activate the haptic feedback on. /// The intensity of the rumble of the controller motor. `0` to `1`. /// The length of time the rumble should continue for. /// The interval to wait between each haptic pulse. public static void TriggerHapticPulse(VRTK_ControllerReference controllerReference, float strength, float duration, float pulseInterval) { SetupInstance(); if (instance != null) { instance.InternalTriggerHapticPulse(controllerReference, strength, duration, pulseInterval); } } /// /// The TriggerHapticPulse/2 method calls a haptic pulse based on a given audio clip. /// /// The reference to the controller to activate the haptic feedback on. /// The audio clip to use for the haptic pattern. public static void TriggerHapticPulse(VRTK_ControllerReference controllerReference, AudioClip clip) { SetupInstance(); if (instance != null) { instance.InternalTriggerHapticPulse(controllerReference, clip); } } /// /// The CancelHapticPulse method cancels the existing running haptic pulse on the given controller index. /// /// The reference to the controller to cancel the haptic feedback on. public static void CancelHapticPulse(VRTK_ControllerReference controllerReference) { SetupInstance(); if (instance != null) { instance.InternalCancelHapticPulse(controllerReference); } } protected virtual void OnDisable() { StopAllCoroutines(); hapticLoopCoroutines.Clear(); } protected static void SetupInstance() { if (instance == null && VRTK_SDKManager.ValidInstance()) { instance = VRTK_SDKManager.instance.gameObject.AddComponent(); } } protected virtual void InternalTriggerHapticPulse(VRTK_ControllerReference controllerReference, float strength) { InternalCancelHapticPulse(controllerReference); float hapticPulseStrength = Mathf.Clamp(strength, 0f, 1f); VRTK_SDK_Bridge.HapticPulse(controllerReference, hapticPulseStrength); } protected virtual void InternalTriggerHapticPulse(VRTK_ControllerReference controllerReference, float strength, float duration, float pulseInterval) { InternalCancelHapticPulse(controllerReference); float hapticPulseStrength = Mathf.Clamp(strength, 0f, 1f); SDK_ControllerHapticModifiers hapticModifiers = VRTK_SDK_Bridge.GetHapticModifiers(); Coroutine hapticLoop = StartCoroutine(SimpleHapticPulseRoutine(controllerReference, duration * hapticModifiers.durationModifier, hapticPulseStrength, pulseInterval * hapticModifiers.intervalModifier)); VRTK_SharedMethods.AddDictionaryValue(hapticLoopCoroutines, controllerReference, hapticLoop); } protected virtual void InternalTriggerHapticPulse(VRTK_ControllerReference controllerReference, AudioClip clip) { InternalCancelHapticPulse(controllerReference); if (!VRTK_SDK_Bridge.HapticPulse(controllerReference, clip)) { //If the SDK Bridge doesn't support audio clips then defer to a local version Coroutine hapticLoop = StartCoroutine(AudioClipHapticsRoutine(controllerReference, clip)); VRTK_SharedMethods.AddDictionaryValue(hapticLoopCoroutines, controllerReference, hapticLoop); } } protected virtual void InternalCancelHapticPulse(VRTK_ControllerReference controllerReference) { Coroutine currentHapticLoopRoutine = VRTK_SharedMethods.GetDictionaryValue(hapticLoopCoroutines, controllerReference); if (currentHapticLoopRoutine != null) { StopCoroutine(currentHapticLoopRoutine); hapticLoopCoroutines.Remove(controllerReference); } } protected virtual IEnumerator SimpleHapticPulseRoutine(VRTK_ControllerReference controllerReference, float duration, float hapticPulseStrength, float pulseInterval) { if (pulseInterval <= 0) { yield break; } while (duration > 0) { VRTK_SDK_Bridge.HapticPulse(controllerReference, hapticPulseStrength); yield return new WaitForSeconds(pulseInterval); duration -= pulseInterval; } } protected virtual IEnumerator AudioClipHapticsRoutine(VRTK_ControllerReference controllerReference, AudioClip clip) { SDK_ControllerHapticModifiers hapticModifiers = VRTK_SDK_Bridge.GetHapticModifiers(); float hapticScalar = hapticModifiers.maxHapticVibration; float[] audioData = new float[hapticModifiers.hapticsBufferSize]; int sampleOffset = -hapticModifiers.hapticsBufferSize; float startTime = Time.time; float length = clip.length / 1; float endTime = startTime + length; float sampleRate = clip.samples; while (Time.time <= endTime) { float lerpVal = (Time.time - startTime) / length; int sampleIndex = (int)(sampleRate * lerpVal); if (sampleIndex >= sampleOffset + hapticModifiers.hapticsBufferSize) { clip.GetData(audioData, sampleIndex); sampleOffset = sampleIndex; } float currentSample = Mathf.Abs(audioData[sampleIndex - sampleOffset]); ushort hapticStrength = (ushort)(hapticScalar * currentSample); VRTK_SDK_Bridge.HapticPulse(controllerReference, hapticStrength); yield return null; } } } }