Files
dungeons/Assets/UltimateXR/Runtime/Scripts/Haptics/Helpers/UxrHapticOnImpact.cs
2024-08-06 21:58:35 +02:00

296 lines
12 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrHapticOnImpact.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using UltimateXR.Avatar;
using UltimateXR.Core;
using UltimateXR.Core.Components.Composite;
using UltimateXR.Extensions.Unity;
using UltimateXR.Manipulation;
using UnityEngine;
#pragma warning disable 414 // Disable warnings due to unused values
namespace UltimateXR.Haptics.Helpers
{
/// <summary>
/// Component that plays haptic clips on the VR controllers whenever certain points hit colliders.
/// This enables to model haptic functionality like hitting walls with a hammer and similar.
/// </summary>
[RequireComponent(typeof(UxrGrabbableObject))]
public partial class UxrHapticOnImpact : UxrGrabbableObjectComponent<UxrHapticOnImpact>
{
#region Inspector Properties/Serialized Fields
[Header("General")] [SerializeField] private List<Transform> _hitPoints;
[SerializeField] private LayerMask _collisionLayers = ~0;
[SerializeField] [Range(0, 180)] private float _forwardAngleThreshold = 30.0f;
[SerializeField] [Range(0, 180)] private float _surfaceAngleThreshold = 30.0f;
[SerializeField] private float _minSpeed = 0.01f;
[SerializeField] private float _maxSpeed = 1.0f;
[Header("Haptics")] [SerializeField] private UxrHapticClipType _hapticClip = UxrHapticClipType.Shot;
[SerializeField] private UxrHapticMode _hapticMode = UxrHapticMode.Mix;
[SerializeField] private float _hapticPulseDurationMin = 0.05f;
[SerializeField] private float _hapticPulseDurationMax = 0.05f;
[SerializeField] [Range(0, 1)] private float _hapticPulseAmplitudeMin = 0.2f;
[SerializeField] [Range(0, 1)] private float _hapticPulseAmplitudeMax = 1.0f;
[Header("Physics")] [SerializeField] private float _minHitForce = 1.0f;
[SerializeField] private float _maxHitForce = 100.0f;
#endregion
#region Public Types & Data
/// <summary>
/// Event triggered when the component detects a collision between any hit point and a collider.
/// </summary>
public event EventHandler<UxrHapticImpactEventArgs> Hit;
/// <summary>
/// Gets the hit point transforms.
/// </summary>
public IEnumerable<Transform> HitPoints => _hitPoints;
/// <summary>
/// Gets the total number of times something was hit.
/// </summary>
public int TotalHitCount { get; private set; }
#endregion
#region Public Methods
/// <summary>
/// Applies an explosive force to a rigidbody as a result of a hit.
/// </summary>
/// <param name="rigidbody">The rigidbody to apply a force to</param>
/// <param name="eventArgs">Event parameters</param>
/// <param name="force">Explosive force applied to the rigidbody</param>
public static void ApplyBreakExplosionForce(Rigidbody rigidbody, UxrHapticImpactEventArgs eventArgs, float force)
{
if (rigidbody != null)
{
rigidbody.AddForceAtPosition(eventArgs.Velocity.normalized * force, eventArgs.HitInfo.point);
rigidbody.AddTorque(eventArgs.Velocity.normalized * force, ForceMode.Impulse);
}
}
#endregion
#region Unity
/// <summary>
/// Initializes internal data.
/// </summary>
protected override void Awake()
{
base.Awake();
CreateHitPointInfo();
}
/// <summary>
/// Subscribes to events and re-initializes data.
/// </summary>
protected override void OnEnable()
{
base.OnEnable();
UxrManager.AvatarsUpdated += UxrManager_AvatarsUpdated;
_hitPointInfos.ForEach(p =>
{
p.LastPos = p.HitPoint.position;
for (int i = 0; i < VelocityAverageSamples; ++i)
{
p.VelocitySamples[i] = Vector3.zero;
}
});
}
/// <summary>
/// Unsubscribes from events.
/// </summary>
protected override void OnDisable()
{
base.OnDisable();
UxrManager.AvatarsUpdated -= UxrManager_AvatarsUpdated;
}
#endregion
#region Event Handling Methods
/// <summary>
/// Called after avatars are updated. Tries to find objects that were hit.
/// </summary>
private void UxrManager_AvatarsUpdated()
{
if (isActiveAndEnabled == false)
{
return;
}
if (Time.deltaTime > 0.0f)
{
foreach (HitPointInfo hitPointInfo in _hitPointInfos)
{
// Update velocity frame history
Vector3 currentFrameVelocity = (hitPointInfo.HitPoint.position - hitPointInfo.LastPos) / Time.deltaTime;
for (int i = 0; i < hitPointInfo.VelocitySamples.Count - 1; ++i)
{
hitPointInfo.VelocitySamples[i] = hitPointInfo.VelocitySamples[i + 1];
}
hitPointInfo.VelocitySamples[hitPointInfo.VelocitySamples.Count - 1] = currentFrameVelocity;
// Average history to compute current velocity
hitPointInfo.Velocity = Vector3.zero;
for (int i = 0; i < hitPointInfo.VelocitySamples.Count - 1; ++i)
{
hitPointInfo.Velocity += hitPointInfo.VelocitySamples[i];
}
hitPointInfo.Velocity = hitPointInfo.Velocity / hitPointInfo.VelocitySamples.Count;
}
}
foreach (HitPointInfo hitPointInfo in _hitPointInfos)
{
float speed = hitPointInfo.Velocity.magnitude;
// Check if we are grabbing the object and moving it with enough speed
if (UxrGrabManager.Instance && UxrGrabManager.Instance.GetGrabbingHand(GrabbableObject, out bool isLeft, out bool isRight) && speed > _minSpeed && speed > 0.0f)
{
Vector3 direction = hitPointInfo.HitPoint.position - hitPointInfo.LastPos;
// Raycast between the previous and current frame positions
RaycastHit[] hits = Physics.RaycastAll(hitPointInfo.LastPos, direction, direction.magnitude, _collisionLayers, QueryTriggerInteraction.Ignore);
foreach (RaycastHit hitInfo in hits)
{
// Avoid self collision first
if (hitInfo.collider.transform.HasParent(transform))
{
continue;
}
// We hit something! get the normalized force 0 = min, 1 = max
float forceT = Mathf.Clamp01((speed - _minSpeed) / (_maxSpeed - _minSpeed));
// Compute angles (forward motion angle and surface angle)
float forwardVelocityAngle = Vector3.Angle(hitPointInfo.Velocity, hitPointInfo.HitPoint.forward);
float surfaceAngle = Vector3.Angle(direction, -hitInfo.normal);
// Below thresholds to trigger event?
if (forwardVelocityAngle <= _forwardAngleThreshold && surfaceAngle <= _surfaceAngleThreshold)
{
// Yes
UxrHapticImpactEventArgs eventArgs = new UxrHapticImpactEventArgs(hitInfo,
forceT,
hitPointInfo.Velocity,
forwardVelocityAngle,
Vector3.Angle(hitPointInfo.HitPoint.forward, -hitInfo.normal));
// Apply physics to the other object if it is dynamic
Rigidbody otherRigidbody = hitInfo.collider.GetComponent<Rigidbody>();
if (otherRigidbody && !otherRigidbody.isKinematic)
{
ApplyBreakExplosionForce(otherRigidbody, eventArgs, Mathf.Lerp(_minHitForce, _maxHitForce, eventArgs.ForceT));
}
// Send haptic feedback
if (UxrAvatar.LocalAvatarInput)
{
float amplitude = Mathf.Lerp(_hapticPulseAmplitudeMin, _hapticPulseAmplitudeMax, forceT);
float duration = Mathf.Lerp(_hapticPulseDurationMin, _hapticPulseDurationMax, forceT);
if (isLeft)
{
UxrAvatar.LocalAvatarInput.SendHapticFeedback(UxrHandSide.Left, _hapticClip, amplitude, duration, _hapticMode);
}
if (isRight)
{
UxrAvatar.LocalAvatarInput.SendHapticFeedback(UxrHandSide.Right, _hapticClip, amplitude, duration, _hapticMode);
}
OnHit(eventArgs);
}
// Check if there is a receiver component to send the event
UxrHapticImpactReceiver receiver = hitInfo.collider.GetComponentInParent<UxrHapticImpactReceiver>();
if (receiver)
{
receiver.OnHit(this, eventArgs);
}
}
}
}
}
foreach (HitPointInfo hitPointInfo in _hitPointInfos)
{
hitPointInfo.LastPos = hitPointInfo.HitPoint.position;
}
}
#endregion
#region Event Trigger Methods
/// <summary>
/// Event trigger for the <see cref="Hit" /> event.
/// </summary>
/// <param name="e">Event parameters</param>
private void OnHit(UxrHapticImpactEventArgs e)
{
TotalHitCount++;
Hit?.Invoke(this, e);
}
#endregion
#region Private Methods
/// <summary>
/// Creates the hit point information list.
/// </summary>
private void CreateHitPointInfo()
{
foreach (Transform hitPoint in _hitPoints)
{
_hitPointInfos.Add(new HitPointInfo(hitPoint));
}
}
#endregion
#region Private Types & Data
/// <summary>
/// The number of frames to sample velocity to average.
/// </summary>
private const int VelocityAverageSamples = 3;
private readonly List<HitPointInfo> _hitPointInfos = new List<HitPointInfo>();
#endregion
}
}
#pragma warning restore 414