// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- 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 { /// /// 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. /// [RequireComponent(typeof(UxrGrabbableObject))] public partial class UxrHapticOnImpact : UxrGrabbableObjectComponent { #region Inspector Properties/Serialized Fields [Header("General")] [SerializeField] private List _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 /// /// Event triggered when the component detects a collision between any hit point and a collider. /// public event EventHandler Hit; /// /// Gets the hit point transforms. /// public IEnumerable HitPoints => _hitPoints; /// /// Gets the total number of times something was hit. /// public int TotalHitCount { get; private set; } #endregion #region Public Methods /// /// Applies an explosive force to a rigidbody as a result of a hit. /// /// The rigidbody to apply a force to /// Event parameters /// Explosive force applied to the rigidbody 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 /// /// Initializes internal data. /// protected override void Awake() { base.Awake(); CreateHitPointInfo(); } /// /// Subscribes to events and re-initializes data. /// 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; } }); } /// /// Unsubscribes from events. /// protected override void OnDisable() { base.OnDisable(); UxrManager.AvatarsUpdated -= UxrManager_AvatarsUpdated; } #endregion #region Event Handling Methods /// /// Called after avatars are updated. Tries to find objects that were hit. /// 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(); 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(); if (receiver) { receiver.OnHit(this, eventArgs); } } } } } foreach (HitPointInfo hitPointInfo in _hitPointInfos) { hitPointInfo.LastPos = hitPointInfo.HitPoint.position; } } #endregion #region Event Trigger Methods /// /// Event trigger for the event. /// /// Event parameters private void OnHit(UxrHapticImpactEventArgs e) { TotalHitCount++; Hit?.Invoke(this, e); } #endregion #region Private Methods /// /// Creates the hit point information list. /// private void CreateHitPointInfo() { foreach (Transform hitPoint in _hitPoints) { _hitPointInfos.Add(new HitPointInfo(hitPoint)); } } #endregion #region Private Types & Data /// /// The number of frames to sample velocity to average. /// private const int VelocityAverageSamples = 3; private readonly List _hitPointInfos = new List(); #endregion } } #pragma warning restore 414