Files
dungeons/Assets/ThirdParty/UltimateXR/Runtime/Scripts/Manipulation/Helpers/UxrAutoSlideInObject.cs

369 lines
15 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrAutoSlideInObject.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Linq;
using UltimateXR.Animation.Interpolation;
using UltimateXR.Avatar;
using UltimateXR.Core;
using UltimateXR.Core.Components.Composite;
using UltimateXR.Core.Math;
using UltimateXR.Core.Settings;
using UltimateXR.Extensions.Unity;
using UltimateXR.Haptics.Helpers;
using UnityEngine;
namespace UltimateXR.Manipulation.Helpers
{
/// <summary>
/// Component that, together with <see cref="UxrAutoSlideInAnchor" /> will add the following behaviour to a
/// <see cref="UxrGrabbableObject" />:
/// <list type="bullet">
/// <item>
/// It will slide along the axis given by the grabbable object translation constraint. The constraint should
/// be pre-configured along a single axis.
/// </item>
/// <item>
/// It will be smoothly removed from the anchor and made free if dragged beyond the upper translation
/// constraint.
/// </item>
/// <item>It will be smoothly placed automatically on the anchor when moved back close enough.</item>
/// <item>It will fall back by itself when released while sliding along the axis.</item>
/// </list>
/// </summary>
public partial class UxrAutoSlideInObject : UxrGrabbableObjectComponent<UxrAutoSlideInObject>
{
[SerializeField] private Vector3 _translationConstraintMin = Vector3.zero;
[SerializeField] private Vector3 _translationConstraintMax = Vector3.forward * 0.1f;
#region Public Types & Data
/// <summary>
/// Event called right after the object hit the end after sliding in after it was released.
/// </summary>
public event Action PlacedAfterSlidingIn;
#endregion
#region Unity
/// <summary>
/// Subscribes to the avatars updated event so that the manipulation logic is done after all manipulation
/// logic has been updated.
/// </summary>
protected override void OnEnable()
{
base.OnEnable();
UxrManager.AvatarsUpdated += UxrManager_AvatarsUpdated;
}
/// <summary>
/// Unsubscribes from the avatars updated event.
/// </summary>
protected override void OnDisable()
{
base.OnDisable();
UxrManager.AvatarsUpdated -= UxrManager_AvatarsUpdated;
}
/// <summary>
/// Initialize component.
/// </summary>
protected override void Start()
{
base.Start();
_placedAfterSlidingIn = GrabbableObject.CurrentAnchor != null;
// Get slide axis
int insertAxis = GrabbableObject.SingleTranslationAxisIndex;
if (insertAxis == -1 || GrabbableObject.TranslationConstraint != UxrTranslationConstraintMode.RestrictLocalOffset)
{
if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.ManipulationModule} {this}: {nameof(UxrGrabbableObject)} component needs to have a local offset translation constraint along a single axis to work properly");
}
_insertAxis = UxrAxis.Z;
}
else
{
_insertAxis = insertAxis;
}
// Store haptic feedback component in case it exists, to disable it while the object is out of the sliding zone
_manipulationHapticFeedback = GetComponent<UxrManipulationHapticFeedback>();
// Compute the slide length
_insertOffset = _translationConstraintMax[_insertAxis] > -_translationConstraintMin[_insertAxis] ? _translationConstraintMax[_insertAxis] : _translationConstraintMin[_insertAxis];
_insertOffsetSign = Mathf.Sign(_insertOffset);
_insertOffset = Mathf.Abs(_insertOffset);
// Fix some object parameters if we need to
if (GrabbableObject.DropSnapMode != UxrSnapToAnchorMode.DontSnap)
{
if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings)
{
Debug.LogWarning($"{UxrConstants.ManipulationModule} {this.GetPathUnderScene()}: GrabbableObject needs DropSnapMode to be DontSnap in order to work properly. Overriding.");
}
GrabbableObject.DropSnapMode = UxrSnapToAnchorMode.DontSnap;
}
GrabbableObject.IsPlaceable = false; // We will handle placement ourselves
// Compute the object size in local coordinates
_objectLocalSize = gameObject.GetLocalBounds(true).size;
}
#endregion
#region Event Handling Methods
/// <summary>
/// Called after UltimateXR has done all the frame updating. Does the manipulation logic.
/// </summary>
private void UxrManager_AvatarsUpdated()
{
bool grabbedByLocalAvatar = UxrGrabManager.Instance.IsBeingGrabbed(GrabbableObject) && UxrGrabManager.Instance.GetGrabbingHands(GrabbableObject).First().Avatar.AvatarMode == UxrAvatarMode.Local;
if (GrabbableObject.CurrentAnchor == null && grabbedByLocalAvatar)
{
// The object is being grabbed and is detached. Check if we need to place it on an anchor again by proximity.
foreach (UxrAutoSlideInAnchor anchor in UxrAutoSlideInAnchor.EnabledComponents.Where(a => a.Anchor.enabled))
{
// If it is inside the valid release "volume", place it in the anchor again and let it slide by re-assigning the constraints
if (anchor.Anchor.CurrentPlacedObject == null && anchor.Anchor.IsCompatibleObject(GrabbableObject) && IsObjectNearPlacement(anchor.Anchor))
{
AttachObject(anchor);
return;
}
}
}
if (GrabbableObject.CurrentAnchor != null && _insertAxis != null)
{
// Object can only move in a specific axis but if it is grabbed past this distance it becomes free
if (transform.parent != null && grabbedByLocalAvatar && Mathf.Abs(GrabbableObject.InitialLocalPosition[_insertAxis] - transform.localPosition[_insertAxis]) > _insertOffset * 0.99f)
{
DetachObject();
return;
}
// If it is not being grabbed it will slide in
if (!GrabbableObject.IsBeingGrabbed)
{
// Use simple gravity to slide in. Gravity will be mapped to z axis to slide in our local coordinate system.
Vector3 speed = Physics.gravity * _slideInTimer;
Vector3 pos = GrabbableObject.transform.localPosition;
pos[_insertAxis] += Time.deltaTime * speed.y * _insertOffsetSign;
if ((_insertOffsetSign > 0.0f && pos[_insertAxis] < GrabbableObject.InitialLocalPosition[_insertAxis]) ||
(_insertOffsetSign < 0.0f && pos[_insertAxis] > GrabbableObject.InitialLocalPosition[_insertAxis]))
{
pos[_insertAxis] = GrabbableObject.InitialLocalPosition[_insertAxis];
if (_placedAfterSlidingIn == false)
{
_placedAfterSlidingIn = true;
OnPlacedAfterSlidingIn();
}
}
// Interpolate other rotation/translation in case the object was released before the transition
float smooth = 0.1f;
pos[_insertAxis.Perpendicular] = UxrInterpolator.SmoothDamp(pos[_insertAxis.Perpendicular], GrabbableObject.InitialLocalPosition[_insertAxis.Perpendicular], smooth);
pos[_insertAxis.OtherPerpendicular] = UxrInterpolator.SmoothDamp(pos[_insertAxis.OtherPerpendicular], GrabbableObject.InitialLocalPosition[_insertAxis.OtherPerpendicular], smooth);
GrabbableObject.transform.localRotation = UxrInterpolator.SmoothDampRotation(GrabbableObject.transform.localRotation, GrabbableObject.InitialLocalRotation, smooth);
// Update
GrabbableObject.transform.localPosition = pos;
_slideInTimer += Time.deltaTime;
}
else
{
_slideInTimer = 0.0f;
}
}
}
#endregion
#region Event Trigger Methods
/// <summary>
/// Called when the object was placed at the end sliding in after it was released.
/// Use in child classes to
/// </summary>
protected virtual void OnPlacedAfterSlidingIn()
{
PlacedAfterSlidingIn?.Invoke();
}
/// <summary>
/// Called right after the object was grabbed.
/// </summary>
/// <param name="e">Event parameters</param>
protected override void OnObjectGrabbed(UxrManipulationEventArgs e)
{
if (!GrabbableObject.IsLockedInPlace)
{
_placedAfterSlidingIn = false;
}
}
/// <summary>
/// Called right after the object was released.
/// </summary>
/// <param name="e">Event parameters</param>
protected override void OnObjectReleased(UxrManipulationEventArgs e)
{
if (e.GrabbableObject.CurrentAnchor != null && e.GrabbableObject.RigidBodySource)
{
// Force kinematic while released, so that we update the position/rotation.
e.GrabbableObject.RigidBodySource.isKinematic = true;
}
}
#endregion
#region Protected Overrides UxrGrabbableObjectComponent<UxrAutoSlideInObject>
/// <inheritdoc />
protected override bool IsGrabbableObjectRequired => true;
#endregion
#region Protected Methods
/// <summary>
/// Attaches the object to the anchor and assigns constraints to let it slide.
/// </summary>
protected void AttachObject(UxrAutoSlideInAnchor anchor)
{
// This method will be synchronized through network
BeginSync();
// Set up constraints and place
GrabbableObject.TranslationConstraint = UxrTranslationConstraintMode.RestrictLocalOffset;
GrabbableObject.RotationConstraint = UxrRotationConstraintMode.Locked;
GrabbableObject.TranslationLimitsMin = _translationConstraintMin;
GrabbableObject.TranslationLimitsMax = _translationConstraintMax;
UxrGrabManager.Instance.PlaceObject(GrabbableObject, anchor.Anchor, UxrPlacementOptions.Smooth | UxrPlacementOptions.DontRelease, true);
if (_manipulationHapticFeedback)
{
_manipulationHapticFeedback.MinAmplitude = _minHapticAmplitude;
_manipulationHapticFeedback.MaxAmplitude = _maxHapticAmplitude;
}
EndSyncMethod(new object[] { anchor });
}
/// <summary>
/// Detaches the object from the anchor so that it becomes free.
/// </summary>
protected void DetachObject()
{
// This method will be synchronized through network
BeginSync();
if (_manipulationHapticFeedback)
{
_minHapticAmplitude = _manipulationHapticFeedback.MinAmplitude;
_maxHapticAmplitude = _manipulationHapticFeedback.MaxAmplitude;
_manipulationHapticFeedback.MinAmplitude = 0.0f;
_manipulationHapticFeedback.MaxAmplitude = 0.0f;
}
UxrGrabManager.Instance.RemoveObjectFromAnchor(GrabbableObject, true);
GrabbableObject.TranslationConstraint = UxrTranslationConstraintMode.Free;
GrabbableObject.RotationConstraint = UxrRotationConstraintMode.Free;
EndSyncMethod();
}
#endregion
#region Private Methods
/// <summary>
/// Checks whether the object is close enough to the given anchor to be placed.
/// </summary>
/// <param name="anchor">Object anchor</param>
/// <returns>Whether the object is close enough</returns>
private bool IsObjectNearPlacement(UxrGrabbableObjectAnchor anchor)
{
if (anchor.enabled == false)
{
return false;
}
// Is it near enough in the longitudinal axis?
float threshold = Mathf.Min(0.03f, Mathf.Abs(_insertOffset * 0.1f));
Vector3 localOffset = anchor.AlignTransform.InverseTransformPoint(GrabbableObject.DropAlignTransform.position) - GrabbableObject.InitialLocalPosition;
bool isInLongitudinalAxisRange = (_insertOffsetSign > 0.0f && localOffset[_insertAxis] < +_insertOffset - threshold && localOffset[_insertAxis] > 0.0f) ||
(_insertOffsetSign < 0.0f && localOffset[_insertAxis] > -_insertOffset + threshold && localOffset[_insertAxis] < 0.0f);
// Is it near enough in both other axes?
float minGrabDistance = float.MaxValue;
foreach (UxrGrabber grabber in UxrGrabManager.Instance.GetGrabbingHands(GrabbableObject))
{
UxrGrabPointInfo grabPointInfo = GrabbableObject.GetGrabPoint(UxrGrabManager.Instance.GetGrabbedPoint(grabber));
if (grabPointInfo.MaxDistanceGrab < minGrabDistance)
{
minGrabDistance = grabPointInfo.MaxDistanceGrab;
}
}
// We use some calculations for the other axes so that it feels good.
float sizeOneAxis = Mathf.Min(Mathf.Max(_objectLocalSize[_insertAxis.Perpendicular], 0.1f), minGrabDistance);
float sizeOtherAxis = Mathf.Min(Mathf.Max(_objectLocalSize[_insertAxis.OtherPerpendicular], 0.1f), minGrabDistance);
// Return conditions
return isInLongitudinalAxisRange && Mathf.Abs(localOffset[_insertAxis.Perpendicular]) < sizeOneAxis && Mathf.Abs(localOffset[_insertAxis.OtherPerpendicular]) < sizeOtherAxis;
}
#endregion
#region Private Types & Data
private UxrAxis _insertAxis;
private float _insertOffset;
private float _insertOffsetSign;
private Vector3 _objectLocalSize;
private float _slideInTimer;
private bool _placedAfterSlidingIn;
private UxrManipulationHapticFeedback _manipulationHapticFeedback;
private float _minHapticAmplitude;
private float _maxHapticAmplitude;
#endregion
}
}