Added Feel plugin
This commit is contained in:
570
Assets/External/Feel/NiceVibrations/Scripts/Components/HapticController.cs
vendored
Normal file
570
Assets/External/Feel/NiceVibrations/Scripts/Components/HapticController.cs
vendored
Normal file
@@ -0,0 +1,570 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Timers;
|
||||
|
||||
#if (UNITY_ANDROID && !UNITY_EDITOR)
|
||||
using System.Text;
|
||||
#elif (UNITY_IOS && !UNITY_EDITOR)
|
||||
using UnityEngine.iOS;
|
||||
#endif
|
||||
|
||||
namespace Lofelt.NiceVibrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides haptic playback functionality.
|
||||
/// </summary>
|
||||
///
|
||||
/// HapticController allows you to load and play <c>.haptic</c> clips, and
|
||||
/// provides various ways to control playback, such as seeking, looping and
|
||||
/// amplitude/frequency modulation.
|
||||
///
|
||||
/// If you need a <c>MonoBehaviour</c> API, use HapticSource and
|
||||
/// HapticReceiver instead.
|
||||
///
|
||||
/// On iOS and Android, the device is vibrated, using <c>LofeltHaptics</c>.
|
||||
/// On any platform, when a gamepad is connected, that gamepad is vibrated,
|
||||
/// using GamepadRumbler.
|
||||
///
|
||||
/// Gamepads are vibrated automatically when HapticController detects that a
|
||||
/// gamepad is connected, no special code is needed to support gamepads.
|
||||
/// Gamepads only support Load(), Play(), Stop(), \ref clipLevel and \ref
|
||||
/// outputLevel. Other features like Seek(), Loop() and \ref clipFrequencyShift
|
||||
/// will have no effect on gamepads.
|
||||
///
|
||||
/// None of the methods here are thread-safe and should only be called from
|
||||
/// the main (Unity) thread. Calling these methods from a secondary thread can
|
||||
/// cause undefined behaviour and memory leaks.
|
||||
public static class HapticController
|
||||
{
|
||||
static bool lofeltHapticsInitalized = false;
|
||||
|
||||
// Timer used to call HandleFinishedPlayback() when playback is complete
|
||||
static Timer playbackFinishedTimer = new Timer();
|
||||
|
||||
// Duration of the loaded haptic clip, in seconds
|
||||
static float clipLoadedDurationSecs = 0.0f;
|
||||
|
||||
// Whether Load() has been called before
|
||||
static bool clipLoaded = false;
|
||||
|
||||
// The value of the last call to seek()
|
||||
static float lastSeekTime = 0.0f;
|
||||
|
||||
// Flag indicating if the device supports playing back .haptic clips
|
||||
static bool deviceMeetsAdvancedRequirements = false;
|
||||
|
||||
// Flag indicating if the user enabled playback looping.
|
||||
// This does not necessarily mean that the currently active playback is looping, for
|
||||
// example gamepads don't support looping.
|
||||
static bool isLoopingEnabledByUser = false;
|
||||
|
||||
// Flag indicating if the currently active playback is looping
|
||||
static bool isPlaybackLooping = false;
|
||||
|
||||
static HapticPatterns.PresetType _fallbackPreset = HapticPatterns.PresetType.None;
|
||||
|
||||
/// <summary>
|
||||
/// The haptic preset to be played when it's not possible to play a haptic clip
|
||||
/// </summary>
|
||||
public static HapticPatterns.PresetType fallbackPreset
|
||||
{
|
||||
get { return _fallbackPreset; }
|
||||
set { _fallbackPreset = value; }
|
||||
}
|
||||
|
||||
internal static bool _hapticsEnabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// Property to enable and disable global haptic playback
|
||||
/// </summary>
|
||||
public static bool hapticsEnabled
|
||||
{
|
||||
get { return _hapticsEnabled; }
|
||||
set
|
||||
{
|
||||
if (_hapticsEnabled)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
_hapticsEnabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal static float _outputLevel = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The overall haptic output level
|
||||
/// </summary>
|
||||
///
|
||||
/// It can be interpreted as the "volume control" for haptic playback.
|
||||
/// Output level is applied in combination with \ref clipLevel 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. \ref outputLevel is best used
|
||||
/// to increase or decrease the overall haptic level in a game.
|
||||
///
|
||||
/// As output level pertains to all clips, unlike \ref clipLevel, it persists when a new clip is loaded.
|
||||
///
|
||||
/// \ref outputLevel is a multiplication factor, it is <i>not</i> a dB value. The factor needs to be
|
||||
/// 0 or greater.
|
||||
///
|
||||
/// The combination of \ref outputLevel and \ref clipLevel can result in a gain (for factors
|
||||
/// greater than 1.0) or an attenuation (for factors less than 1.0) to the clip. If the
|
||||
/// combination of \ref outputLevel, \ref clipLevel and the amplitude within the loaded haptic
|
||||
/// is greater than 1.0, it is clipped to 1.0. Hard clipping is performed, no limiter is used.
|
||||
///
|
||||
/// On Android, an adjustment to \ref outputLevel will take effect in the next call to Play().
|
||||
/// On iOS, it will take effect right away.
|
||||
[System.ComponentModel.DefaultValue(1.0f)]
|
||||
public static float outputLevel
|
||||
{
|
||||
get { return _outputLevel; }
|
||||
set
|
||||
{
|
||||
_outputLevel = value;
|
||||
|
||||
ApplyLevelsToLofeltHaptics();
|
||||
ApplyLevelsToGamepadRumbler();
|
||||
}
|
||||
}
|
||||
|
||||
internal static float _clipLevel = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The level of the loaded clip
|
||||
/// </summary>
|
||||
///
|
||||
/// Clip level is applied in combination with \ref outputLevel, 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.
|
||||
/// \ref clipLevel is best used to adjust the level of a single clip based on game state.
|
||||
///
|
||||
/// As clip level is specific to an individual clip, unlike \ref outputLevel, it resets to
|
||||
/// 1.0 when a new clip is loaded.
|
||||
///
|
||||
/// \ref clipLevel is a multiplication factor, it is <i>not</i> a dB value. The factor needs to be
|
||||
/// 0 or greater.
|
||||
///
|
||||
/// The combination of \ref outputLevel and \ref clipLevel can result in a gain (for factors
|
||||
/// greater than 1.0) or an attenuation (for factors less than 1.0) to the clip.
|
||||
///
|
||||
/// If the combination of \ref outputLevel, \ref clipLevel and the amplitude within the loaded
|
||||
/// haptic is greater than 1.0, it is clipped to 1.0. Hard clipping is performed, no limiter is used.
|
||||
///
|
||||
/// The clip needs to be loaded with Load() before adjusting \ref clipLevel. Loading a clip
|
||||
/// resets \ref clipLevel back to the default of 1.0.
|
||||
///
|
||||
/// On Android, an adjustment to \ref clipLevel will take effect in the next call to Play(). On iOS,
|
||||
/// it will take effect right away.
|
||||
///
|
||||
/// On Android, setting the clip level should be done before calling \ref Seek(), since
|
||||
/// setting a clip level ignores the sought value.
|
||||
///
|
||||
[System.ComponentModel.DefaultValue(1.0f)]
|
||||
public static float clipLevel
|
||||
{
|
||||
get { return _clipLevel; }
|
||||
set
|
||||
{
|
||||
_clipLevel = value;
|
||||
|
||||
ApplyLevelsToLofeltHaptics();
|
||||
ApplyLevelsToGamepadRumbler();
|
||||
}
|
||||
}
|
||||
|
||||
/// Action that is invoked when Load() is called
|
||||
public static Action LoadedClipChanged;
|
||||
|
||||
/// Action that is invoked when Play() is called
|
||||
public static Action PlaybackStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Action that is invoked when the playback has finished
|
||||
/// </summary>
|
||||
///
|
||||
/// This happens either when Stop() is explicitly called, or when a non-looping
|
||||
/// clip has finished playing.
|
||||
///
|
||||
/// This can be invoked spuriously, even if no haptics are currently playing, for example
|
||||
/// if Stop() is called multiple times in a row.
|
||||
public static Action PlaybackStopped;
|
||||
|
||||
// Applies the current clip level and output level as the amplitude multiplication to
|
||||
// LofeltHaptics
|
||||
private static void ApplyLevelsToLofeltHaptics()
|
||||
{
|
||||
if (Init())
|
||||
{
|
||||
LofeltHaptics.SetAmplitudeMultiplication(_outputLevel * _clipLevel);
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the current clip level and output level as the motor speed multiplication to
|
||||
// GamepadRumbler
|
||||
private static void ApplyLevelsToGamepadRumbler()
|
||||
{
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
GamepadRumbler.SetMotorSpeedMultiplication(_outputLevel * _clipLevel, _outputLevel * _clipLevel);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes HapticController.
|
||||
/// </summary>
|
||||
///
|
||||
/// Calling this method multiple times has no effect and is safe.
|
||||
///
|
||||
/// You do not need to call this method, HapticController automatically calls this
|
||||
/// method before any operation that needs initialization, such as Play().
|
||||
/// However it can be beneficial to call this early during startup, so the initialization
|
||||
/// time is spent at startup instead of when the first haptic is triggered during gameplay.
|
||||
/// If you have a HapticReceiver in your scene, it takes care of calling
|
||||
/// Init() during startup for you.
|
||||
///
|
||||
/// Do not call this method from a static constructor. Unity often invokes static
|
||||
/// constructors from a different thread, for example during deserialization. The
|
||||
/// initialization code is not thread-safe. This is the reason this method is not called
|
||||
/// from the static constructor of HapticController or HapticReceiver.
|
||||
///
|
||||
/// <returns>Whether the device supports the minimum requirements to play haptics</returns>
|
||||
public static bool Init()
|
||||
{
|
||||
if (!lofeltHapticsInitalized)
|
||||
{
|
||||
lofeltHapticsInitalized = true;
|
||||
|
||||
var syncContext = System.Threading.SynchronizationContext.Current;
|
||||
playbackFinishedTimer.Elapsed += (object obj, System.Timers.ElapsedEventArgs args) =>
|
||||
{
|
||||
// Timer elapsed events are called from a separate thread, so use
|
||||
// SynchronizationContext to handle it in the main thread.
|
||||
syncContext.Post(_ =>
|
||||
{
|
||||
HandleFinishedPlayback();
|
||||
}, null);
|
||||
};
|
||||
|
||||
if (DeviceCapabilities.isVersionSupported)
|
||||
{
|
||||
LofeltHaptics.Initialize();
|
||||
DeviceCapabilities.Init();
|
||||
deviceMeetsAdvancedRequirements = DeviceCapabilities.meetsAdvancedRequirements;
|
||||
}
|
||||
|
||||
GamepadRumbler.Init();
|
||||
}
|
||||
return deviceMeetsAdvancedRequirements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a haptic clip given in JSON format for later playback.
|
||||
/// </summary>
|
||||
///
|
||||
/// This overload of Load() is useful in cases there is only the JSON data of a haptic clip
|
||||
/// available. Due to only having the JSON data and no GamepadRumble, gamepad playback is
|
||||
/// not supported with this overload.
|
||||
///
|
||||
/// <param name="data">The haptic clip, which is the content of the
|
||||
/// <c>.haptic</c> file, a UTF-8 encoded JSON string without a null
|
||||
/// terminator</param>
|
||||
public static void Load(byte[] data)
|
||||
{
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
GamepadRumbler.Unload(GamepadRumbler.GetCurrentGamepadID());
|
||||
#endif
|
||||
lastSeekTime = 0.0f;
|
||||
clipLoaded = true;
|
||||
clipLoadedDurationSecs = 0.0f;
|
||||
if (Init())
|
||||
{
|
||||
LofeltHaptics.Load(data);
|
||||
}
|
||||
clipLevel = 1.0f;
|
||||
LoadedClipChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the given HapticClip for later playback.
|
||||
/// </summary>
|
||||
///
|
||||
/// This is the standard way to load a haptic clip, while the other overloads of Load()
|
||||
/// are for more specialized cases.
|
||||
///
|
||||
/// At the moment only one clip can be loaded at a time.
|
||||
///
|
||||
/// <param name="clip">The HapticClip to be loaded</param>
|
||||
public static void Load(HapticClip clip)
|
||||
{
|
||||
Load(clip.json, clip.gamepadRumble);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the haptic clip given as JSON and GamepadRumble for later playback.
|
||||
/// </summary>
|
||||
///
|
||||
/// This is an overload of Load() that is useful when a HapticClip is not available, and
|
||||
/// both the JSON and GamepadRumble are. One such case is generating both dynamically at
|
||||
/// runtime.
|
||||
///
|
||||
/// <param name="json">The haptic clip, which is the content of the <c>.haptic</c> file,
|
||||
/// a UTF-8 encoded JSON string without a null terminator</param>
|
||||
/// <param name="rumble">The GamepadRumble representation of the haptic clip</param>
|
||||
public static void Load(byte[] json, GamepadRumble rumble)
|
||||
{
|
||||
Load(json);
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
GamepadRumbler.Load(rumble, GamepadRumbler.GetCurrentGamepadID());
|
||||
#endif
|
||||
// GamepadRumbler.Load() resets the motor speed multiplication to 1.0, so the levels
|
||||
// need to be applied here again
|
||||
ApplyLevelsToGamepadRumbler();
|
||||
|
||||
// Load() only sets the correct clip duration on iOS and Android, and sets it to 0.0
|
||||
// on other platforms. For the other platforms, set a clip duration based on the
|
||||
// GamepadRumble here.
|
||||
if (clipLoadedDurationSecs == 0.0f && rumble.IsValid())
|
||||
{
|
||||
clipLoadedDurationSecs = rumble.totalDurationMs / 1000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
static void HandleFinishedPlayback()
|
||||
{
|
||||
lastSeekTime = 0.0f;
|
||||
isPlaybackLooping = false;
|
||||
playbackFinishedTimer.Enabled = false;
|
||||
PlaybackStopped?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the haptic clip that was previously loaded with Load().
|
||||
/// </summary>
|
||||
///
|
||||
/// If <c>Loop(true)</c> was called previously, the playback will be repeated
|
||||
/// until Stop() is called. Otherwise the haptic clip will only play once.
|
||||
///
|
||||
/// In case the device does not meet the requirements to play <c>.haptic</c> clips, this
|
||||
/// function will call HapticPatterns.PlayPreset() with the \ref fallbackPreset set. In this
|
||||
/// case, functionality like seeking, looping and runtime modulation won't do anything as
|
||||
/// they aren't available for haptic presets.
|
||||
public static void Play()
|
||||
{
|
||||
if (!_hapticsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float remainingPlayDuration = 0.0f;
|
||||
bool canLoop = false;
|
||||
if (GamepadRumbler.CanPlay())
|
||||
{
|
||||
remainingPlayDuration = clipLoadedDurationSecs;
|
||||
GamepadRumbler.Play();
|
||||
}
|
||||
else if (Init())
|
||||
{
|
||||
remainingPlayDuration = Mathf.Max(clipLoadedDurationSecs - lastSeekTime, 0.0f);
|
||||
canLoop = DeviceCapabilities.canLoop;
|
||||
LofeltHaptics.Play();
|
||||
}
|
||||
else if (DeviceCapabilities.isVersionSupported)
|
||||
{
|
||||
remainingPlayDuration = HapticPatterns.GetPresetDuration(fallbackPreset);
|
||||
HapticPatterns.PlayPreset(fallbackPreset);
|
||||
}
|
||||
|
||||
isPlaybackLooping = isLoopingEnabledByUser && canLoop;
|
||||
PlaybackStarted?.Invoke();
|
||||
|
||||
//
|
||||
// Call HandleFinishedPlayback() after the playback finishes
|
||||
//
|
||||
if (remainingPlayDuration > 0.0f)
|
||||
{
|
||||
playbackFinishedTimer.Interval = remainingPlayDuration * 1000;
|
||||
playbackFinishedTimer.AutoReset = false;
|
||||
playbackFinishedTimer.Enabled = !isPlaybackLooping;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Setting playbackFinishedTimer.Interval needs an interval > 0, otherwise it will
|
||||
// throw an exception.
|
||||
// Even if the remaining play duration is 0, we still want to trigger everything
|
||||
// that happens in HandleFinishedPlayback().
|
||||
// A playback duration of 0 happens in the Unity editor, when loading the clip
|
||||
// failed or when seeking to the end of a clip.
|
||||
HandleFinishedPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Loads and plays the HapticClip given as an argument.
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="clip">The HapticClip to be played</param>
|
||||
public static void Play(HapticClip clip)
|
||||
{
|
||||
Load(clip);
|
||||
Play();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops haptic playback
|
||||
///
|
||||
/// </summary>
|
||||
public static void Stop()
|
||||
{
|
||||
|
||||
if (Init())
|
||||
{
|
||||
LofeltHaptics.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
LofeltHaptics.StopPattern();
|
||||
}
|
||||
GamepadRumbler.Stop();
|
||||
HandleFinishedPlayback();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jumps to a time position in the haptic clip.
|
||||
/// </summary>
|
||||
///
|
||||
/// The playback will always be stopped when this function is called.
|
||||
/// This is to match the behavior between iOS and Android, since Android needs to
|
||||
/// restart playback for seek to have effect.
|
||||
///
|
||||
/// If seeking beyond the end of the clip, Play() will not reproduce any haptics.
|
||||
/// Seeking to a negative position will seek to the beginning of the clip.
|
||||
///
|
||||
/// <param name="time">The new position within the clip, as seconds from the beginning
|
||||
/// of the clip</param>
|
||||
public static void Seek(float time)
|
||||
{
|
||||
if (Init())
|
||||
{
|
||||
LofeltHaptics.Stop();
|
||||
LofeltHaptics.Seek(time);
|
||||
}
|
||||
GamepadRumbler.Stop();
|
||||
lastSeekTime = time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given shift to the frequency of every breakpoint in the clip, including the
|
||||
/// emphasis.
|
||||
/// </summary>
|
||||
///
|
||||
/// In other words, this property shifts all frequencies of the clip. The frequency shift is
|
||||
/// added to each frequency value and needs to be between -1.0 and 1.0. If the resulting
|
||||
/// frequency of a breakpoint is smaller than 0.0 or greater than 1.0, it is clipped to that
|
||||
/// range. The frequency is clipped hard, no limiter is used.
|
||||
///
|
||||
/// The clip needs to be loaded with Load() first. Loading a clip resets the shift back
|
||||
/// to the default of 0.0.
|
||||
///
|
||||
/// Setting the frequency shift has no effect on Android; it only works on iOS.
|
||||
///
|
||||
/// A call to this property will change the frequency shift of a currently playing clip
|
||||
/// right away. If no clip is playing, the shift is applied in the next call to
|
||||
/// Play().
|
||||
[System.ComponentModel.DefaultValue(0.0f)]
|
||||
public static float clipFrequencyShift
|
||||
{
|
||||
set
|
||||
{
|
||||
if (Init())
|
||||
{
|
||||
LofeltHaptics.SetFrequencyShift(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the playback of a haptic clip to loop.
|
||||
/// </summary>
|
||||
///
|
||||
/// On Android, calling this will always put the playback position at the start of the clip.
|
||||
/// Also, it will only have an effect when Play() is called again.
|
||||
///
|
||||
/// On iOS, if a clip is already playing, calling this will leave the playback position as
|
||||
/// it is and repeat when it reaches the end. No need to call Play() again for
|
||||
/// changes to take effect.
|
||||
///
|
||||
/// <param name="enabled">If the value is <c>true</c>, looping will be enabled which results
|
||||
/// in repeating the playback until Stop() is called; if <c>false</c>, the haptic
|
||||
/// clip will only be played once.</param>
|
||||
public static void Loop(bool enabled)
|
||||
{
|
||||
if (Init())
|
||||
{
|
||||
LofeltHaptics.Loop(enabled);
|
||||
}
|
||||
isLoopingEnabledByUser = enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the loaded haptic clip is playing.
|
||||
/// </summary>
|
||||
///
|
||||
/// <returns>Whether the loaded clip is playing</returns>
|
||||
public static bool IsPlaying()
|
||||
{
|
||||
if (playbackFinishedTimer.Enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return isPlaybackLooping;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops playback and resets the playback state.
|
||||
/// </summary>
|
||||
///
|
||||
/// Seek position, clip level, clip frequency shift and loop are reset to the
|
||||
/// default values.
|
||||
/// The currently loaded clip stays loaded.
|
||||
/// \ref hapticsEnabled and \ref outputLevel are not reset.
|
||||
public static void Reset()
|
||||
{
|
||||
if (clipLoaded)
|
||||
{
|
||||
Seek(0.0f);
|
||||
Stop();
|
||||
clipLevel = 1.0f;
|
||||
clipFrequencyShift = 0.0f;
|
||||
Loop(false);
|
||||
}
|
||||
fallbackPreset = HapticPatterns.PresetType.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes an application focus change event.
|
||||
/// </summary>
|
||||
///
|
||||
/// If you have a HapticReceiver in your scene, the HapticReceiver
|
||||
/// will take care of calling this method when needed. Otherwise it is your
|
||||
/// responsibility to do so.
|
||||
///
|
||||
/// When the application loses the focus, playback is stopped.
|
||||
///
|
||||
/// <param name="hasFocus">Whether the application now has focus</param>
|
||||
public static void ProcessApplicationFocus(bool hasFocus)
|
||||
{
|
||||
if (!hasFocus)
|
||||
{
|
||||
// While LofeltHaptics stops playback when the app loses focus,
|
||||
// calling Stop() here handles additional things such as invoking
|
||||
// the PlaybackStopped Action.
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user