diff --git a/Assets/Plugins.meta b/Assets/Plugins.meta new file mode 100644 index 0000000..7c261b5 --- /dev/null +++ b/Assets/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3805328a1b4e4095ad1ada100d89bdb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android.meta b/Assets/Plugins/Android.meta new file mode 100644 index 0000000..75d12b1 --- /dev/null +++ b/Assets/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9390e14b79696426d8527e368308b77c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/libs.meta b/Assets/Plugins/Android/libs.meta new file mode 100644 index 0000000..c23b029 --- /dev/null +++ b/Assets/Plugins/Android/libs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c8e190729c262425ab13ea81b5b478c6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/libs/LofeltHaptics.aar b/Assets/Plugins/Android/libs/LofeltHaptics.aar new file mode 100644 index 0000000..5c2bd4e Binary files /dev/null and b/Assets/Plugins/Android/libs/LofeltHaptics.aar differ diff --git a/Assets/Plugins/Android/libs/LofeltHaptics.aar.meta b/Assets/Plugins/Android/libs/LofeltHaptics.aar.meta new file mode 100644 index 0000000..6dc46a5 --- /dev/null +++ b/Assets/Plugins/Android/libs/LofeltHaptics.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 4052800b132124e29b9627e77b348b41 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Windows.meta b/Assets/Plugins/Windows.meta new file mode 100644 index 0000000..934e753 --- /dev/null +++ b/Assets/Plugins/Windows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 04babe37531df1741bce773dd20ddca0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Windows/x64.meta b/Assets/Plugins/Windows/x64.meta new file mode 100644 index 0000000..f6a565c --- /dev/null +++ b/Assets/Plugins/Windows/x64.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d1cfa7fc0db963a4897d26b3d50d11ec +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Windows/x64/nice_vibrations_editor_plugin.dll b/Assets/Plugins/Windows/x64/nice_vibrations_editor_plugin.dll new file mode 100644 index 0000000..97a5b45 Binary files /dev/null and b/Assets/Plugins/Windows/x64/nice_vibrations_editor_plugin.dll differ diff --git a/Assets/Plugins/Windows/x64/nice_vibrations_editor_plugin.dll.meta b/Assets/Plugins/Windows/x64/nice_vibrations_editor_plugin.dll.meta new file mode 100644 index 0000000..57fdade --- /dev/null +++ b/Assets/Plugins/Windows/x64/nice_vibrations_editor_plugin.dll.meta @@ -0,0 +1,80 @@ +fileFormatVersion: 2 +guid: b177fc310544d6a4c94b26e23d9b31d1 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude WebGL: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude WindowsStoreApps: 1 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: x86_64 + DefaultValueInitialized: true + OS: Windows + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + WebGL: WebGL + second: + enabled: 0 + settings: {} + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: X64 + DontProcess: false + PlaceholderPath: + SDK: AnySDK + ScriptingBackend: AnyScriptingBackend + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS.meta b/Assets/Plugins/iOS.meta new file mode 100644 index 0000000..f20b5c7 --- /dev/null +++ b/Assets/Plugins/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3cc8ce9203c4a4d03b07e0ae9a3cf219 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/LofeltHaptics.framework.meta b/Assets/Plugins/iOS/LofeltHaptics.framework.meta new file mode 100644 index 0000000..47312cc --- /dev/null +++ b/Assets/Plugins/iOS/LofeltHaptics.framework.meta @@ -0,0 +1,81 @@ +fileFormatVersion: 2 +guid: 3f3b3e40c5ec34183af765f15c1ce362 +folderAsset: yes +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: true + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/LofeltHaptics.framework/Headers/LofeltHaptics.h b/Assets/Plugins/iOS/LofeltHaptics.framework/Headers/LofeltHaptics.h new file mode 100644 index 0000000..848ed56 --- /dev/null +++ b/Assets/Plugins/iOS/LofeltHaptics.framework/Headers/LofeltHaptics.h @@ -0,0 +1,163 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +//! Project version number for LofeltHaptics. +FOUNDATION_EXPORT double LofeltHapticsVersionNumber; + +//! Project version string for LofeltHaptics. +FOUNDATION_EXPORT const unsigned char LofeltHapticsVersionString[]; + +//! Custom error domain +extern NSString *_Nonnull const LofeltErrorDomain; + +/*! + @class LofeltHaptics + @brief The LofeltHaptics class + @discussion Defines the API of Lofelt SDK for iOS. + + The LofeltHaptics class is not thread safe and can only be used from + the main thread. + + When the app is put into the background, Core Haptics will not allow + playing any haptics. LofeltHaptics will detect this situation and cease + all activity. + When the app is put into the foreground again, Core Haptics will allow + playing haptics again, and LofeltHaptics re-initalizes itself. However, + haptics that were interrupted when the app was backgrounded do not + automatically resume and need to be started again by calling @c play(). + @author Joao Freire, James Kneafsey, Thomas McGuire, Tomash GHz + @copyright © 2020 Lofelt. All rights reserved. + */ +@interface LofeltHaptics : NSObject +{ + void *_controller; + id _foregroundNotificationObserver; + id _backgroundNotificationObserver; +} + +/*! @abstract Checks if the iPhone meets the minimum requirements + @discussion This allows for a runtime check on iPhones that won't + meet the requirements for Lofelt Haptics. + + @return Whether the iPhone supports or not Lofelt Haptics + */ ++ (BOOL)deviceMeetsMinimumRequirement; + +- (instancetype)init NS_UNAVAILABLE; + +/*! @abstract Creates an instance of LofeltHaptics. + @discussion There should only be one instance of `LofeltHaptics` created in a given application. + @param error If the initialization fails, this will be set to a valid NSError describing the error. +*/ +- (nullable instancetype)initAndReturnError:(NSError **)error API_AVAILABLE(ios(13)) NS_SWIFT_NAME(init()); + +/*! @abstract Loads a haptic clip from string data. + @discussion The data must be in a valid Lofelt JSON format. + If a haptic clip is currently playing, it will be stopped. + @param data The Lofelt JSON format string. + @param error If the load operation fails, this will be set to a valid NSError describing the error. + @return Whether the operation succeeded +*/ +- (BOOL)load:(NSString *_Nonnull)data error:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract A version of @c load() taking @c NSData instead of @c NSString. + @discussion This method can be faster than @c load(), as it avoids string conversions. + @param data The .haptic clip, as UTF-8 encoded JSON string without a null terminator. + @param error If the load operation fails, this will be set to a valid NSError describing the error. + @return Whether the operation succeeded +*/ +- (BOOL)loadFromData:(NSData *_Nonnull)data error:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract Plays a loaded haptic clip. + @discussion The data must be preloaded using @c load() . + Only one haptic clip can play at a time. + Playback will start from the beginning of the haptic clip, or from the seek + position if seek() has been called before. + Calling play() if the clip is already playing has no effect. + @param error If the play operation fails, this will be set to a valid NSError describing the error. + @return Whether the operation succeeded +*/ +- (BOOL)play:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract Stops the haptic clip that is currently playing. + @discussion The call is ignored if no clip is loaded or no clip is playing. + @param error If the stop operation fails, this will be set to a valid NSError describing the error. + @return Whether the operation succeeded + */ +- (BOOL)stop:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract Jumps to a time position in the haptic clip + @discussion The playback state (playing or stopped) will not be changed unless seeking + beyond the end of the haptic clip. Seeking beyond the end of the clip will stop + playback. + Seeking to a negative position will start playback after a delay. + @param time The new position within the clip, as seconds from the beginning of the clip + @param error If the seek operation fails, this will be set to a valid NSError describing the error. + @return Whether the operation succeeded + */ +- (BOOL)seek:(float)time error:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract Multiplies the amplitude of every breakpoint of the clip with the given + multiplication factor + @discussion In other words, this function applies a gain (for factors greater than 1.0) + or an attenuation (for factors less than 1.0) to the clip. + If the resulting amplitude of a breakpoint is greater than 1.0, it is + clipped to 1.0. The amplitude is clipped hard, no limiter is used. + The clip needs to be loaded with @c load() first. Loading a clip resets + the multiplication factor back to the default of 1.0. + If no clip is currently playing, the multiplication will take effect + once @c play() is called. If a clip is currently playing, the multiplication + will take effect immediately. + @param amplitudeMultiplication The factor by which each amplitude will be multiplied. This value is a + multiplication factor, it is not a dB value. The factor needs to be 0 + or greater. + @param error If the operation fails, this will be set to a valid NSError describing + the error. An error can for example happen if no clip is loaded, or if + the factor is outside of the valid range. + @return Whether the operation succeeded + */ +- (BOOL)setAmplitudeMultiplication:(float)amplitudeMultiplication error:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract Adds the given shift to the frequency of every breakpoint in the clip, + including the emphasis. + @discussion In other words, this function shifts all frequencies of the clip. + 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 @c load() first. Loading a clip resets + the shift back to the default of 0.0. + If no clip is currently playing, the shift will take effect once @c play() + is called. If a clip is currently playing, the shift will take effect + immediately. + @param shift The amount by which each frequency should be shifted. This number is added + to each frequency value. The shift needs to be between -1.0 and 1.0. + @param error If the operation fails, this will be set to a valid NSError describing + the error. An error can for example happen if no clip is loaded, or if + the shift is outside of the valid range. + @return Whether the operation succeeded + */ +- (BOOL)setFrequencyShift:(float)shift error:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract Sets the playback to repeat from the start at the end of the clip. + @discussion Changes done with this function are only applied when @c play() is called. + When @c load() is called, looping is always disabled. + Playback will always start at the beginning of the clip, even if + @c seek() was used to jump to a different clip position before. + @param enabled When true, looping is set enabled; false disables looping. + @param error If the loop operation fails, this will be set to a valid NSError describing the error. + @return Whether the operation succeeded + */ +- (BOOL)loop:(BOOL)enabled error:(NSError *_Nullable *_Nullable)error API_AVAILABLE(ios(13)); + +/*! @abstract Returns the duration of the loaded clip + @discussion It will return 0.0 for an invalid clip + @return Duration of the loaded clip + */ +- (float)getClipDuration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Assets/Plugins/iOS/LofeltHaptics.framework/Info.plist b/Assets/Plugins/iOS/LofeltHaptics.framework/Info.plist new file mode 100644 index 0000000..370ca3a Binary files /dev/null and b/Assets/Plugins/iOS/LofeltHaptics.framework/Info.plist differ diff --git a/Assets/Plugins/iOS/LofeltHaptics.framework/LofeltHaptics b/Assets/Plugins/iOS/LofeltHaptics.framework/LofeltHaptics new file mode 100755 index 0000000..490f9f2 Binary files /dev/null and b/Assets/Plugins/iOS/LofeltHaptics.framework/LofeltHaptics differ diff --git a/Assets/Plugins/iOS/LofeltHaptics.framework/Modules/module.modulemap b/Assets/Plugins/iOS/LofeltHaptics.framework/Modules/module.modulemap new file mode 100644 index 0000000..44a6c28 --- /dev/null +++ b/Assets/Plugins/iOS/LofeltHaptics.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module LofeltHaptics { + umbrella header "LofeltHaptics.h" + + export * + module * { export * } +} diff --git a/Assets/Plugins/macOS.meta b/Assets/Plugins/macOS.meta new file mode 100644 index 0000000..34c7995 --- /dev/null +++ b/Assets/Plugins/macOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d7aed595fe8f44c70b4bf38214153c03 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/macOS/libnice_vibrations_editor_plugin.dylib b/Assets/Plugins/macOS/libnice_vibrations_editor_plugin.dylib new file mode 100644 index 0000000..f237cdd Binary files /dev/null and b/Assets/Plugins/macOS/libnice_vibrations_editor_plugin.dylib differ diff --git a/Assets/Plugins/macOS/libnice_vibrations_editor_plugin.dylib.meta b/Assets/Plugins/macOS/libnice_vibrations_editor_plugin.dylib.meta new file mode 100644 index 0000000..c6ca628 --- /dev/null +++ b/Assets/Plugins/macOS/libnice_vibrations_editor_plugin.dylib.meta @@ -0,0 +1,80 @@ +fileFormatVersion: 2 +guid: 279c3792841e74f96b13b007d349facc +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 1 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 1 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: OSX + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + iPhone: iOS + second: + enabled: 0 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt.meta b/Assets/Scripts/Lofelt.meta new file mode 100644 index 0000000..fb0ae43 --- /dev/null +++ b/Assets/Scripts/Lofelt.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ea1308527d88a4ad79766c4f2ef437bb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations.meta b/Assets/Scripts/Lofelt/NiceVibrations.meta new file mode 100644 index 0000000..7f8c92a --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dbba4d66508f04365a91f3b7eb67d2f3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components.meta new file mode 100644 index 0000000..6e9457f --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 688d27f50942c40c39cb42dc1e5eab7a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/AssemblyInfo.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/AssemblyInfo.cs new file mode 100644 index 0000000..0cac064 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/AssemblyInfo.cs @@ -0,0 +1,3 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("NiceVibrationTests")] \ No newline at end of file diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/AssemblyInfo.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/AssemblyInfo.cs.meta new file mode 100644 index 0000000..2a79def --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0924103a050c4bbc88d415b79a67df2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/DeviceCapabilities.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/DeviceCapabilities.cs new file mode 100644 index 0000000..0e7c6b5 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/DeviceCapabilities.cs @@ -0,0 +1,253 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using UnityEngine; + +#if (UNITY_IOS && !UNITY_EDITOR) + using UnityEngine.iOS; +#endif + +namespace Lofelt.NiceVibrations +{ + /// + /// A class containing properties that describe the current device capabilities for use with + /// Nice Vibrations + /// + /// + /// This class describes the capabilities of an iOS or Android device, gamepads are not handled + /// by it. + public static class DeviceCapabilities + { + /// + /// Property that holds the current RuntimePlatform + /// + public static RuntimePlatform platform { get; } + + /// + /// Property that holds the current platform version. + /// + /// iOS version on iOS, Android API level on Android or 0 otherwise. + public static int platformVersion { get; } + + /// + /// Indicates if the device meets the requirements to play advanced haptics with + /// Nice Vibrations + /// + /// + /// Advanced requirements means that the device can play back .haptic clips. + /// While devices that don't meet the advanced requirements can not play back .haptic + /// clips, they can still play back simpler fallback haptics as long as + /// \ref isVersionSupported is true. + /// + /// While DeviceCapabilities.isVersionSupported only checks the OS version, this method + /// additionally checks the device capabilities. + /// + /// The required device capabilities are: + /// - iOS: iPhone >= 8 + /// - Android: Amplitude control for the Vibrator + /// + /// You don't usually need to check this property. All other methods in HapticController + /// will check \ref meetsAdvancedRequirements before calling into LofeltHaptics. + /// In case the device does not support advanced haptics there is a possibility of fallback + /// haptics based on presets. + public static bool meetsAdvancedRequirements + { + get + { + return _meetsAdvancedRequirements; + } + } + private static bool _meetsAdvancedRequirements; + + /// + /// Indicates if the OS version is high enough to play haptics with Nice Vibrations. + /// + /// + /// The minimum required versions are: + /// - iOS >= 11 + /// - Android API level >= 17 + /// + /// This only checks the minimum supported OS version in terms of API and does not guarantee + /// that advanced haptics with amplitude control can be recreated, For that check with + /// \ref meetsAdvancedRequirements. + public static bool isVersionSupported { get; } + + /// + /// Indicates if the device is capable of amplitude control in order to recreate + /// advanced haptics. + /// + public static bool hasAmplitudeControl + { + get + { + return _hasAmplitudeControl; + } + } + private static bool _hasAmplitudeControl; + + /// + /// Indicates if the device is capable of changing the frequency of haptic signals + /// + public static bool hasFrequencyControl + { + get + { + return _hasFrequencyControl; + } + } + private static bool _hasFrequencyControl; + + /// + /// Indicates if the device is capable of real-time amplitude modulation of haptic signals + /// + public static bool hasAmplitudeModulation + { + get + { + return _hasAmplitudeModulation; + } + } + private static bool _hasAmplitudeModulation; + + /// + /// Indicates if the device is capable of real-time frequency modulation of haptic signals + /// + public static bool hasFrequencyModulation + { + get + { + return _hasFrequencyModulation; + } + } + private static bool _hasFrequencyModulation; + + /// + /// Indicates if the device is capable of natively reproducing emphasized haptics + /// + public static bool hasEmphasis + { + get + { + return _hasEmphasis; + } + } + private static bool _hasEmphasis; + + /// + /// Indicates if the device is capable of emulating emphasized haptics + /// + public static bool canEmulateEmphasis + { + get + { + return _canEmulateEmphasis; + } + } + private static bool _canEmulateEmphasis; + + /// + /// Indicates if the device is capable of looping haptic clips + /// + public static bool canLoop + { + get + { + return _canLoop; + } + } + private static bool _canLoop; + + /// + /// Constructor that fills in the only the DeviceCapabilities platform version properties. + /// + /// This is separate of Init() because we need to first check the version numbers before + /// initializing LofeltHaptics + static DeviceCapabilities() + { + platform = Application.platform; + platformVersion = 0; + isVersionSupported = false; + +#if (UNITY_ANDROID && !UNITY_EDITOR) + platformVersion = int.Parse(SystemInfo.operatingSystem.Substring(SystemInfo.operatingSystem.IndexOf("-") + 1, 3)); + const int minimumSupportedAndroidSDKVersion = 17; + isVersionSupported = platformVersion >= minimumSupportedAndroidSDKVersion; +#elif (UNITY_IOS && !UNITY_EDITOR) + string versionString = Device.systemVersion; + string[] versionArray = versionString.Split('.'); + platformVersion = int.Parse(versionArray[0]); + const int minimumSupportedIOSVersion = 11; + isVersionSupported = platformVersion >= minimumSupportedIOSVersion; + + DeviceGeneration generation = Device.generation; + if ((generation == DeviceGeneration.iPhone3G) + || (generation == DeviceGeneration.iPhone3GS) + || (generation == DeviceGeneration.iPodTouch1Gen) + || (generation == DeviceGeneration.iPodTouch2Gen) + || (generation == DeviceGeneration.iPodTouch3Gen) + || (generation == DeviceGeneration.iPodTouch4Gen) + || (generation == DeviceGeneration.iPhone4) + || (generation == DeviceGeneration.iPhone4S) + || (generation == DeviceGeneration.iPhone5) + || (generation == DeviceGeneration.iPhone5C) + || (generation == DeviceGeneration.iPhone5S) + || (generation == DeviceGeneration.iPhone6) + || (generation == DeviceGeneration.iPhone6Plus) + || (generation == DeviceGeneration.iPhone6S) + || (generation == DeviceGeneration.iPhone6SPlus) + || (generation == DeviceGeneration.iPhoneSE1Gen) + || (generation == DeviceGeneration.iPad1Gen) + || (generation == DeviceGeneration.iPad2Gen) + || (generation == DeviceGeneration.iPad3Gen) + || (generation == DeviceGeneration.iPad4Gen) + || (generation == DeviceGeneration.iPad5Gen) + || (generation == DeviceGeneration.iPadAir1) + || (generation == DeviceGeneration.iPadAir2) + || (generation == DeviceGeneration.iPadMini1Gen) + || (generation == DeviceGeneration.iPadMini2Gen) + || (generation == DeviceGeneration.iPadMini3Gen) + || (generation == DeviceGeneration.iPadMini4Gen) + || (generation == DeviceGeneration.iPadPro10Inch1Gen) + || (generation == DeviceGeneration.iPadPro10Inch2Gen) + || (generation == DeviceGeneration.iPadPro11Inch) + || (generation == DeviceGeneration.iPadPro1Gen) + || (generation == DeviceGeneration.iPadPro2Gen) + || (generation == DeviceGeneration.iPadPro3Gen) + || (generation == DeviceGeneration.iPadUnknown) + || (generation == DeviceGeneration.iPodTouch1Gen) + || (generation == DeviceGeneration.iPodTouch2Gen) + || (generation == DeviceGeneration.iPodTouch3Gen) + || (generation == DeviceGeneration.iPodTouch4Gen) + || (generation == DeviceGeneration.iPodTouch5Gen) + || (generation == DeviceGeneration.iPodTouch6Gen) + || (generation == DeviceGeneration.iPhone6SPlus)) + { + isVersionSupported = false; + } + +#elif (UNITY_EDITOR) + isVersionSupported = true; +#endif + } + + /// + /// Function that initializes the rest of the DeviceCapabilities properties. + /// Must be called after LofeltHaptics was initialized. + /// + public static void Init() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + _hasAmplitudeControl = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + _canEmulateEmphasis = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + _canLoop = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); +#elif (UNITY_IOS && !UNITY_EDITOR) + _hasAmplitudeControl = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + _hasFrequencyControl = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + _hasAmplitudeModulation = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + _hasFrequencyModulation = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + _hasEmphasis = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + _canLoop = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); +#endif + _meetsAdvancedRequirements = LofeltHaptics.DeviceMeetsMinimumPlatformRequirements(); + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/DeviceCapabilities.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/DeviceCapabilities.cs.meta new file mode 100644 index 0000000..715c644 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/DeviceCapabilities.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca68228d4301d47fab6a64b6d285e2dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Gamepad.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/Gamepad.cs new file mode 100644 index 0000000..e700ce3 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Gamepad.cs @@ -0,0 +1,435 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using System; +using System.Diagnostics; +using System.Timers; +using UnityEngine; + +// There are 3 conditions for working gamepad support in Nice Vibrations: +// +// 1. NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED - The input system package needs to be installed. +// See https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Installation.html#installing-the-package +// This is set by Nice Vibrations' assembly definition file, using a version define. +// See https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html#define-symbols +// about version defines, and see Lofelt.NiceVibrations.asmdef for the usage in Nice Vibrations. +// +// 2. ENABLE_INPUT_SYSTEM - The input system needs to be enabled in the project settings. +// See https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Installation.html#enabling-the-new-input-backends +// This define is set by Unity, see https://docs.unity3d.com/Manual/PlatformDependentCompilation.html +// +// 3. NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT - This is a user-defined define which needs to be not set. +// NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT is not set by default. It can be set by a user in the +// player settings to disable gamepad support completely. One reason to do this is to reduce the +// size of a HapticClip asset, as setting this define changes to HapticImporter to not add the +// GamepadRumble to the HapticClip. Changing this define requires re-importing all .haptic clip +// assets to update HapticClip's GamepadRumble. +// +// If any of the 3 conditions is not met, GamepadRumbler doesn't contain any calls into +// UnityEngine.InputSystem, and CanPlay() always returns false. +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT +using UnityEngine.InputSystem; +#endif + +namespace Lofelt.NiceVibrations +{ + /// + /// Contains a vibration pattern to make a gamepad rumble. + /// + /// + /// GamepadRumble contains the information on when to set what motor speeds on a gamepad + /// to make it rumble with a specific pattern. + /// + /// GamepadRumble has three arrays of the same length representing the rumble pattern. The + /// entries for each array index describe for how long to turn on the gamepad's vibration + /// motors, at what speed. + [Serializable] + public struct GamepadRumble + { + /// + /// The duration, in milliseconds, that the motors will be turned on at the speed set + /// in \ref lowFrequencyMotorSpeeds and \ref highFrequencyMotorSpeeds at the same array + /// index + /// + [SerializeField] + public int[] durationsMs; + + /// + /// The total duration of the GamepadRumble, in milliseconds + /// + [SerializeField] + public int totalDurationMs; + + /// + /// The motor speeds of the low frequency motor + /// + [SerializeField] + public float[] lowFrequencyMotorSpeeds; + + /// + /// The motor speeds of the high frequency motor + /// + [SerializeField] + public float[] highFrequencyMotorSpeeds; + + /// + /// Checks if the GamepadRumble is valid and also not empty + /// + /// Whether the GamepadRumble is valid + public bool IsValid() + { + return durationsMs != null && + lowFrequencyMotorSpeeds != null && + highFrequencyMotorSpeeds != null && + durationsMs.Length == lowFrequencyMotorSpeeds.Length && + durationsMs.Length == highFrequencyMotorSpeeds.Length && + durationsMs.Length > 0; + } + } + + /// + /// Vibrates a gamepad based on a GamepadRumble rumble pattern. + /// + /// + /// GamepadRumbler can load and play back a GamepadRumble pattern on the current + /// gamepad. + /// + /// This is a low-level class that normally doesn't need to be used directly. Instead, + /// you can use HapticSource and HapticController to play back haptic clips, as those + /// classes support gamepads by using GamepadRumbler internally. + public static class GamepadRumbler + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + static GamepadRumble loadedRumble; + + static bool rumbleLoaded = false; + + // This Timer is used to wait until it is time to advance to the next entry in loadedRumble. + // When the Timer is elapsed, ProcessNextRumble() is called to set new motor speeds to the + // gamepad. + static Timer rumbleTimer = new Timer(); + + // The index of the entry of loadedRumble that is currently being played back + static int rumbleIndex = -1; + + // The total duration of rumble entries that have been played back so far + static long rumblePositionMs = 0; + + // Keeps track of how much time elapsed since playback was started + static Stopwatch playbackWatch = new Stopwatch(); + + /// + /// A multiplication factor applied to the motor speeds of the low frequency motor. + /// + /// + /// The multiplication factor is applied to the low frequency motor speed of every + /// GamepadRumble entry before playing it. + /// + /// In other words, this applies a gain (for factors greater than 1.0) or an attenuation + /// (for factors less than 1.0) to the clip. If the resulting speed of an entry is + /// greater than 1.0, it is clipped to 1.0. The speed is clipped hard, no limiter is + /// used. + /// + /// The motor speed multiplication is reset when calling Load(), so Load() needs to be + /// called first before setting the multiplication. + /// + /// A change of the multiplication is applied to a currently playing rumble, but only + /// for the next rumble entry, not the one currently playing. + public static float lowFrequencyMotorSpeedMultiplication = 1.0f; + + /// + /// Same as \ref lowFrequencyMotorSpeedMultiplication, but for the high frequency speed + /// motor. + /// + public static float highFrequencyMotorSpeedMultiplication = 1.0f; + + static int currentGamepadID = -1; + +#endif + + /// + /// Initializes the GamepadRumbler. + /// + /// + /// This needs to be called from the main thread, which is the reason why this is a method + /// instead of a static constructor: Sometimes Unity calls static constructors from a + /// different thread, and an explicit Init() method gives us more control over this. + public static void Init() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + // Initialize rumbleTimer, so that ProcessNextRumble() will be called on the main thread + // when the timer is triggered. + var syncContext = System.Threading.SynchronizationContext.Current; + rumbleTimer.Elapsed += (object obj, System.Timers.ElapsedEventArgs args) => + { + syncContext.Post(_ => + { + ProcessNextRumble(); + }, null); + }; +#endif + } + + /// + /// Checks whether a call to Play() would trigger playback on a gamepad. + /// + /// + /// Playing back a rumble pattern with Play() only works if a gamepad is connected and if + /// a GamepadRumble has been loaded with Load() before. + /// + /// Whether a vibration can be triggered on a gamepad + public static bool CanPlay() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + return IsConnected() && rumbleLoaded && loadedRumble.IsValid(); +#else + return false; +#endif + } + +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + /// + /// Gets the Gamepad object corresponding to the specified gamepad ID. + /// + /// + /// If the specified ID is out of range of the connected gamepad(s), + /// InputSystem.Gamepad.current will be returned. + /// + /// The ID of the gamepad to be returned. + /// A InputSystem.Gamepad + static UnityEngine.InputSystem.Gamepad GetGamepad(int gamepadID) + { + if (gamepadID >= 0) + { + if (gamepadID >= UnityEngine.InputSystem.Gamepad.all.Count) + { + return UnityEngine.InputSystem.Gamepad.current; + } + else + { + return UnityEngine.InputSystem.Gamepad.all[gamepadID]; + } + } + return UnityEngine.InputSystem.Gamepad.current; + } +#endif + + /// + /// Set the current gamepad for haptics playback by ID. + /// + /// + /// This method needs be called before haptics playback, e.g. \ref HapticController.Play(), + /// \ref HapticPatterns.PlayEmphasis(), \ref HapticPatterns.PlayConstant(), etc, for + /// for the gamepad to be properly selected. + /// + /// If this method isn't called, haptics will be played on InputSystem.Gamepad.current + /// + /// For example, if you have 3 controllers connected, you have to choose between values 0, 1, + /// and 2. + /// + /// If the gamepad ID value doesn't match any connected gamepad, calling + /// this method has no effect. + /// The ID of the gamepad + public static void SetCurrentGamepad(int gamepadID) + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + if (gamepadID < UnityEngine.InputSystem.Gamepad.all.Count) + { + currentGamepadID = gamepadID; + } +#endif + } + + /// + /// Checks whether a gamepad is connected and recognized by Unity's input system. + /// + /// + /// If the input system package is not installed or not enabled, the gamepad is not + /// recognized and treated as not connected here. + /// + /// If the NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT define is set in the player settings, + /// this function pretends no gamepad is connected. + /// + /// Whether a gamepad is connected + public static bool IsConnected() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + return GetGamepad(currentGamepadID) != null; +#else + return false; +#endif + } + + /// + /// Loads a rumble pattern for later playback. + /// + /// + /// The rumble pattern to load + public static void Load(GamepadRumble rumble) + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + if (rumble.IsValid()) + { + loadedRumble = rumble; + rumbleLoaded = true; + lowFrequencyMotorSpeedMultiplication = 1.0f; + highFrequencyMotorSpeedMultiplication = 1.0f; + } + else + { + Unload(); + } +#endif + } + + /// + /// Plays back the rumble pattern loaded previously with Load(). + /// + /// + /// If no rumble pattern has been loaded, or if no gamepad is connected, this method does + /// nothing. + public static void Play() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + if (CanPlay()) + { + rumbleIndex = 0; + rumblePositionMs = 0; + playbackWatch.Restart(); + ProcessNextRumble(); + } +#endif + } + + /// + /// Stops playback previously started with Play() by turning off the gamepad's motors. + /// + public static void Stop() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + if (GetGamepad(currentGamepadID) != null) + { + GetGamepad(currentGamepadID).ResetHaptics(); + } + rumbleTimer.Enabled = false; + rumbleIndex = -1; + rumblePositionMs = 0; + playbackWatch.Stop(); +#endif + } + + /// + /// Stops playback and unloads the currently loaded GamepadRumble from memory. + /// + public static void Unload() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + loadedRumble.highFrequencyMotorSpeeds = null; + loadedRumble.lowFrequencyMotorSpeeds = null; + loadedRumble.durationsMs = null; + rumbleLoaded = false; + Stop(); +#endif + } + + // Advances the position in the GamepadRumble by one. + // + // If the end of the rumble has been reached, playback is stopped and false is returned. + private static bool IncreaseRumbleIndex() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + rumblePositionMs += loadedRumble.durationsMs[rumbleIndex]; + rumbleIndex++; + if (rumbleIndex == loadedRumble.durationsMs.Length) + { + Stop(); + return false; + } + + return true; +#else + return false; +#endif + } + + // Processes the next entry in loadedRumble by setting the gamepad's motor speeds to the + // speeds stored in that entry. + // + // Afterwards, the rumbleTimer is set to call this method again, after the time stored + // in entry of loadedRumble. + private static void ProcessNextRumble() + { +#if ((!UNITY_ANDROID && !UNITY_IOS) || UNITY_EDITOR) && NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED && ENABLE_INPUT_SYSTEM && !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + // rumbleIndex can be -1 after Stop() has been called after the call to + // ProcessNextRumble() has already been queued up via SynchronizationContext. + if (rumbleIndex == -1) + { + return; + } + + if (rumbleIndex == loadedRumble.durationsMs.Length) + { + Stop(); + return; + } + + UnityEngine.Debug.Assert(loadedRumble.IsValid()); + UnityEngine.Debug.Assert(rumbleLoaded); + UnityEngine.Debug.Assert(rumbleIndex >= 0 && rumbleIndex <= loadedRumble.durationsMs.Length); + + // Figure out for how long the current rumble entry should be played (durationToWait). + // Due to the timer not waiting for exactly the same amount of time that we requested, + // there can be a bit of error that we need to compensate for. For example, if the timer + // waited for 3ms longer than we requested, we play the next rumble entry for a 3ms + // less to compensate for that. + // In fact, Unity triggers the timer only once per frame, so at 30 FPS, the timer + // resolution is 32ms. That means that the timing error can be bigger than the duration + // of the whole rumble entry, and to compensate for that, the entire rumble entry needs + // to be skipped. That's what the loop does: It skips rumble entries to compensate for + // timer error. + long elapsed = playbackWatch.ElapsedMilliseconds; + long durationToWait = 0; + while (true) + { + long rumbleEntryDuration = loadedRumble.durationsMs[rumbleIndex]; + long error = elapsed - rumblePositionMs; + durationToWait = rumbleEntryDuration - error; + + // If durationToWait is <= 0, the current rumble entry needs to be skipped to + // compensate for timer error. Otherwise break and play the current rumble entry. + if (durationToWait > 0) + { + break; + } + + // If the end of the rumble has been reached, return, as playback has stopped. + if (!IncreaseRumbleIndex()) + { + return; + } + } + + float lowFrequencySpeed = loadedRumble.lowFrequencyMotorSpeeds[rumbleIndex] * Mathf.Max(lowFrequencyMotorSpeedMultiplication, 0.0f); + float highFrequencySpeed = loadedRumble.highFrequencyMotorSpeeds[rumbleIndex] * Mathf.Max(highFrequencyMotorSpeedMultiplication, 0.0f); + + UnityEngine.InputSystem.Gamepad currentGamepad = GetGamepad(currentGamepadID); + // Check if gamepad was disconnected while playing + if (currentGamepad != null) + { + currentGamepad.SetMotorSpeeds(lowFrequencySpeed, highFrequencySpeed); + } + else + { + return; + } + + // Set up the timer to call ProcessNextRumble() again with the next rumble entry, after + // the duration of the current rumble entry. + rumblePositionMs += loadedRumble.durationsMs[rumbleIndex]; + rumbleIndex++; + rumbleTimer.Interval = durationToWait; + rumbleTimer.AutoReset = false; + rumbleTimer.Enabled = true; +#endif + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Gamepad.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Gamepad.cs.meta new file mode 100644 index 0000000..3459d00 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Gamepad.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef20247bd5f04449293bb8ea3982f3ac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticClip.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticClip.cs new file mode 100644 index 0000000..0f3705a --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticClip.cs @@ -0,0 +1,35 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using UnityEngine; + +namespace Lofelt.NiceVibrations +{ + /// + /// Represents an imported haptic clip asset. + /// + /// + /// HapticClip contains the data of a haptic clip asset imported from a .haptic file, + /// in a format suitable for playing it back at runtime. + /// A HapticClip is created by HapticImporter when importing a haptic clip asset + /// in the Unity editor, and can be played back at runtime with e.g. HapticSource or + /// HapticController::Play(). + /// + /// It contains two representations: + /// - JSON, used for playback on iOS and Android + /// - GamepadRumble, used for playback on gamepads with the GamepadRumbler class + public class HapticClip : ScriptableObject + { + /// + /// The JSON representation of the haptic clip, stored as a byte array encoded in UTF-8, + /// without a null terminator + /// + [SerializeField] + public byte[] json; + + /// + /// The haptic clip represented as a GamepadRumble struct + /// + [SerializeField] + public GamepadRumble gamepadRumble; + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticClip.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticClip.cs.meta new file mode 100644 index 0000000..8768c72 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticClip.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df8d044f677634e749812dc987300584 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticController.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticController.cs new file mode 100644 index 0000000..4a586f3 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticController.cs @@ -0,0 +1,568 @@ +// 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 +{ + /// + /// Provides haptic playback functionality. + /// + /// + /// HapticController allows you to load and play .haptic clips, and + /// provides various ways to control playback, such as seeking, looping and + /// amplitude/frequency modulation. + /// + /// If you need a MonoBehaviour API, use HapticSource and + /// HapticReceiver instead. + /// + /// On iOS and Android, the device is vibrated, using LofeltHaptics. + /// 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; + + /// + /// The haptic preset to be played when it's not possible to play a haptic clip + /// + public static HapticPatterns.PresetType fallbackPreset + { + get { return _fallbackPreset; } + set { _fallbackPreset = value; } + } + + internal static bool _hapticsEnabled = true; + + /// + /// Property to enable and disable global haptic playback + /// + public static bool hapticsEnabled + { + get { return _hapticsEnabled; } + set + { + if (_hapticsEnabled) + { + Stop(); + } + _hapticsEnabled = value; + } + } + + internal static float _outputLevel = 1.0f; + + /// + /// The overall haptic output level + /// + /// + /// 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 not 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; + + /// + /// The level of the loaded clip + /// + /// + /// 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 not 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; + + /// + /// Action that is invoked when the playback has finished + /// + /// + /// 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.lowFrequencyMotorSpeedMultiplication = _outputLevel * _clipLevel; + GamepadRumbler.highFrequencyMotorSpeedMultiplication = _outputLevel * _clipLevel; + #endif + } + + /// + /// Initializes HapticController. + /// + /// + /// 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. + /// + /// Whether the device supports the minimum requirements to play haptics + 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; + } + + /// + /// Loads a haptic clip given in JSON format for later playback. + /// + /// + /// 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. + /// + /// The haptic clip, which is the content of the + /// .haptic file, a UTF-8 encoded JSON string without a null + /// terminator + public static void Load(byte[] data) + { + GamepadRumbler.Unload(); + lastSeekTime = 0.0f; + clipLoaded = true; + clipLoadedDurationSecs = 0.0f; + if (Init()) + { + LofeltHaptics.Load(data); + } + clipLevel = 1.0f; + LoadedClipChanged?.Invoke(); + } + + /// + /// Loads the given HapticClip for later playback. + /// + /// + /// 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. + /// + /// The HapticClip to be loaded + public static void Load(HapticClip clip) + { + Load(clip.json, clip.gamepadRumble); + } + + /// + /// Loads the haptic clip given as JSON and GamepadRumble for later playback. + /// + /// + /// 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. + /// + /// The haptic clip, which is the content of the .haptic file, + /// a UTF-8 encoded JSON string without a null terminator + /// The GamepadRumble representation of the haptic clip + public static void Load(byte[] json, GamepadRumble rumble) + { + Load(json); + + GamepadRumbler.Load(rumble); + // 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(); + } + + /// + /// Plays the haptic clip that was previously loaded with Load(). + /// + /// + /// If Loop(true) 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 .haptic 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(); + } + } + + + /// + /// Loads and plays the HapticClip given as an argument. + /// + /// + /// The HapticClip to be played + public static void Play(HapticClip clip) + { + Load(clip); + Play(); + } + + /// + /// Stops haptic playback + /// + /// + public static void Stop() + { + + if (Init()) + { + LofeltHaptics.Stop(); + } + else + { + LofeltHaptics.StopPattern(); + } + GamepadRumbler.Stop(); + HandleFinishedPlayback(); + } + + /// + /// Jumps to a time position in the haptic clip. + /// + /// + /// 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. + /// + /// The new position within the clip, as seconds from the beginning + /// of the clip + public static void Seek(float time) + { + if (Init()) + { + LofeltHaptics.Stop(); + LofeltHaptics.Seek(time); + } + GamepadRumbler.Stop(); + lastSeekTime = time; + } + + /// + /// Adds the given shift to the frequency of every breakpoint in the clip, including the + /// emphasis. + /// + /// + /// 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); + } + } + } + + /// + /// Set the playback of a haptic clip to loop. + /// + /// + /// 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. + /// + /// If the value is true, looping will be enabled which results + /// in repeating the playback until Stop() is called; if false, the haptic + /// clip will only be played once. + public static void Loop(bool enabled) + { + if (Init()) + { + LofeltHaptics.Loop(enabled); + } + isLoopingEnabledByUser = enabled; + } + + /// + /// Checks if the loaded haptic clip is playing. + /// + /// + /// Whether the loaded clip is playing + public static bool IsPlaying() + { + if (playbackFinishedTimer.Enabled) + { + return true; + } + else + { + return isPlaybackLooping; + } + } + + /// + /// Stops playback and resets the playback state. + /// + /// + /// 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; + } + + /// + /// Processes an application focus change event. + /// + /// + /// 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. + /// + /// Whether the application now has focus + 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(); + } + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticController.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticController.cs.meta new file mode 100644 index 0000000..cd8f350 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eea19a9647af946678dbcea38129dd98 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticPatterns.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticPatterns.cs new file mode 100644 index 0000000..d900d38 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticPatterns.cs @@ -0,0 +1,514 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using System; +using UnityEngine; +using System.Globalization; + +namespace Lofelt.NiceVibrations +{ + /// + /// A collection of methods to play simple haptic patterns. + /// + /// + /// 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 }; + + /// + /// Enum that represents all the types of haptic presets available + /// + public enum PresetType + { + Selection = 0, + Success = 1, + Warning = 2, + Failure = 3, + LightImpact = 4, + MediumImpact = 5, + HeavyImpact = 6, + RigidImpact = 7, + SoftImpact = 8, + None = -1 + } + + /// + /// Structure that represents a haptic pattern with amplitude variations. + /// + /// + /// \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; + } + } + } + + /// + /// Predefined Preset that represents a "Selection" haptic preset + /// + internal static Preset Selection; + + /// + /// Predefined Preset that represents a "Light" haptic preset + /// + internal static Preset Light; + + /// + /// Predefined Preset that represents a "Medium" haptic preset + /// + internal static Preset Medium; + + /// + /// Predefined Preset that represents a "Heavy" haptic preset + /// + internal static Preset Heavy; + + /// + /// Predefined Preset that represents a "Rigid" haptic preset + /// + internal static Preset Rigid; + + /// + /// Predefined Preset that represents a "Soft" haptic preset + /// + internal static Preset Soft; + + /// + /// Predefined Preset that represents a "Success" haptic preset + /// + internal static Preset Success; + + /// + /// Predefined Preset that represents a "Failure" haptic preset + /// + internal static Preset Failure; + + /// + /// Predefined Preset that represents a "Warning" haptic preset + /// + 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 }); + } + + /// + /// Plays a single emphasis point. + /// + /// + /// 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. + /// + /// The amplitude of the emphasis, from 0.0 to 1.0 + /// The frequency of the emphasis, from 0.0 to 1.0 + 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 + } + } + + /// + /// Automatically selects the fallback preset based on the emphasis point amplitude. + /// + /// + /// The amplitude of the emphasis, from 0.0 to 1.0 + 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; + } + } + + /// + /// Plays a haptic with constant amplitude and frequency. + /// + /// + /// 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: + ///
    + ///
  • On iOS, it will play the preset HapticPatterns.PresetType.HeavyImpact.
  • + /// + ///
  • On Android, it will play a pattern with maximum amplitude for the set duration + /// since there is no amplitude control.
  • + /// + ///
