// Copyright (c) Meta Platforms, Inc. and affiliates. using UnityEngine; namespace Lofelt.NiceVibrations { /// /// Provides haptic playback functionality for a single haptic clip. /// /// /// HapticSource plays back the HapticClip assigned in the \ref clip property /// when calling Play(). It also provides various ways to control playback, such as /// seeking, looping and amplitude/frequency modulation. /// /// When a gamepad is connected, the haptic clip will be played back on that gamepad. /// See the HapticController documentation for more details about gamepad support. /// /// At the moment, playback of a haptic source is not triggered automatically /// by e.g. proximity between the HapticReceiver and the HapticSource, /// so you need to call Play() to trigger playback. /// /// You can place multiple HapticSource components in your scene, with a different /// HapticClip assigned to each. /// /// HapticSource provides a per-clip MonoBehaviour API for the functionality /// in HapticController, while HapticReceiver provides a MonoBehaviour API /// for the global functionality in HapticController. /// /// HapticSourceInspector provides a custom editor for HapticSource for the /// Inspector. [AddComponentMenu("Nice Vibrations/Haptic Source")] public class HapticSource : MonoBehaviour { const int DEFAULT_PRIORITY = 128; /// The HapticClip this HapticSource loads and plays. public HapticClip clip; /// /// The priority of the HapticSource /// /// /// This property is set by HapticSourceInspector. 0 is the highest priority and 256 /// is the lowest priority. /// /// The default value is 128. public int priority = DEFAULT_PRIORITY; /// /// Jump in time position of haptic source playback. /// /// /// Initially set to 0.0 seconds. /// This value can only be set when using Seek(). float seekTime = 0.0f; [SerializeField] HapticPatterns.PresetType _fallbackPreset = HapticPatterns.PresetType.None; /// /// The haptic preset to be played when it's not possible to play a haptic clip /// [System.ComponentModel.DefaultValue(HapticPatterns.PresetType.None)] public HapticPatterns.PresetType fallbackPreset { get { return _fallbackPreset; } set { _fallbackPreset = value; } } [SerializeField] bool _loop = false; /// /// Set the haptic source to loop playback of the haptic clip. /// /// /// It will only have any effect once Play() is called. /// /// See HapticController::Loop() for further details. [System.ComponentModel.DefaultValue(false)] public bool loop { get { return _loop; } set { _loop = value; } } [SerializeField] float _level = 1.0f; /// /// The level of the haptic source /// /// /// Haptic source level is applied in combination with output level (which can be set on either /// HapticReceiver or HapticController according to preference), to the currently playing /// haptic clip. The combination of these two levels and the amplitude within the loaded /// haptic at a given moment in time determines the strength of the vibration felt on the device. See /// HapticController::clipLevel for further details. [System.ComponentModel.DefaultValue(1.0)] public float level { get { return _level; } set { _level = value; if (IsLoaded()) { HapticController.clipLevel = _level; } } } [SerializeField] float _frequencyShift = 0.0f; /// /// This shift is added to the frequency of every breakpoint in the clip, including the /// emphasis. /// /// /// See HapticController::clipFrequencyShift for further details. [System.ComponentModel.DefaultValue(0.0)] public float frequencyShift { get { return _frequencyShift; } set { _frequencyShift = value; if (IsLoaded()) { HapticController.clipFrequencyShift = _frequencyShift; } } } /// The HapticSource that is currently loaded into HapticController. /// This can be null if nothing was ever loaded, or if HapticController::Load() /// was called directly, bypassing HapticSource. static HapticSource loadedHapticSource = null; /// The HapticSource that was last played. /// This can be null if nothing was ever player, or if HapticController::Play() /// was called directly, bypassing HapticSource. /// The lastPlayedHapticSource isn't necessarily playing now, lastPlayedHapticSource /// will remain set even if playback has finished or was stopped. static HapticSource lastPlayedHapticSource = null; static HapticSource() { // When HapticController::Load() or HapticController::Play() is // called directly, bypassing HapticSource, reset loadedHapticSource // and lastPlayedHapticSource. HapticController.LoadedClipChanged += () => { loadedHapticSource = null; }; HapticController.PlaybackStarted += () => { lastPlayedHapticSource = null; }; } /// /// Loads and plays back the haptic clip. /// /// /// At the moment only one haptic clip at a time can be played. If another /// HapticSource is currently playing and has lower priority, its playback will /// be stopped. /// /// If a seek time within the time range of the clip has been set with Seek(), /// it will jump to that position if \ref loop is false. If \ref loop /// is true, seeking will have no effect. /// /// It will loop playback in case \ref loop is true. public void Play() { if (CanPlay()) { // // Load // HapticController.Load(clip); loadedHapticSource = this; // // Apply properties like loop, modulation and seek position // HapticController.Loop(loop); HapticController.clipLevel = level; HapticController.clipFrequencyShift = frequencyShift; if (seekTime != 0.0f && !loop) { HapticController.Seek(seekTime); } // // Play // HapticController.fallbackPreset = fallbackPreset; HapticController.Play(); lastPlayedHapticSource = this; } } private bool CanPlay() { return (!HapticController.IsPlaying() || (lastPlayedHapticSource != null && priority <= lastPlayedHapticSource.priority)); } /// /// Checks if the current HapticSource has been loaded into HapticController. /// /// /// This is used to avoid triggering operations on HapticController while /// another HapticSource is loaded. private bool IsLoaded() { return Object.ReferenceEquals(this, loadedHapticSource); } /// /// Stops playback that was previously started with Play(). /// public void Stop() { if (IsLoaded()) { HapticController.Stop(); } } /// /// Sets the time position to jump to when Play() is called. /// /// /// It will only have an effect once Play() is called. /// /// The position in the clip, in seconds public void Seek(float time) { this.seekTime = time; } /// /// When a GameObject is disabled, stop playback if this HapticSource is /// playing. /// public void OnDisable() { if (HapticController.IsPlaying() && IsLoaded()) { this.Stop(); } } } }