course_heia_techimm_haptics.../Assets/Resources/Scripts/StepsLink/HapticManager.cs

345 lines
13 KiB
C#
Raw Permalink Normal View History

2025-07-12 18:59:18 +02:00
using UnityEngine;
using Bhaptics.SDK2;
using System;
using NaughtyAttributes;
namespace Tools.Bhaptics
{
/// <summary>
/// Class that manages haptic feedback for the step link.
/// </summary>
/// <remarks>
/// This class is implemented as a singleton. Use <see cref="GetInstance"/> to obtain the instance of the <see cref="HapticManager"/>.
/// </remarks>
/// <remarks>
/// This class is not thread-safe; however, it should not pose a problem for this use case. The <see cref="BhapticsLibrary"/>
/// appears to be thread-safe while playing the haptic feedback.
/// </remarks>
/// <example>
/// <code>
/// // Get the instance of the HapticManager
/// HapticManager hapticManager = HapticManager.GetInstance;
///
/// // Play the haptic feedback for the start of the game
/// hapticManager.PlayStartGame();
/// </code>
/// </example>
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";
/// <summary>
/// Haptic feedback for the start of the game
/// </summary>
private static readonly string STEP_LINK_GAMESTART = BhapticsEvent.GAME_START;
/// <summary>
/// Haptic feedback for the step link update of the haptic feedback
/// </summary>
private static readonly string STEP_LINK_EFFECT = BhapticsEvent.STEP_LINK;
/// <summary>
/// Haptic feedback for the step link update of the haptic feedback when the user is looking precisely at the object
/// </summary>
private static readonly string STEP_LINK_EFFECT_PERFECT = BhapticsEvent.HAPTIC_STEP_LINK_PERFECT;
/// <summary>
/// Angle in degrees for the perfect look at the object
/// </summary>
private static readonly float ANGLE_FOR_PERFECTION = 10f;
/// <summary>
/// The camera of the player, used as center of the reference system
/// </summary>
[Tooltip("The camera of the player, used as center of the reference system")]
[Required]
public Camera playerCamera;
/// <summary>
/// Minimum distance starting decreasing the intensity of the haptic feedback
/// </summary>
[Tooltip("Minimum distance starting decreasing the intensity of the haptic feedback")]
[MinValue(0.0f)]
public float minDistance_meters = 4f;
/// <summary>
/// Refresh rate of the distance / angle calculation and haptic feedback in seconds
/// </summary>
[Tooltip("Refresh rate of the distance / angle calculation and haptic feedback in seconds")]
[MinValue(0.2f)]
public float refreshRate = 0.3f;
/// <summary>
/// If true, when precisely looking at the object, the haptic feedback on head will be used intead
/// </summary>
[Header("Use Haptic Tactal")]
[Tooltip("If true, when precisely looking at the object, use the tactal")]
public bool headForPreciseLook = true;
/// <summary>
/// The camera of the player, used as center of the reference system
/// </summary>
public float MinDistance_meters
{
get
{
return minDistance_meters;
}
set
{
if (value >= 0)
{
minDistance_meters = value;
}
else
{
throw new Exception(EXC_MIN_DISTANCE);
}
}
}
/// <summary>
/// Refresh rate of the distance / angle calculation and haptic feedback in seconds
/// </summary>
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);
}
}
/// <summary>
/// Play the haptic feedback intended for the start of the game
/// </summary>
public void PlayStartGame()
{
BhapticsLibrary.Play(STEP_LINK_GAMESTART);
}
/// <summary>
/// Play the haptic feedback of the fast heartbeat for the seconds specified
/// </summary>
public void PlayHeartBeatFast()
{
if (!BhapticsLibrary.IsPlayingByEventId(BhapticsEvent.HEARTBEAT_FAST))
{
BhapticsLibrary.PlayParam(BhapticsEvent.HEARTBEAT_FAST,
intensity: 1f,
duration: 1f,
angleX: 0f,
offsetY: 0f);
}
}
/// <summary>
/// Stop the haptic feedback of the heartbeat
/// </summary>
public void StopHeartBeatFast()
{
BhapticsLibrary.StopByEventId(BhapticsEvent.HEARTBEAT_FAST);
}
/// <summary>
/// Play the haptic feedback of the fast heartbeat for the seconds specified
/// </summary>
public void PlayHeartBeat()
{
if (!BhapticsLibrary.IsPlayingByEventId(BhapticsEvent.HEARTBEAT))
{
BhapticsLibrary.PlayParam(BhapticsEvent.HEARTBEAT,
intensity: 1f,
duration: 1f,
angleX: 0f,
offsetY: 0f);
}
}
/// <summary>
/// Stop the haptic feedback of the heartbeat
/// </summary>
public void StopHeartBeat()
{
BhapticsLibrary.StopByEventId(BhapticsEvent.HEARTBEAT_FAST);
}
/// <summary>
/// Play the haptic feedback of the fbomb for the seconds specified
/// </summary>
public void PlayBomb(GameObject gameObject)
{
BhapticsLibrary.PlayParam(BhapticsEvent.EXPLOSION,
intensity: 2f,
duration: 1f,
angleX: ComputeXZAngle(playerCamera.gameObject, gameObject),
offsetY: 0f);
}
/// <summary>
/// Stop all the haptic feedback
/// </summary>
public void StopAll()
{
BhapticsLibrary.StopAll();
}
/// <summary>
/// Get XZ angle between the camera and the target
/// The XZ is the horizontal plane
/// </summary>
/// <param name="camera"></param>
/// <param name="target"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Compute the angle between the camera and the target on the YZ plane
/// The YZ is the vertical plane going forward from the camera
/// </summary>
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);
}
/// <summary>
/// Compute the distance between the camera and the target
/// </summary>
/// <param name="camera"></param>
/// <param name="target"></param>
/// <returns></returns>
private float ComputeDistance(GameObject camera, GameObject target)
{
float distance = Vector3.Distance(camera.transform.position, target.transform.position);
return distance;
}
/// <summary>
/// Compute the intensity of the haptic feedback based on the distance between the camera and the target
/// </summary>
/// <param name="camera"></param>
/// <param name="target"></param>
/// <returns></returns>
private float ComputeIntensitiy(GameObject camera, GameObject target)
{
float distance = ComputeDistance(camera, target);
float intensity = distance > minDistance_meters ? 1 : distance /= minDistance_meters;
return intensity;
}
/// <summary>
/// Compute the offsetY of the haptic feedback based on the angleYZ between the camera and the target
/// </summary>
/// <param name="angleYZ"></param>
/// <returns></returns>
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
/// <summary>
/// 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
/// </summary>
/// <param name="linkedObject"></param>
/// <returns></returns>
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);
}
}
}
}