345 lines
13 KiB
C#
345 lines
13 KiB
C#
|
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);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|