using UnityEngine; using Bhaptics.SDK2; using System; using NaughtyAttributes; namespace Tools.Bhaptics { /// /// Class that manages haptic feedback for the step link. /// /// /// This class is implemented as a singleton. Use to obtain the instance of the . /// /// /// This class is not thread-safe; however, it should not pose a problem for this use case. The /// appears to be thread-safe while playing the haptic feedback. /// /// /// /// // Get the instance of the HapticManager /// HapticManager hapticManager = HapticManager.GetInstance; /// /// // Play the haptic feedback for the start of the game /// hapticManager.PlayStartGame(); /// /// public class HapticManager : MonoBehaviour { // Const strings private const string EXC_NOT_INIT = "HapticManager not initialized, call Initialize first"; private const string LOG_ALREADY_INIT = "HapticManager already initialized"; private const string EXC_CAMERA_NULL = "PlayerCamera not set."; private const string EXC_MIN_DISTANCE = "MinDistance_meters must be greater than 0 meters"; private const string EXC_REFRATE_MIN = "RefreshRate (in seconds) must be greater than 0.1"; private const string LOG_HIGH_REFRESHRATE = "RefreshRate is high, consider using a lower value"; private const string LOG_INIT_DONE = "HapticManager initialized"; /// /// Haptic feedback for the start of the game /// private static readonly string STEP_LINK_GAMESTART = BhapticsEvent.GAME_START; /// /// Haptic feedback for the step link update of the haptic feedback /// private static readonly string STEP_LINK_EFFECT = BhapticsEvent.STEP_LINK; /// /// Haptic feedback for the step link update of the haptic feedback when the user is looking precisely at the object /// private static readonly string STEP_LINK_EFFECT_PERFECT = BhapticsEvent.HAPTIC_STEP_LINK_PERFECT; /// /// Angle in degrees for the perfect look at the object /// private static readonly float ANGLE_FOR_PERFECTION = 10f; /// /// The camera of the player, used as center of the reference system /// [Tooltip("The camera of the player, used as center of the reference system")] [Required] public Camera playerCamera; /// /// Minimum distance starting decreasing the intensity of the haptic feedback /// [Tooltip("Minimum distance starting decreasing the intensity of the haptic feedback")] [MinValue(0.0f)] public float minDistance_meters = 4f; /// /// Refresh rate of the distance / angle calculation and haptic feedback in seconds /// [Tooltip("Refresh rate of the distance / angle calculation and haptic feedback in seconds")] [MinValue(0.2f)] public float refreshRate = 0.3f; /// /// If true, when precisely looking at the object, the haptic feedback on head will be used intead /// [Header("Use Haptic Tactal")] [Tooltip("If true, when precisely looking at the object, use the tactal")] public bool headForPreciseLook = true; /// /// The camera of the player, used as center of the reference system /// public float MinDistance_meters { get { return minDistance_meters; } set { if (value >= 0) { minDistance_meters = value; } else { throw new Exception(EXC_MIN_DISTANCE); } } } /// /// Refresh rate of the distance / angle calculation and haptic feedback in seconds /// public float RefreshRate { get { return refreshRate; } set { if (value > 0.1f) { refreshRate = value; } else { throw new Exception(EXC_REFRATE_MIN); } } } private void Awake() { if (playerCamera == null) { throw new Exception(EXC_CAMERA_NULL); } else if (minDistance_meters < 0.1) { throw new Exception(EXC_MIN_DISTANCE); } else if (refreshRate <= 0.1) { throw new Exception(EXC_REFRATE_MIN); } else if (refreshRate >= 5) { Debug.LogWarning(LOG_HIGH_REFRESHRATE); } } /// /// Play the haptic feedback intended for the start of the game /// public void PlayStartGame() { BhapticsLibrary.Play(STEP_LINK_GAMESTART); } /// /// Play the haptic feedback of the fast heartbeat for the seconds specified /// public void PlayHeartBeatFast() { if (!BhapticsLibrary.IsPlayingByEventId(BhapticsEvent.HEARTBEAT_FAST)) { BhapticsLibrary.PlayParam(BhapticsEvent.HEARTBEAT_FAST, intensity: 1f, duration: 1f, angleX: 0f, offsetY: 0f); } } /// /// Stop the haptic feedback of the heartbeat /// public void StopHeartBeatFast() { BhapticsLibrary.StopByEventId(BhapticsEvent.HEARTBEAT_FAST); } /// /// Play the haptic feedback of the fast heartbeat for the seconds specified /// public void PlayHeartBeat() { if (!BhapticsLibrary.IsPlayingByEventId(BhapticsEvent.HEARTBEAT)) { BhapticsLibrary.PlayParam(BhapticsEvent.HEARTBEAT, intensity: 1f, duration: 1f, angleX: 0f, offsetY: 0f); } } /// /// Stop the haptic feedback of the heartbeat /// public void StopHeartBeat() { BhapticsLibrary.StopByEventId(BhapticsEvent.HEARTBEAT_FAST); } /// /// Play the haptic feedback of the fbomb for the seconds specified /// public void PlayBomb(GameObject gameObject) { BhapticsLibrary.PlayParam(BhapticsEvent.EXPLOSION, intensity: 2f, duration: 1f, angleX: ComputeXZAngle(playerCamera.gameObject, gameObject), offsetY: 0f); } /// /// Stop all the haptic feedback /// public void StopAll() { BhapticsLibrary.StopAll(); } /// /// Get XZ angle between the camera and the target /// The XZ is the horizontal plane /// /// /// /// private float ComputeXZAngle(GameObject camera, GameObject target) { Vector2 cameraPositionXZ = new(camera.transform.position.x, camera.transform.position.z); Vector2 targetPositionFromCameraXZ = new(target.transform.position.x, target.transform.position.z); Vector2 cameraToNextObjectXZDirection = targetPositionFromCameraXZ - cameraPositionXZ; cameraToNextObjectXZDirection.Normalize(); Vector2 cameraDirectionXZ = new(camera.transform.forward.x, camera.transform.forward.z); return Vector2.SignedAngle(cameraDirectionXZ, cameraToNextObjectXZDirection); } /// /// Compute the angle between the camera and the target on the YZ plane /// The YZ is the vertical plane going forward from the camera /// private float ComputeYZAngle(GameObject camera, GameObject target) { //plane of the, create 2 vectors to use for the angleXZ calculation Vector2 cameraPositionYZ = new(camera.transform.position.y, camera.transform.position.z); Vector2 targetPositionFromCameraYZ = new(target.transform.position.y, target.transform.position.z); Vector2 cameraToNextObjectYZDirection = targetPositionFromCameraYZ - cameraPositionYZ; cameraToNextObjectYZDirection.Normalize(); Vector2 cameraDirectionYZ = new(camera.transform.forward.y, camera.transform.forward.z); return Vector2.SignedAngle(cameraDirectionYZ, cameraToNextObjectYZDirection); } /// /// Compute the distance between the camera and the target /// /// /// /// private float ComputeDistance(GameObject camera, GameObject target) { float distance = Vector3.Distance(camera.transform.position, target.transform.position); return distance; } /// /// Compute the intensity of the haptic feedback based on the distance between the camera and the target /// /// /// /// private float ComputeIntensitiy(GameObject camera, GameObject target) { float distance = ComputeDistance(camera, target); float intensity = distance > minDistance_meters ? 1 : distance /= minDistance_meters; return intensity; } /// /// Compute the offsetY of the haptic feedback based on the angleYZ between the camera and the target /// /// /// private float ComputeOffsetY(float angleYZ) { float offsetY = angleYZ / 180f; if (offsetY > 0.5f) offsetY = 0.5f; else if (offsetY < -0.5f) offsetY = -0.5f; return -offsetY; } // This method plays the haptic feedback based on the distance and angleXZ between // the headset and the parameter object /// /// Play the haptic feedback based on the distance and angleXZ between the headset and the parameter object /// also the offsetY is calculated based on the angleYZ between the headset and the parameter object /// its possible to use the head for precise look, in this case the haptic feedback on the head will be used /// /// /// public void PlayDirectionalLink(GameObject linkedObject) { float angleXZ = ComputeXZAngle(playerCamera.gameObject, linkedObject); float angleYZ = ComputeYZAngle(playerCamera.gameObject, linkedObject); BhapticsLibrary.StopAll(); // if the user is looking at the object, play head haptic feedback, otherwise play vest haptic feedback if (headForPreciseLook && Mathf.Abs(angleXZ) < ANGLE_FOR_PERFECTION && Mathf.Abs(angleYZ) < ANGLE_FOR_PERFECTION) { BhapticsLibrary.PlayParam(STEP_LINK_EFFECT_PERFECT, intensity: 0.5f, duration: RefreshRate, angleX: 0f, offsetY: 0f); // Debug.Log("HEAD: Distance: " + 0 + " m, angleXZ: " + 0 + "°, intensity: " + 0.1f + " duration: " + RefreshRate + " s, offsetY: " + 0f); } else { float intensity = ComputeIntensitiy(playerCamera.gameObject, linkedObject); float offsetY = ComputeOffsetY(angleYZ); // intensity: 1f, The value multiplied by the original value // duration: 1f, The value multiplied by the original value // angleX: 20f, The value that rotates around global Vector3.up(0~360f) // offsetY: 0.3f, // The value to move up and down(-0.5~0.5) BhapticsLibrary.PlayParam(STEP_LINK_EFFECT, intensity: intensity, duration: RefreshRate, angleX: angleXZ, offsetY: offsetY); // Debug.Log("SUIT: Distance: " + distance + " m, angleXZ: " + angleXZ + "°, intensity: " + intensity + " duration: " + RefreshRate + " s, offsetY: " + offsetY); } } } }