Added Feel plugin
This commit is contained in:
514
Assets/External/Feel/NiceVibrations/Scripts/Components/HapticPatterns.cs
vendored
Normal file
514
Assets/External/Feel/NiceVibrations/Scripts/Components/HapticPatterns.cs
vendored
Normal file
@@ -0,0 +1,514 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Lofelt.NiceVibrations
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of methods to play simple haptic patterns.
|
||||
/// </summary>
|
||||
///
|
||||
/// Each of the methods here load and play a simple haptic clip or a
|
||||
/// haptic pattern, depending on the device capabilities.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// After playback has finished, the loaded clips in this class will remain
|
||||
/// loaded in HapticController.
|
||||
|
||||
public static class HapticPatterns
|
||||
{
|
||||
static String emphasisTemplate;
|
||||
static String constantTemplate;
|
||||
static NumberFormatInfo numberFormat;
|
||||
static private float[] constantPatternTime = new float[] { 0.0f, 0.0f };
|
||||
|
||||
/// <summary>
|
||||
/// Enum that represents all the types of haptic presets available
|
||||
/// </summary>
|
||||
public enum PresetType
|
||||
{
|
||||
Selection = 0,
|
||||
Success = 1,
|
||||
Warning = 2,
|
||||
Failure = 3,
|
||||
LightImpact = 4,
|
||||
MediumImpact = 5,
|
||||
HeavyImpact = 6,
|
||||
RigidImpact = 7,
|
||||
SoftImpact = 8,
|
||||
None = -1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structure that represents a haptic pattern with amplitude variations.
|
||||
/// </summary>
|
||||
///
|
||||
/// \ref time values have be incremental to be compatible with Preset.
|
||||
struct Pattern
|
||||
{
|
||||
public float[] time;
|
||||
public float[] amplitude;
|
||||
|
||||
static String clipJsonTemplate;
|
||||
|
||||
static Pattern()
|
||||
{
|
||||
clipJsonTemplate = (Resources.Load("nv-pattern-template") as TextAsset).text;
|
||||
}
|
||||
|
||||
public Pattern(float[] time, float[] amplitude)
|
||||
{
|
||||
this.time = time;
|
||||
this.amplitude = amplitude;
|
||||
}
|
||||
|
||||
// Converts a Pattern to a GamepadRumble
|
||||
//
|
||||
// Each pair of adjacent entries in the Pattern create one entry in the GamepadRumble.
|
||||
public GamepadRumble ToRumble()
|
||||
{
|
||||
GamepadRumble result = new GamepadRumble();
|
||||
if (time.Length <= 1)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
Debug.Assert(time.Length == amplitude.Length);
|
||||
|
||||
// The first pattern entry needs to have a time of 0.0 for the algorithm below to work
|
||||
Debug.Assert(time[0] == 0.0f);
|
||||
|
||||
int rumbleCount = time.Length - 1;
|
||||
result.durationsMs = new int[rumbleCount];
|
||||
result.lowFrequencyMotorSpeeds = new float[rumbleCount];
|
||||
result.highFrequencyMotorSpeeds = new float[rumbleCount];
|
||||
result.totalDurationMs = 0;
|
||||
for (int rumbleIndex = 0; rumbleIndex < rumbleCount; rumbleIndex++)
|
||||
{
|
||||
int patternDurationMs = (int)((time[rumbleIndex + 1] - time[rumbleIndex]) * 1000.0f);
|
||||
result.durationsMs[rumbleIndex] = patternDurationMs;
|
||||
result.lowFrequencyMotorSpeeds[rumbleIndex] = amplitude[rumbleIndex];
|
||||
result.highFrequencyMotorSpeeds[rumbleIndex] = amplitude[rumbleIndex];
|
||||
result.totalDurationMs += result.durationsMs[rumbleIndex];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Converts a Pattern to a haptic clip JSON string.
|
||||
public String ToClip()
|
||||
{
|
||||
if (clipJsonTemplate == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
String amplitudeEnvelope = "";
|
||||
for (int i = 0; i < time.Length; i++)
|
||||
{
|
||||
float clampedAmplitude = Mathf.Clamp(amplitude[i], 0.0f, 1.0f);
|
||||
amplitudeEnvelope += "{ \"time\":" + time[i].ToString(numberFormat) + "," +
|
||||
"\"amplitude\":" + clampedAmplitude.ToString(numberFormat) + "}";
|
||||
|
||||
// Don't add a comma to the JSON data if we're at the end of the envelope
|
||||
if (i + 1 < time.Length)
|
||||
{
|
||||
amplitudeEnvelope += ",";
|
||||
}
|
||||
}
|
||||
|
||||
return clipJsonTemplate.Replace("{amplitude-envelope}", amplitudeEnvelope);
|
||||
}
|
||||
}
|
||||
|
||||
// A haptic preset in its different representations
|
||||
//
|
||||
// A Preset has four different representations, as there are four different playback methods.
|
||||
// Each representation is created at construction time, so that playing a
|
||||
// Preset has no further conversion cost at playback time.
|
||||
internal struct Preset
|
||||
{
|
||||
// For playback on iOS, using system haptics
|
||||
public PresetType type;
|
||||
|
||||
// For playback on Android devices without amplitude control
|
||||
public float[] maximumAmplitudePattern;
|
||||
|
||||
// For playback on Android devices with amplitude control
|
||||
public byte[] jsonClip;
|
||||
|
||||
// For playback on gamepads
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
public GamepadRumble gamepadRumble;
|
||||
#endif
|
||||
|
||||
public Preset(PresetType type, float[] time, float[] amplitude)
|
||||
{
|
||||
Debug.Assert(type != PresetType.None);
|
||||
Pattern pattern = new Pattern(time, amplitude);
|
||||
this.type = type;
|
||||
this.maximumAmplitudePattern = pattern.time;
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
this.gamepadRumble = pattern.ToRumble();
|
||||
#endif
|
||||
this.jsonClip = System.Text.Encoding.UTF8.GetBytes(pattern.ToClip());
|
||||
}
|
||||
|
||||
public float GetDuration()
|
||||
{
|
||||
if (maximumAmplitudePattern.Length > 0)
|
||||
{
|
||||
return maximumAmplitudePattern[maximumAmplitudePattern.Length - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Selection" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Selection;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Light" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Light;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Medium" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Medium;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Heavy" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Heavy;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Rigid" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Rigid;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Soft" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Soft;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Success" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Success;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Failure" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Failure;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined Preset that represents a "Warning" haptic preset
|
||||
/// </summary>
|
||||
internal static Preset Warning;
|
||||
|
||||
static HapticPatterns()
|
||||
{
|
||||
emphasisTemplate = (Resources.Load("nv-emphasis-template") as TextAsset).text;
|
||||
constantTemplate = (Resources.Load("nv-constant-template") as TextAsset).text;
|
||||
|
||||
numberFormat = new NumberFormatInfo();
|
||||
numberFormat.NumberDecimalSeparator = ".";
|
||||
|
||||
// Initialize presets after setting the number format, so that the correct decimal
|
||||
// separator is used when building the JSON representation.
|
||||
|
||||
Selection = new Preset(PresetType.Selection, new float[] { 0.0f, 0.04f },
|
||||
new float[] { 0.471f, 0.471f });
|
||||
|
||||
Light = new Preset(PresetType.LightImpact, new float[] { 0.000f, 0.040f },
|
||||
new float[] { 0.156f, 0.156f });
|
||||
|
||||
Medium = new Preset(PresetType.MediumImpact, new float[] { 0.000f, 0.080f },
|
||||
new float[] { 0.471f, 0.471f });
|
||||
|
||||
Heavy = new Preset(PresetType.HeavyImpact, new float[] { 0.0f, 0.16f },
|
||||
new float[] { 1.0f, 1.00f });
|
||||
|
||||
Rigid = new Preset(PresetType.RigidImpact, new float[] { 0.0f, 0.04f },
|
||||
new float[] { 1.0f, 1.00f });
|
||||
|
||||
Soft = new Preset(PresetType.SoftImpact, new float[] { 0.000f, 0.160f },
|
||||
new float[] { 0.156f, 0.156f });
|
||||
|
||||
Success = new Preset(PresetType.Success, new float[] { 0.0f, 0.040f, 0.080f, 0.240f },
|
||||
new float[] { 0.0f, 0.157f, 0.000f, 1.000f });
|
||||
|
||||
Failure = new Preset(PresetType.Failure,
|
||||
new float[] { 0.0f, 0.080f, 0.120f, 0.200f, 0.240f, 0.400f, 0.440f, 0.480f },
|
||||
new float[] { 0.0f, 0.470f, 0.000f, 0.470f, 0.000f, 1.000f, 0.000f, 0.157f });
|
||||
|
||||
Warning = new Preset(PresetType.Warning, new float[] { 0.0f, 0.120f, 0.240f, 0.280f },
|
||||
new float[] { 0.0f, 1.000f, 0.000f, 0.470f });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a single emphasis point.
|
||||
/// </summary>
|
||||
///
|
||||
/// Plays a haptic clip that consists only of one breakpoint with emphasis.
|
||||
/// On iOS, this translates to a transient, and on Android and gamepads to
|
||||
/// a quick vibration.
|
||||
///
|
||||
/// <param name="amplitude">The amplitude of the emphasis, from 0.0 to 1.0</param>
|
||||
/// <param name="frequency">The frequency of the emphasis, from 0.0 to 1.0</param>
|
||||
public static void PlayEmphasis(float amplitude, float frequency)
|
||||
{
|
||||
if (emphasisTemplate == null || !HapticController.hapticsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use HapticController.Play() to play a .haptic clip on mobile devices
|
||||
// that support it, or to play a gamepad rumble if a gamepad is connected.
|
||||
if (HapticController.Init() || GamepadRumbler.IsConnected())
|
||||
{
|
||||
float clampedAmplitude = Mathf.Clamp(amplitude, 0.0f, 1.0f);
|
||||
float clampedFrequency = Mathf.Clamp(frequency, 0.0f, 1.0f);
|
||||
const float duration = 0.1f;
|
||||
|
||||
String json = emphasisTemplate
|
||||
.Replace("{amplitude}", clampedAmplitude.ToString(numberFormat))
|
||||
.Replace("{frequency}", clampedFrequency.ToString(numberFormat))
|
||||
.Replace("{duration}", duration.ToString(numberFormat));
|
||||
|
||||
// This preprocessor section will only run for non-mobile platforms
|
||||
GamepadRumble rumble = new GamepadRumble();
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
rumble.durationsMs = new int[] { (int)(duration * 1000) };
|
||||
rumble.lowFrequencyMotorSpeeds = new float[] { clampedAmplitude };
|
||||
rumble.highFrequencyMotorSpeeds = new float[] { clampedFrequency };
|
||||
#endif
|
||||
|
||||
HapticController.Load(System.Text.Encoding.UTF8.GetBytes(json), rumble);
|
||||
HapticController.Loop(false);
|
||||
HapticController.Play();
|
||||
}
|
||||
|
||||
// As a fallback, play a short buzz on Android, or a preset on iOS.
|
||||
else if (DeviceCapabilities.isVersionSupported)
|
||||
{
|
||||
#if (UNITY_ANDROID && !UNITY_EDITOR)
|
||||
LofeltHaptics.PlayMaximumAmplitudePattern(new float[]{ 0.0f, 0.05f });
|
||||
#elif (UNITY_IOS && !UNITY_EDITOR)
|
||||
PresetType preset = presetTypeForEmphasis(amplitude);
|
||||
LofeltHaptics.TriggerPresetHaptics((int)preset);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically selects the fallback preset based on the emphasis point amplitude.
|
||||
/// </summary>
|
||||
///
|
||||
/// <param name="amplitude">The amplitude of the emphasis, from 0.0 to 1.0</param>
|
||||
static PresetType presetTypeForEmphasis(float amplitude)
|
||||
{
|
||||
if (amplitude > 0.5f)
|
||||
{
|
||||
return HapticPatterns.PresetType.HeavyImpact;
|
||||
}
|
||||
else if (amplitude <= 0.5f && amplitude > 0.3)
|
||||
{
|
||||
return HapticPatterns.PresetType.MediumImpact;
|
||||
}
|
||||
else
|
||||
{
|
||||
return HapticPatterns.PresetType.LightImpact;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a haptic with constant amplitude and frequency.
|
||||
/// </summary>
|
||||
///
|
||||
/// On iOS and with gamepads, you can use HapticController::clipLevel to modulate the haptic
|
||||
/// while it is playing. iOS additional supports modulating the frequency with
|
||||
/// HapticController::clipFrequencyShift.
|
||||
///
|
||||
/// When \ref DeviceCapabilities.meetsAdvancedRequirements returns false on mobile,
|
||||
/// the behavior of this method is different for iOS and Android:
|
||||
/// <ul>
|
||||
/// <li>On iOS, it will play the preset <c>HapticPatterns.PresetType.HeavyImpact</c>. </li>
|
||||
///
|
||||
/// <li>On Android, it will play a pattern with maximum amplitude for the set <c>duration</c>
|
||||
/// since there is no amplitude control.</li>
|
||||
///
|
||||
/// </ul>
|
||||
/// <param name="amplitude">Amplitude, from 0.0 to 1.0</param>
|
||||
/// <param name="frequency">Frequency, from 0.0 to 1.0</param>
|
||||
/// <param name="duration">Play duration in seconds</param>
|
||||
public static void PlayConstant(float amplitude, float frequency, float duration)
|
||||
{
|
||||
if (constantTemplate == null || !HapticController.hapticsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float clampedAmplitude = Mathf.Clamp(amplitude, 0.0f, 1.0f);
|
||||
float clampedFrequency = Mathf.Clamp(frequency, 0.0f, 1.0f);
|
||||
float clampedDurationSecs = Mathf.Max(duration, 0.0f);
|
||||
|
||||
String json = constantTemplate
|
||||
.Replace("{duration}", clampedDurationSecs.ToString(numberFormat));
|
||||
|
||||
// This preprocessor section will only run for non-mobile platforms
|
||||
GamepadRumble rumble = new GamepadRumble();
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
int rumbleDurationMs = (int)(clampedDurationSecs * 1000);
|
||||
const int rumbleEntryDurationMs = 16; // One rumble entry per frame at 60 FPS, which is the limit of what GamepadRumbler can play
|
||||
int rumbleEntryCount = rumbleDurationMs / rumbleEntryDurationMs;
|
||||
rumble.durationsMs = new int[rumbleEntryCount];
|
||||
rumble.lowFrequencyMotorSpeeds = new float[rumbleEntryCount];
|
||||
rumble.highFrequencyMotorSpeeds = new float[rumbleEntryCount];
|
||||
|
||||
// Create many rumble entries instead of just one. With just one entry, changing
|
||||
// clipLevel while the rumble is playing would have no effect, as GamepadRumbler applies
|
||||
// a change only to the next rumble entry, not the one currently playing.
|
||||
for (int i = 0; i < rumbleEntryCount; i++)
|
||||
{
|
||||
rumble.durationsMs[i] = rumbleEntryDurationMs;
|
||||
rumble.lowFrequencyMotorSpeeds[i] = 1.0f;
|
||||
rumble.highFrequencyMotorSpeeds[i] = 1.0f;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (HapticController.Init() || GamepadRumbler.IsConnected())
|
||||
{
|
||||
HapticController.Load(System.Text.Encoding.UTF8.GetBytes(json), rumble);
|
||||
HapticController.Loop(false);
|
||||
HapticController.clipLevel = clampedAmplitude;
|
||||
HapticController.clipFrequencyShift = clampedFrequency;
|
||||
HapticController.Play();
|
||||
}
|
||||
else if (DeviceCapabilities.isVersionSupported)
|
||||
{
|
||||
#if (UNITY_ANDROID && !UNITY_EDITOR)
|
||||
constantPatternTime[1] = duration;
|
||||
LofeltHaptics.PlayMaximumAmplitudePattern(constantPatternTime);
|
||||
#elif (UNITY_IOS && !UNITY_EDITOR)
|
||||
HapticPatterns.PlayPreset(PresetType.HeavyImpact);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static Preset GetPresetForType(PresetType type)
|
||||
{
|
||||
Debug.Assert(type != PresetType.None);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case PresetType.Selection:
|
||||
return Selection;
|
||||
case PresetType.LightImpact:
|
||||
return Light;
|
||||
case PresetType.MediumImpact:
|
||||
return Medium;
|
||||
case PresetType.HeavyImpact:
|
||||
return Heavy;
|
||||
case PresetType.RigidImpact:
|
||||
return Rigid;
|
||||
case PresetType.SoftImpact:
|
||||
return Soft;
|
||||
case PresetType.Success:
|
||||
return Success;
|
||||
case PresetType.Failure:
|
||||
return Failure;
|
||||
case PresetType.Warning:
|
||||
return Warning;
|
||||
}
|
||||
|
||||
// Silence compiler warning about not all code paths returning something
|
||||
return Medium;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a set of predefined haptic patterns.
|
||||
/// </summary>
|
||||
///
|
||||
/// These predefined haptic patterns are played and represented in different ways for iOS,
|
||||
/// Android and gamepads.
|
||||
///
|
||||
/// - On iOS, this function triggers system haptics that are native to iOS. Calling
|
||||
/// \ref HapticController.Stop() won't stop haptics.
|
||||
/// - On Android devices that can play <c>.haptic</c> clips (DeviceCapabilities.meetsAdvancedRequirements
|
||||
/// is <c>true</c>) and on gamepads, this function plays a haptic pattern that has a similar
|
||||
/// experience to the matching iOS system haptics.
|
||||
/// - On Android devices that can not play <c>.haptic</c> clips (DeviceCapabilities.meetsAdvancedRequirements
|
||||
/// is <c>false</c>), this function plays a haptic pattern that has a similar experience to
|
||||
/// the matching iOS system haptics, by turning the motor off and on at maximum amplitude.
|
||||
///
|
||||
/// This is a "fire-and-forget" method. Other functionalities like seeking, looping, and
|
||||
/// runtime modulation won't work after calling this method.
|
||||
///
|
||||
/// <param name="presetType">Type of preset represented by a \ref PresetType enum</param>
|
||||
public static void PlayPreset(PresetType presetType)
|
||||
{
|
||||
if (!HapticController.hapticsEnabled || presetType == PresetType.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Preset preset = GetPresetForType(presetType);
|
||||
|
||||
#if (UNITY_IOS && !UNITY_EDITOR)
|
||||
LofeltHaptics.TriggerPresetHaptics((int)presetType);
|
||||
return;
|
||||
#else
|
||||
if (HapticController.Init() || GamepadRumbler.IsConnected())
|
||||
{
|
||||
#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT
|
||||
HapticController.Load(preset.jsonClip, preset.gamepadRumble);
|
||||
#else
|
||||
HapticController.Load(preset.jsonClip);
|
||||
#endif
|
||||
HapticController.Loop(false);
|
||||
HapticController.Play();
|
||||
return;
|
||||
}
|
||||
|
||||
if (DeviceCapabilities.isVersionSupported)
|
||||
{
|
||||
#if (UNITY_ANDROID && !UNITY_EDITOR)
|
||||
LofeltHaptics.PlayMaximumAmplitudePattern(preset.maximumAmplitudePattern);
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the haptic preset duration.
|
||||
/// </summary>
|
||||
///
|
||||
/// While a preset is played back in different ways on iOS, Android and gamepads, the
|
||||
/// duration is similar for each playback method.
|
||||
///
|
||||
/// <param name="presetType"> Type of preset represented by a \ref PresetType enum </param>
|
||||
/// <returns>Returns a float with a the preset duration; if the selected preset is `None`, it returns 0</returns>
|
||||
public static float GetPresetDuration(PresetType presetType)
|
||||
{
|
||||
if (presetType == PresetType.None)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return GetPresetForType(presetType).GetDuration();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user