+ /// Amplitude, from 0.0 to 1.0 + /// Frequency, from 0.0 to 1.0 + /// Play duration in seconds + 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; + } + + /// + /// Plays a set of predefined haptic patterns. + /// + /// + /// 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 .haptic clips (DeviceCapabilities.meetsAdvancedRequirements + /// is true) 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 .haptic clips (DeviceCapabilities.meetsAdvancedRequirements + /// is false), 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. + /// + /// Type of preset represented by a \ref PresetType enum + 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 + } + + /// + /// Returns the haptic preset duration. + /// + /// + /// While a preset is played back in different ways on iOS, Android and gamepads, the + /// duration is similar for each playback method. + /// + /// Type of preset represented by a \ref PresetType enum + /// Returns a float with a the preset duration; if the selected preset is `None`, it returns 0 + public static float GetPresetDuration(PresetType presetType) + { + if (presetType == PresetType.None) + { + return 0; + } + + return GetPresetForType(presetType).GetDuration(); + } + } + +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticPatterns.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticPatterns.cs.meta new file mode 100644 index 0000000..a11d0e6 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticPatterns.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e98a6cfb8386a479a8a5c3ded1f05862 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticReceiver.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticReceiver.cs new file mode 100644 index 0000000..e96ade1 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticReceiver.cs @@ -0,0 +1,108 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using UnityEngine; + +namespace Lofelt.NiceVibrations +{ + /// + /// A MonoBehaviour that forwards global properties from HapticController and + /// handles events + /// + /// + /// While HapticSource provides a per-clip MonoBehaviour API for the functionality + /// in HapticController, HapticReceiver provides a MonoBehaviour API for + /// the global functionality in HapticController. + /// + /// HapticReceiver is also responsible for global event handling, such as an application + /// focus change. To make this work correctly, your scene should have exactly one + /// HapticReceiver component, similar to how a scene should have exactly one + /// AudioListener. + /// + /// In the future HapticReceiver might receive parameters and distance to + /// HapticSource components, and can be used for global parameter control through Unity + /// Editor GUI. + [AddComponentMenu("Nice Vibrations/Haptic Receiver")] + public class HapticReceiver : MonoBehaviour, ISerializationCallbackReceiver + { + // These two fields are only used for serialization and deserialization. + // HapticController manages the output haptic level and global haptic toggle, + // HapticReceiver forwards these properties so they are available in a + // MonoBehaviour. + // To be able to serialize these properties, HapticReceiver needs to have + // fields for them. Before serialization, these fields are set to the values + // from HapticController, and after deserialization the values are restored + // back to HapticController. + [SerializeField] + [Range(0.0f, 5.0f)] + private float _outputLevel = 1.0f; + [SerializeField] + private bool _hapticsEnabled = true; + + /// + /// Loads all fields from HapticController. + /// + public void OnBeforeSerialize() + { + _outputLevel = HapticController._outputLevel; + _hapticsEnabled = HapticController._hapticsEnabled; + } + + /// + /// Writes all fields to HapticController. + /// + public void OnAfterDeserialize() + { + HapticController._outputLevel = _outputLevel; + HapticController._hapticsEnabled = _hapticsEnabled; + } + + /// + /// Forwarded HapticController::outputLevel + /// + [System.ComponentModel.DefaultValue(1.0f)] + public float outputLevel + { + get { return HapticController.outputLevel; } + set { HapticController.outputLevel = value; } + } + + + /// + /// Forwarded HapticController::hapticsEnabled + /// + [System.ComponentModel.DefaultValue(true)] + public bool hapticsEnabled + { + get { return HapticController.hapticsEnabled; } + set { HapticController.hapticsEnabled = value; } + } + + /// + /// Initializes HapticController. + /// + /// + /// This ensures that the initialization time is spent at startup instead of when + /// the first haptic is triggered during gameplay. + void Start() + { + HapticController.Init(); + } + + /// + /// Forwards an application focus change event to HapticController. + /// + void OnApplicationFocus(bool hasFocus) + { + HapticController.ProcessApplicationFocus(hasFocus); + } + + /// + /// Stops haptic playback on the gamepad when destroyed, to make sure the gamepad + /// stops vibrating when quitting the application. + /// + void OnDestroy() + { + GamepadRumbler.Stop(); + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticReceiver.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticReceiver.cs.meta new file mode 100644 index 0000000..8ba2383 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticReceiver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ceb29a83998eb4949bc0a9c8e5662fa1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 24c63d27288824cf68c83ec01e0f3643, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticSource.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticSource.cs new file mode 100644 index 0000000..ec118d9 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticSource.cs @@ -0,0 +1,262 @@ +// 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(); + } + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticSource.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticSource.cs.meta new file mode 100644 index 0000000..cdb65dc --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/HapticSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d20df93fb7de8457baa15a213a53ab19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7c1be57d46a3143daa1fe62dbc59772f, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons.meta new file mode 100644 index 0000000..0947622 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 729f668a205524058a62426043ee3083 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticReceiverIcon.png b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticReceiverIcon.png new file mode 100644 index 0000000..f088ee6 Binary files /dev/null and b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticReceiverIcon.png differ diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticReceiverIcon.png.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticReceiverIcon.png.meta new file mode 100644 index 0000000..43b7cc9 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticReceiverIcon.png.meta @@ -0,0 +1,92 @@ +fileFormatVersion: 2 +guid: 24c63d27288824cf68c83ec01e0f3643 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: -1 + wrapV: -1 + wrapW: -1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticSourceIcon.png b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticSourceIcon.png new file mode 100644 index 0000000..22683a9 Binary files /dev/null and b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticSourceIcon.png differ diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticSourceIcon.png.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticSourceIcon.png.meta new file mode 100644 index 0000000..7e56ba0 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Icons/HapticSourceIcon.png.meta @@ -0,0 +1,92 @@ +fileFormatVersion: 2 +guid: 7c1be57d46a3143daa1fe62dbc59772f +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: -1 + wrapV: -1 + wrapW: -1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/JNIHelpers.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/JNIHelpers.cs new file mode 100644 index 0000000..8bc7124 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/JNIHelpers.cs @@ -0,0 +1,135 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +#if (UNITY_ANDROID && !UNITY_EDITOR) + +using System; +using UnityEngine; + +namespace Lofelt.NiceVibrations +{ + // Android JNI call wrappers that are more efficient than AndroidJavaObject::Call() + // + // Calling a method via AndroidJavaObject, e.g. `lofeltHaptics.Call("play")`, is inefficient: + // - It looks up the method by name for each call + // - It allocates memory during method lookup and argument conversion + // + // JNIHelpers provides alternative Call() methods that are more efficient: + // - It allows calling by method ID rather by method name, so that the method only needs to + // be looked up once, not for every call + // - It does not allocate memory for converting the arguments to jvalue[] + // + // In addition to that, exceptions thrown in Java are handled automatically by logging them. + // + // The Call() overload here do not cover all cases that AndroidJavaObject::Call() covers. For + // example, only methods with one argument are supported, and that only for certain types. In + // addition, not all overloads are free of allocations. This however is good enough so that the + // calls triggered by common playback scenarios such as HapticController::Play() and + // HapticPatterns::PlayPreset() don't allocate. + internal static class JNIHelpers + { + // The array for the JNI arguments is created here, so that it doesn't need to be created + // for every call. This saves the allocation in each call. + // The array supports only methods with 0 or 1 argument, but that covers our needs. + static jvalue[] jniArgs = new jvalue[1]; + + // Returns an exception message and stack trace for the given Java exception + static String javaThrowableToString(IntPtr throwable) + { + IntPtr throwableClass = AndroidJNI.FindClass("java/lang/Throwable"); + IntPtr androidUtilLogClass = AndroidJNI.FindClass("android/util/Log"); + try + { + IntPtr toStringMethodId = AndroidJNI.GetMethodID(throwableClass, "toString", "()Ljava/lang/String;"); + IntPtr getStackTraceStringMethodId = AndroidJNI.GetStaticMethodID(androidUtilLogClass, "getStackTraceString", "(Ljava/lang/Throwable;)Ljava/lang/String;"); + string exceptionMessage = AndroidJNI.CallStringMethod(throwable, toStringMethodId, new jvalue[] { }); + jniArgs[0].l = throwable; + string exceptionCallStack = AndroidJNI.CallStaticStringMethod(androidUtilLogClass, getStackTraceStringMethodId, jniArgs); + return exceptionMessage + "\n" + exceptionCallStack; + } + finally + { + if (throwable != IntPtr.Zero) + AndroidJNI.DeleteLocalRef(throwable); + if (throwableClass != IntPtr.Zero) + AndroidJNI.DeleteLocalRef(throwableClass); + if (androidUtilLogClass != IntPtr.Zero) + AndroidJNI.DeleteLocalRef(androidUtilLogClass); + } + } + + public static void Call(AndroidJavaObject obj, IntPtr methodId, jvalue[] jniArgs) + { + if (methodId == IntPtr.Zero) + { + return; + } + + try + { + AndroidJNI.CallVoidMethod(obj.GetRawObject(), methodId, jniArgs); + IntPtr throwable = AndroidJNI.ExceptionOccurred(); + if (throwable != IntPtr.Zero) + { + AndroidJNI.ExceptionClear(); + String exception = javaThrowableToString(throwable); + Debug.LogError(exception); + } + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + + public static void Call(AndroidJavaObject obj, IntPtr methodId) + { + jniArgs[0].l = System.IntPtr.Zero; + Call(obj, methodId, jniArgs); + } + + public static void Call(AndroidJavaObject obj, IntPtr methodId, float arg) + { + jniArgs[0].f = arg; + Call(obj, methodId, jniArgs); + } + + public static void Call(AndroidJavaObject obj, IntPtr methodId, bool arg) + { + jniArgs[0].z = arg; + Call(obj, methodId, jniArgs); + } + + public static void Call(AndroidJavaObject obj, IntPtr methodId, float[] arg) + { + // The allocations in the next two lines could probably be removed to optimize this + // further. + object[] args = new object[] { arg }; + jvalue[] jniArgs = AndroidJNIHelper.CreateJNIArgArray(args); + try + { + JNIHelpers.Call(obj, methodId, jniArgs); + } + finally + { + AndroidJNIHelper.DeleteJNIArgArray(args, jniArgs); + } + } + + // The method isn't yet optimized to reduce allocations, but unlike the other overloads of + // Call(), it supports non-void return types. + public static ReturnType Call(AndroidJavaObject obj, string methodName) + { + try + { + return obj.Call(methodName); + } + catch (Exception ex) + { + Debug.LogException(ex); + return default(ReturnType); + } + } + + } +} +#endif \ No newline at end of file diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/JNIHelpers.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/JNIHelpers.cs.meta new file mode 100644 index 0000000..9926d84 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/JNIHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 309cd98b547c14b48b9f1c523a6fdc26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/LofeltHaptics.cs b/Assets/Scripts/Lofelt/NiceVibrations/Components/LofeltHaptics.cs new file mode 100644 index 0000000..404e62c --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/LofeltHaptics.cs @@ -0,0 +1,295 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using UnityEngine; +using System; + +#if (UNITY_ANDROID && !UNITY_EDITOR) +using System.Text; +using System.Runtime.InteropServices; +#elif (UNITY_IOS && !UNITY_EDITOR) +using UnityEngine.iOS; +using System.Runtime.InteropServices; +#endif + +namespace Lofelt.NiceVibrations +{ + /// + /// C# wrapper for the Lofelt Studio Android and iOS SDK. + /// + /// + /// You should not use this class directly, use HapticController instead, or the + /// MonoBehaviour classes HapticReceiver and HapticSource. + /// + /// The Lofelt Studio Android and iOS SDK are included in Nice Vibrations as pre-compiled + /// binary plugins. + /// + /// Each method here delegates to either the Android or iOS SDK. The methods should only be + /// called if DeviceMeetsMinimumPlatformRequirements() returns true, otherwise there will + /// be runtime errors. + /// + /// All the methods do nothing when running in the Unity editor. + /// + /// Before calling any other method, Initialize() needs to be called. + /// + /// Errors are printed and swallowed, no exceptions are thrown. On iOS, this happens inside + /// the SDK, on Android this happens with try/catch blocks in this class and in JNIHelpers. + public static class LofeltHaptics + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + static AndroidJavaObject lofeltHaptics; + static AndroidJavaObject hapticPatterns; + static long nativeController; + + // Cache the most commonly used JNI method IDs during initialization. + // Calling a Java method via its method ID is faster and uses less allocations than + // calling a method by string, like e.g. 'lofeltHaptics.Call("play")'. + static IntPtr playMethodId = IntPtr.Zero; + static IntPtr stopMethodId = IntPtr.Zero; + static IntPtr seekMethodId = IntPtr.Zero; + static IntPtr loopMethodId = IntPtr.Zero; + static IntPtr setAmplitudeMultiplicationMethodId = IntPtr.Zero; + static IntPtr playMaximumAmplitudePattern = IntPtr.Zero; + + [DllImport("lofelt_sdk")] + private static extern bool lofeltHapticsLoadDirect(IntPtr controller, [In] byte[] bytes, long size); + +#elif (UNITY_IOS && !UNITY_EDITOR) + // imports of iOS Framework bindings + + [DllImport("__Internal")] + private static extern bool lofeltHapticsDeviceMeetsMinimumRequirementsBinding(); + + [DllImport("__Internal")] + private static extern IntPtr lofeltHapticsInitBinding(); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsLoadBinding(IntPtr controller, [In] byte[] bytes, long size); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsPlayBinding(IntPtr controller); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsStopBinding(IntPtr controller); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsSeekBinding(IntPtr controller, float time); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsSetAmplitudeMultiplicationBinding(IntPtr controller, float factor); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsSetFrequencyShiftBinding(IntPtr controller, float shift); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsLoopBinding(IntPtr controller, bool enable); + + [DllImport("__Internal")] + private static extern float lofeltHapticsGetClipDurationBinding(IntPtr controller); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsReleaseBinding(IntPtr controller); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsSystemHapticsTriggerBinding(int type); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsSystemHapticsInitializeBinding(); + + [DllImport("__Internal")] + private static extern bool lofeltHapticsSystemHapticsReleaseBinding(); + + static IntPtr controller = IntPtr.Zero; + + static bool systemHapticsInitialized = false; +#endif + + /// + /// Initializes the iOS framework or Android library plugin. + /// + /// + /// This needs to be called before calling any other method. + public static void Initialize() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + try + { + using (var unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (var context = unityPlayerClass.GetStatic("currentActivity")) + { + lofeltHaptics = new AndroidJavaObject("com.lofelt.haptics.LofeltHaptics", context); + nativeController = lofeltHaptics.Call("getControllerHandle"); + hapticPatterns = new AndroidJavaObject("com.lofelt.haptics.HapticPatterns", context); + + playMethodId = AndroidJNIHelper.GetMethodID(lofeltHaptics.GetRawClass(), "play", "()V", false); + stopMethodId = AndroidJNIHelper.GetMethodID(lofeltHaptics.GetRawClass(), "stop", "()V", false); + seekMethodId = AndroidJNIHelper.GetMethodID(lofeltHaptics.GetRawClass(), "seek", "(F)V", false); + loopMethodId = AndroidJNIHelper.GetMethodID(lofeltHaptics.GetRawClass(), "loop", "(Z)V", false); + setAmplitudeMultiplicationMethodId = AndroidJNIHelper.GetMethodID(lofeltHaptics.GetRawClass(), "setAmplitudeMultiplication", "(F)V", false); + playMaximumAmplitudePattern = AndroidJNIHelper.GetMethodID(hapticPatterns.GetRawClass(), "playMaximumAmplitudePattern", "([F)V", false); + } + } + catch (Exception ex) + { + Debug.LogException(ex); + } +#elif (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsSystemHapticsInitializeBinding(); + systemHapticsInitialized = true; + controller = lofeltHapticsInitBinding(); +#endif + } + + /// + /// Releases the resources used by the iOS framework or Android library plugin. + /// + public static void Release() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + try + { + lofeltHaptics.Dispose(); + lofeltHaptics = null; + + hapticPatterns.Dispose(); + hapticPatterns = null; + } + catch (Exception ex) + { + Debug.LogWarning(ex); + } +#elif (UNITY_IOS && !UNITY_EDITOR) + if(DeviceCapabilities.isVersionSupported) { + lofeltHapticsSystemHapticsReleaseBinding(); + if(controller != IntPtr.Zero) { + lofeltHapticsReleaseBinding(controller); + controller = IntPtr.Zero; + } + } +#endif + } + + public static bool DeviceMeetsMinimumPlatformRequirements() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + return JNIHelpers.Call(lofeltHaptics, "deviceMeetsMinimumRequirements"); +#elif (UNITY_IOS && !UNITY_EDITOR) + return lofeltHapticsDeviceMeetsMinimumRequirementsBinding(); +#else + return true; +#endif + } + + public static void Load(byte[] data) + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + // For performance reasons, we do *not* call into the Java API with + // `lofeltHaptics.Call("load", data)` here. Instead, we bypass the Java layer and + // call into the native library directly, saving the costly conversion from + // C#'s byte[] to Java's byte[]. + // + // No exception handling needed here, lofeltHapticsLoadDirect() is a native method that + // doesn't throw an exception and instead logs the error. + lofeltHapticsLoadDirect((IntPtr)nativeController, data, data.Length); +#elif (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsLoadBinding(controller, data, data.Length); +#endif + } + + public static float GetClipDuration() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + return JNIHelpers.Call(lofeltHaptics, "getClipDuration"); +#elif (UNITY_IOS && !UNITY_EDITOR) + return lofeltHapticsGetClipDurationBinding(controller); +#else + //No haptic clip was loaded with Lofelt SDK, so it returns 0.0f + return 0.0f; +#endif + } + + public static void Play() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + JNIHelpers.Call(lofeltHaptics, playMethodId); +#elif (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsPlayBinding(controller); +#endif + } + + public static void PlayMaximumAmplitudePattern(float[] timings) + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + JNIHelpers.Call(hapticPatterns, playMaximumAmplitudePattern, timings); +#endif + } + + public static void Stop() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + JNIHelpers.Call(lofeltHaptics, stopMethodId); +#elif (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsStopBinding(controller); +#endif + } + + public static void StopPattern() + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + try + { + hapticPatterns.Call("stopPattern"); + } + catch (Exception ex) + { + Debug.LogWarning(ex); + } +#endif + } + + public static void Seek(float time) + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + JNIHelpers.Call(lofeltHaptics, seekMethodId, time); +#elif (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsSeekBinding(controller, time); +#endif + } + + public static void SetAmplitudeMultiplication(float factor) + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + JNIHelpers.Call(lofeltHaptics, setAmplitudeMultiplicationMethodId, factor); +#elif (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsSetAmplitudeMultiplicationBinding(controller, factor); +#endif + } + + public static void SetFrequencyShift(float shift) + { +#if (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsSetFrequencyShiftBinding(controller, shift); +#endif + } + + public static void Loop(bool enabled) + { +#if (UNITY_ANDROID && !UNITY_EDITOR) + JNIHelpers.Call(lofeltHaptics, loopMethodId, enabled); +#elif (UNITY_IOS && !UNITY_EDITOR) + lofeltHapticsLoopBinding(controller, enabled); +#endif + } + + public static void TriggerPresetHaptics(int type) + { +#if (UNITY_IOS && !UNITY_EDITOR) + if (!systemHapticsInitialized) + { + lofeltHapticsSystemHapticsInitializeBinding(); + systemHapticsInitialized = true; + } + lofeltHapticsSystemHapticsTriggerBinding(type); +#endif + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/LofeltHaptics.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/LofeltHaptics.cs.meta new file mode 100644 index 0000000..411bcc5 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/LofeltHaptics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 921537c8cf6464a24bd55f54ec8ea0d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources.meta new file mode 100644 index 0000000..657a720 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dfbca06fe23ec4830a998e7db8247870 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-constant-template.txt b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-constant-template.txt new file mode 100644 index 0000000..8b96dc6 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-constant-template.txt @@ -0,0 +1,33 @@ +{ + "version": { + "major": 1, + "minor": 0, + "patch": 0 + }, + "signals": { + "continuous": { + "envelopes": { + "amplitude": [ + { + "time": 0.0, + "amplitude": 1.0 + }, + { + "time": {duration}, + "amplitude": 1.0 + } + ], + "frequency": [ + { + "time": 0, + "frequency": 0.0 + }, + { + "time": {duration}, + "frequency": 0.0 + } + ] + } + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-constant-template.txt.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-constant-template.txt.meta new file mode 100644 index 0000000..e87f520 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-constant-template.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e4781704d93144109a483e1899b7cec6 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-emphasis-template.txt b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-emphasis-template.txt new file mode 100644 index 0000000..ae3b902 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-emphasis-template.txt @@ -0,0 +1,37 @@ +{ + "version": { + "major": 1, + "minor": 0, + "patch": 0 + }, + "signals": { + "continuous": { + "envelopes": { + "amplitude": [ + { + "time": 0.0, + "amplitude": 0.0, + "emphasis": { + "amplitude": {amplitude}, + "frequency": {frequency} + } + }, + { + "time": {duration}, + "amplitude": 0.0 + } + ], + "frequency": [ + { + "time": 0, + "frequency": 1.0 + }, + { + "time": {duration}, + "frequency": 1.0 + } + ] + } + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-emphasis-template.txt.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-emphasis-template.txt.meta new file mode 100644 index 0000000..dd5bc85 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-emphasis-template.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e1c679e9069854e06b54bb65f38215ff +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-pattern-template.txt b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-pattern-template.txt new file mode 100644 index 0000000..79ac208 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-pattern-template.txt @@ -0,0 +1,14 @@ +{ + "version": { + "major": 1, + "minor": 0, + "patch": 0 + }, + "signals": { + "continuous": { + "envelopes": { + "amplitude": [ {amplitude-envelope} ] + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-pattern-template.txt.meta b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-pattern-template.txt.meta new file mode 100644 index 0000000..db6e73c --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Components/Resources/nv-pattern-template.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cbb8181953d2a49a49f926ebcc75626c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Editor.meta b/Assets/Scripts/Lofelt/NiceVibrations/Editor.meta new file mode 100644 index 0000000..3633b33 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 290aee7a75d474f7985eaad9d87ac572 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticImporter.cs b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticImporter.cs new file mode 100644 index 0000000..85534b4 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticImporter.cs @@ -0,0 +1,119 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using System.IO; +using System.Runtime.InteropServices; +using System; +using UnityEngine; +using System.Text; + +#if UNITY_2020_2_OR_NEWER +using UnityEditor.AssetImporters; +#elif UNITY_2019_4_OR_NEWER +using UnityEditor.Experimental.AssetImporters; +#endif + +namespace Lofelt.NiceVibrations +{ + /// + /// Provides an importer for the HapticClip component. + /// + /// + /// The importer takes a .haptic file and converts it into a HapticClip. + [ScriptedImporter(version: 3, ext: "haptic", AllowCaching = true)] + public class HapticImporter : ScriptedImporter + { +#if !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + [DllImport("nice_vibrations_editor_plugin")] + private static extern IntPtr nv_plugin_convert_haptic_to_gamepad_rumble([In] byte[] bytes, long size); + + [DllImport("nice_vibrations_editor_plugin")] + private static extern void nv_plugin_destroy(IntPtr gamepadRumble); + + [DllImport("nice_vibrations_editor_plugin")] + private static extern UIntPtr nv_plugin_get_length(IntPtr gamepadRumble); + + [DllImport("nice_vibrations_editor_plugin")] + private static extern void nv_plugin_get_durations(IntPtr gamepadRumble, [Out] int[] durations); + + [DllImport("nice_vibrations_editor_plugin")] + private static extern void nv_plugin_get_low_frequency_motor_speeds(IntPtr gamepadRumble, [Out] float[] lowFrequencies); + + [DllImport("nice_vibrations_editor_plugin")] + private static extern void nv_plugin_get_high_frequency_motor_speeds(IntPtr gamepadRumble, [Out] float[] highFrequencies); + + // We can not use "[return: MarshalAs(UnmanagedType.LPUTF8Str)]" here, and have to use + // IntPtr for the return type instead. Otherwise, the C# runtime tries to free the returned + // string, which is invalid as the native plugin keeps ownership of the string. + // We use PtrToStringUTF8() to manually convert the IntPtr to a string instead. + [DllImport("nice_vibrations_editor_plugin")] + private static extern IntPtr nv_plugin_get_last_error(); + + [DllImport("nice_vibrations_editor_plugin")] + private static extern UIntPtr nv_plugin_get_last_error_length(); + + // Alternative to Marshal.PtrToStringUTF8() which was introduced in .NET 5 and isn't yet + // supported by Unity + private string PtrToStringUTF8(IntPtr ptr, int length) + { + byte[] bytes = new byte[length]; + Marshal.Copy(ptr, bytes, 0, length); + return Encoding.UTF8.GetString(bytes, 0, length); + } +#endif + + public override void OnImportAsset(AssetImportContext ctx) + { + // Load .haptic clip from file + var fileName = Path.GetFileNameWithoutExtension(ctx.assetPath); + var jsonBytes = File.ReadAllBytes(ctx.assetPath); + var hapticClip = HapticClip.CreateInstance(); + hapticClip.json = jsonBytes; + +#if !NICE_VIBRATIONS_DISABLE_GAMEPAD_SUPPORT + // Convert JSON to a GamepadRumble struct. The conversion algorithm is inside the native + // library nice_vibrations_editor_plugin. That plugin is only used in the Unity editor, and + // not at runtime. + GamepadRumble rumble = default; + IntPtr nativeRumble = nv_plugin_convert_haptic_to_gamepad_rumble(jsonBytes, jsonBytes.Length); + if (nativeRumble != IntPtr.Zero) + { + try + { + uint length = (uint)nv_plugin_get_length(nativeRumble); + rumble.durationsMs = new int[length]; + rumble.lowFrequencyMotorSpeeds = new float[length]; + rumble.highFrequencyMotorSpeeds = new float[length]; + + nv_plugin_get_durations(nativeRumble, rumble.durationsMs); + nv_plugin_get_low_frequency_motor_speeds(nativeRumble, rumble.lowFrequencyMotorSpeeds); + nv_plugin_get_high_frequency_motor_speeds(nativeRumble, rumble.highFrequencyMotorSpeeds); + + int totalDurationMs = 0; + foreach (int duration in rumble.durationsMs) + { + totalDurationMs += duration; + } + rumble.totalDurationMs = totalDurationMs; + } + finally + { + nv_plugin_destroy(nativeRumble); + } + } + else + { + var lastErrorPtr = nv_plugin_get_last_error(); + var lastErrorLength = (int)nv_plugin_get_last_error_length(); + var lastError = PtrToStringUTF8(lastErrorPtr, lastErrorLength); + Debug.LogWarning($"Failed to convert haptic clip {ctx.assetPath} to gamepad rumble: {lastError}"); + } + + hapticClip.gamepadRumble = rumble; +#endif + + // Use hapticClip as the imported asset + ctx.AddObjectToAsset("com.lofelt.HapticClip", hapticClip); + ctx.SetMainObject(hapticClip); + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticImporter.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticImporter.cs.meta new file mode 100644 index 0000000..e96189f --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc84fb4fa9e67485a972c887d976d004 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticSourceInspector.cs b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticSourceInspector.cs new file mode 100644 index 0000000..342face --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticSourceInspector.cs @@ -0,0 +1,155 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using System.IO; + +namespace Lofelt.NiceVibrations +{ + [CustomEditor(typeof(HapticSource))] + [CanEditMultipleObjects] + /// + /// Provides an inspector for the HapticSource component + /// + /// + /// The inspector lets you link a HapticSource to a HapticClip. + public class HapticSourceInspector : Editor + { + string hapticsDirectory; + + SerializedProperty hapticClip; + SerializedProperty priority; + SerializedProperty level; + SerializedProperty frequencyShift; + SerializedProperty loop; + SerializedProperty fallbackPreset; + + public static GUIContent hapticClipLabel = EditorGUIUtility.TrTextContent("Haptic Clip", "The HapticClip asset played by the HapticSource."); + public static GUIContent fallbackPresetLabel = EditorGUIUtility.TrTextContent("Haptic Preset fallback", "Set the haptic preset to play in case the device doesn't support playback of haptic clips"); + public static GUIContent loopLabel = EditorGUIUtility.TrTextContent("Loop", "Set the haptic source to loop playback of the haptic clip"); + + void OnEnable() + { + hapticClip = serializedObject.FindProperty("clip"); + priority = serializedObject.FindProperty("priority"); + level = serializedObject.FindProperty("_level"); + frequencyShift = serializedObject.FindProperty("_frequencyShift"); + fallbackPreset = serializedObject.FindProperty("_fallbackPreset"); + loop = serializedObject.FindProperty("_loop"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PropertyField(hapticClip, hapticClipLabel); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PropertyField(fallbackPreset, fallbackPresetLabel); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PropertyField(loop, loopLabel); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(); + + CreatePrioritySlider(); + CreateLevelSlider(); + CreateFrequencyShiftSlider(); + + serializedObject.ApplyModifiedProperties(); + } + + /// Helper function to create a priority slider for haptic source with High and Max text labels. + void CreatePrioritySlider() + { + Rect position = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight); + + EditorGUI.IntSlider(position, priority, 0, 256); + + // Move to next line + position.y += EditorGUIUtility.singleLineHeight; + + // Subtract the label + position.x += EditorGUIUtility.labelWidth; + position.width -= EditorGUIUtility.labelWidth; + + // Subtract the text field width thats drawn with slider + position.width -= EditorGUIUtility.fieldWidth; + + GUIStyle style = GUI.skin.label; + TextAnchor defaultAlignment = GUI.skin.label.alignment; + style.alignment = TextAnchor.UpperLeft; EditorGUI.LabelField(position, "High", style); + style.alignment = TextAnchor.UpperRight; EditorGUI.LabelField(position, "Low", style); + GUI.skin.label.alignment = defaultAlignment; + + // Allow space for the High/Low labels + EditorGUILayout.Space(); + EditorGUILayout.Space(); + EditorGUILayout.Space(); + } + + /// Helper function to create a level slider for haptic + /// source with labels. + void CreateLevelSlider() + { + Rect position = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight); + + EditorGUI.Slider(position, level, 0.0f, 5.0f); + + // Move to next line + position.y += EditorGUIUtility.singleLineHeight; + + // Subtract the label + position.x += EditorGUIUtility.labelWidth; + position.width -= EditorGUIUtility.labelWidth; + + // Subtract the text field width thats drawn with slider + position.width -= EditorGUIUtility.fieldWidth; + + GUIStyle style = GUI.skin.label; + TextAnchor defaultAlignment = GUI.skin.label.alignment; + style.alignment = TextAnchor.UpperLeft; EditorGUI.LabelField(position, "0.0", style); + style.alignment = TextAnchor.UpperRight; EditorGUI.LabelField(position, "5.0", style); + GUI.skin.label.alignment = defaultAlignment; + + // Allow space for the labels + EditorGUILayout.Space(); + EditorGUILayout.Space(); + EditorGUILayout.Space(); + } + + /// Helper function to create a frequency shift slider for haptic + /// source with labels. + void CreateFrequencyShiftSlider() + { + Rect position = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight); + + EditorGUI.Slider(position, frequencyShift, -1.0f, 1.0f); + + // Move to next line + position.y += EditorGUIUtility.singleLineHeight; + + // Subtract the label + position.x += EditorGUIUtility.labelWidth; + position.width -= EditorGUIUtility.labelWidth; + + // Subtract the text field width thats drawn with slider + position.width -= EditorGUIUtility.fieldWidth; + + GUIStyle style = GUI.skin.label; + TextAnchor defaultAlignment = GUI.skin.label.alignment; + style.alignment = TextAnchor.UpperLeft; EditorGUI.LabelField(position, "-1.0", style); + style.alignment = TextAnchor.UpperRight; EditorGUI.LabelField(position, "1.0", style); + GUI.skin.label.alignment = defaultAlignment; + + // Allow space for the labels + EditorGUILayout.Space(); + EditorGUILayout.Space(); + EditorGUILayout.Space(); + } + } +} diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticSourceInspector.cs.meta b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticSourceInspector.cs.meta new file mode 100644 index 0000000..4b89d54 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Editor/HapticSourceInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 90a030b5ab0574cd9880e136f5e0261c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Editor/Lofelt.NiceVibrations.Editor.asmdef b/Assets/Scripts/Lofelt/NiceVibrations/Editor/Lofelt.NiceVibrations.Editor.asmdef new file mode 100644 index 0000000..01980f9 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Editor/Lofelt.NiceVibrations.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Lofelt.NiceVibrations.Editor", + "rootNamespace": "", + "references": [ + "GUID:7399982fb54df4bb981f9e015a651afa" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Editor/Lofelt.NiceVibrations.Editor.asmdef.meta b/Assets/Scripts/Lofelt/NiceVibrations/Editor/Lofelt.NiceVibrations.Editor.asmdef.meta new file mode 100644 index 0000000..eb588a4 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Editor/Lofelt.NiceVibrations.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 67bc5fafbf62b48858241ce814d3d489 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Lofelt.NiceVibrations.asmdef b/Assets/Scripts/Lofelt/NiceVibrations/Lofelt.NiceVibrations.asmdef new file mode 100644 index 0000000..2ba2225 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Lofelt.NiceVibrations.asmdef @@ -0,0 +1,21 @@ +{ + "name": "Lofelt.NiceVibrations", + "references": [ + "GUID:75469ad4d38634e559750d17036d5f7c" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.unity.inputsystem", + "expression": "1.0", + "define": "NICE_VIBRATIONS_INPUTSYSTEM_INSTALLED" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/Lofelt/NiceVibrations/Lofelt.NiceVibrations.asmdef.meta b/Assets/Scripts/Lofelt/NiceVibrations/Lofelt.NiceVibrations.asmdef.meta new file mode 100644 index 0000000..ca1cec9 --- /dev/null +++ b/Assets/Scripts/Lofelt/NiceVibrations/Lofelt.NiceVibrations.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7399982fb54df4bb981f9e015a651afa +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/OCES/Haptic/Handwritten.meta b/Assets/Scripts/OCES/Haptic/Handwritten.meta new file mode 100644 index 0000000..093bc83 --- /dev/null +++ b/Assets/Scripts/OCES/Haptic/Handwritten.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9e1d12770ca114030b35c14f8c72b193 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/OCES/Haptic/Handwritten/HandwrittenDefinitions.cs b/Assets/Scripts/OCES/Haptic/Handwritten/HandwrittenDefinitions.cs new file mode 100644 index 0000000..f3579d5 --- /dev/null +++ b/Assets/Scripts/OCES/Haptic/Handwritten/HandwrittenDefinitions.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace OCES.Haptic +{ + public interface IBinarySerializable + { + void DeSerialize(BinaryReader reader); + void Serialize(BinaryWriter writer); + } + + public enum HapticType + { + Preset = 0, //播放预制 + Transient = 1, //播放瞬时 + Continuous = 2, //播放连续 + Advance = 3, //播放文件 + } +} diff --git a/Assets/Scripts/OCES/Haptic/Handwritten/HandwrittenDefinitions.cs.meta b/Assets/Scripts/OCES/Haptic/Handwritten/HandwrittenDefinitions.cs.meta new file mode 100644 index 0000000..e1e56a1 --- /dev/null +++ b/Assets/Scripts/OCES/Haptic/Handwritten/HandwrittenDefinitions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d7267d262bcf45e0ba1855dccdc5e87e +timeCreated: 1775705326 \ No newline at end of file diff --git a/Assets/Scripts/OCES/Haptic/HapticSystem.cs b/Assets/Scripts/OCES/Haptic/Handwritten/HapticSystem.cs similarity index 100% rename from Assets/Scripts/OCES/Haptic/HapticSystem.cs rename to Assets/Scripts/OCES/Haptic/Handwritten/HapticSystem.cs diff --git a/Assets/Scripts/OCES/Haptic/HapticSystem.cs.meta b/Assets/Scripts/OCES/Haptic/Handwritten/HapticSystem.cs.meta similarity index 100% rename from Assets/Scripts/OCES/Haptic/HapticSystem.cs.meta rename to Assets/Scripts/OCES/Haptic/Handwritten/HapticSystem.cs.meta diff --git a/HapticSystem.sln.DotSettings b/HapticSystem.sln.DotSettings new file mode 100644 index 0000000..f6019ad --- /dev/null +++ b/HapticSystem.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Packages/manifest.json b/Packages/manifest.json index 9e4493a..0cb9b7e 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -2,7 +2,7 @@ "dependencies": { "com.unity.collab-proxy": "2.12.4", "com.unity.feature.development": "1.0.1", - "com.unity.textmeshpro": "3.0.7", + "com.unity.textmeshpro": "3.0.9", "com.unity.timeline": "1.7.7", "com.unity.ugui": "1.0.0", "com.unity.visualscripting": "1.9.4", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index a21f4d7..ccde727 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -96,7 +96,7 @@ "url": "https://packages.unity.com" }, "com.unity.textmeshpro": { - "version": "3.0.7", + "version": "3.0.9", "depth": 0, "source": "registry", "dependencies": {