Move third party assets to ThirdParty folder

This commit is contained in:
2024-08-08 11:26:28 +02:00
parent 386f303057
commit bd91af6f98
10340 changed files with 100 additions and 175 deletions

View File

@@ -0,0 +1,94 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrAlignOnRelease.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System.Collections.Generic;
using UltimateXR.Animation.Interpolation;
using UltimateXR.Core.Components;
using UnityEngine;
namespace UltimateXR.Manipulation.Helpers
{
/// <summary>
/// Aligns an object smoothly whenever it is released to keep it leveled. Should be used on non physics-driven
/// grabbable objects, which remain floating in the air when being released.
/// </summary>
public class UxrAlignOnRelease : UxrComponent
{
#region Inspector Properties/Serialized Fields
[SerializeField] private bool _onlyLevel = true;
[SerializeField] [Range(0.0f, 1.0f)] private float _smoothFactor = 0.2f;
[SerializeField] private List<UxrGrabbableObject> _grabbableObjects;
#endregion
#region Unity
/// <summary>
/// Caches the transform component.
/// </summary>
protected override void Awake()
{
base.Awake();
_selfTransform = transform;
}
/// <summary>
/// Updates the transform while the object is not being grabbed.
/// </summary>
private void Update()
{
if (!IsBeingGrabbed)
{
// Smoothly rotate towards horizontal orientation when not being grabbed
if (_onlyLevel == false)
{
_selfTransform.rotation = UxrInterpolator.SmoothDampRotation(_selfTransform.rotation, Quaternion.FromToRotation(_selfTransform.up, Vector3.up) * _selfTransform.rotation, _smoothFactor);
}
else
{
Vector3 projectedRight = Vector3.ProjectOnPlane(transform.right, Vector3.up);
Quaternion targetRotation = Quaternion.FromToRotation(_selfTransform.right, projectedRight) * _selfTransform.rotation;
if ((targetRotation * Vector3.up).y < 0.0f)
{
targetRotation = targetRotation * Quaternion.AngleAxis(180.0f, Vector3.forward);
}
_selfTransform.rotation = UxrInterpolator.SmoothDampRotation(_selfTransform.rotation, targetRotation, _smoothFactor);
}
}
}
#endregion
#region Private Types & Data
/// <summary>
/// Gets whether the object is being grabbed using any of the registered grabbable objects.
/// </summary>
private bool IsBeingGrabbed
{
get
{
foreach (UxrGrabbableObject grabbableObject in _grabbableObjects)
{
if (UxrGrabManager.Instance.IsBeingGrabbed(grabbableObject))
{
return true;
}
}
return false;
}
}
private Transform _selfTransform;
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1dd4099f01318e34680aafb12edd7b4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,61 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrAutoSlideInAnchor.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UltimateXR.Core.Components;
using UnityEngine;
namespace UltimateXR.Manipulation.Helpers
{
/// <summary>
/// Anchor component for <see cref="UxrAutoSlideInAnchor" />. Grabbable objects with the
/// <see cref="UxrAutoSlideInObject" /> component will automatically attach/detach from this anchor.
/// </summary>
[RequireComponent(typeof(UxrGrabbableObjectAnchor))]
public class UxrAutoSlideInAnchor : UxrComponent<UxrAutoSlideInAnchor>
{
#region Public Types & Data
/// <summary>
/// Gets the anchor.
/// </summary>
public UxrGrabbableObjectAnchor Anchor
{
get
{
if (_anchor == null)
{
_anchor = GetComponent<UxrGrabbableObjectAnchor>();
}
return _anchor;
}
}
#endregion
#region Unity
/// <summary>
/// Initializes the component.
/// </summary>
protected override void Awake()
{
base.Awake();
if (_anchor == null)
{
_anchor = GetComponent<UxrGrabbableObjectAnchor>();
}
}
#endregion
#region Private Types & Data
private UxrGrabbableObjectAnchor _anchor;
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0e85a05d37194014d8a60f3f2f72936a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrAutoSlideInObject.StateSave.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UltimateXR.Core.StateSave;
namespace UltimateXR.Manipulation.Helpers
{
public partial class UxrAutoSlideInObject
{
#region Protected Overrides UxrComponent
/// <inheritdoc />
protected override void SerializeState(bool isReading, int stateSerializationVersion, UxrStateSaveLevel level, UxrStateSaveOptions options)
{
base.SerializeState(isReading, stateSerializationVersion, level, options);
// Manipulations are already handled through events, we don't serialize them in incremental changes
if (level > UxrStateSaveLevel.ChangesSincePreviousSave)
{
SerializeStateValue(level, options, nameof(_insertAxis), ref _insertAxis);
SerializeStateValue(level, options, nameof(_insertOffset), ref _insertOffset);
SerializeStateValue(level, options, nameof(_insertOffsetSign), ref _insertOffsetSign);
SerializeStateValue(level, options, nameof(_objectLocalSize), ref _objectLocalSize);
SerializeStateValue(level, options, nameof(_slideInTimer), ref _slideInTimer);
SerializeStateValue(level, options, nameof(_placedAfterSlidingIn), ref _placedAfterSlidingIn);
SerializeStateValue(level, options, nameof(_manipulationHapticFeedback), ref _manipulationHapticFeedback);
SerializeStateValue(level, options, nameof(_minHapticAmplitude), ref _minHapticAmplitude);
SerializeStateValue(level, options, nameof(_maxHapticAmplitude), ref _maxHapticAmplitude);
}
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be3cda6fb2a64942a98f487a1e810b67
timeCreated: 1706041007

View File

@@ -0,0 +1,369 @@
// --------------------------------------------------------------------------------------------------------------------
// <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
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7264c3b787447c4596209ea4b950a39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,100 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrDependentGrabbable.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UltimateXR.Core.Components.Composite;
using UnityEngine;
namespace UltimateXR.Manipulation.Helpers
{
/// <summary>
/// Component that allows an object be grabbed only if another object is being grabbed. For instance, it can
/// be added to a grenade pin to make sure the pin is never grabbed unless the grenade is being grabbed too.
/// Otherwise the pin could be removed by mistake when trying to grab the grenade.
/// </summary>
public class UxrDependentGrabbable : UxrGrabbableObjectComponent<UxrDependentGrabbable>
{
#region Inspector Properties/Serialized Fields
[SerializeField] private UxrGrabbableObject _dependentOn;
[SerializeField] private bool _onlyOnce;
#endregion
#region Public Types & Data
/// <summary>
/// Gets or sets the grabbable object the component depends on.
/// </summary>
public UxrGrabbableObject DependentFrom
{
get => _dependentOn;
set => _dependentOn = value;
}
/// <summary>
/// Whether to stop toggling the enabled state once the dependent object was grabbed. For instance, a grenade pin
/// should remain grabbable once it has been removed from the grenade, no matter if the grenade is being grabbed or
/// not at that point.
/// </summary>
public bool OnlyOnce
{
get => _onlyOnce;
set => _onlyOnce = value;
}
#endregion
#region Unity
/// <summary>
/// Initializes the grabbable object state.
/// </summary>
protected override void Start()
{
base.Start();
GrabbableObject.enabled = false;
}
/// <summary>
/// Updates the grabbable object state.
/// </summary>
private void Update()
{
if (GrabbableObject && DependentFrom && _check)
{
GrabbableObject.enabled = UxrGrabManager.Instance.IsBeingGrabbed(DependentFrom);
if (GrabbableObject.enabled && OnlyOnce)
{
_check = false;
}
}
}
#endregion
#region Event Trigger Methods
/// <summary>
/// Called whenever the object was grabbed.
/// </summary>
/// <param name="e">Event parameters</param>
protected override void OnObjectGrabbed(UxrManipulationEventArgs e)
{
base.OnObjectGrabbed(e);
_check = false;
}
#endregion
#region Private Types & Data
private bool _check = true;
#endregion
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 873afa6bf9451c3479e294d279b19ed9
timeCreated: 1492963941
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrGrabbableResizable.StateSave.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UltimateXR.Core.StateSave;
namespace UltimateXR.Manipulation.Helpers
{
public sealed partial class UxrGrabbableResizable
{
#region Protected Overrides UxrComponent
/// <inheritdoc />
protected override void SerializeState(bool isReading, int stateSerializationVersion, UxrStateSaveLevel level, UxrStateSaveOptions options)
{
base.SerializeState(isReading, stateSerializationVersion, level, options);
// Manipulations are already handled through events, we don't serialize them in incremental changes
if (level > UxrStateSaveLevel.ChangesSincePreviousSave)
{
SerializeStateValue(level, options, nameof(_grabbingCount), ref _grabbingCount);
SerializeStateValue(level, options, nameof(_grabbedCount), ref _grabbedCount);
}
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 75671c44324f466baec19cfc69507c7b
timeCreated: 1706004361

View File

@@ -0,0 +1,421 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrGrabbableResizable.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections;
using UltimateXR.Avatar;
using UltimateXR.Core;
using UltimateXR.Core.Components;
using UltimateXR.Extensions.Unity;
using UnityEngine;
#pragma warning disable 67 // Disable warnings due to unused events
namespace UltimateXR.Manipulation.Helpers
{
/// <summary>
/// <para>
/// Component that allows an object to be scaled by grabbing it by both sides and moving them closer or apart.
/// The hierarchy should be as follows:
/// </para>
/// <code>
/// -Root GameObject: With UxrGrabbableResizable and UxrGrabbableObject component.
/// | The UxrGrabbableObject is a dummy grabbable parent that enables moving
/// | this root by grabbing the child extensions. It can also have its own
/// | grab points but they are not required.
/// |---Root resizable: Object that will be scaled when the two extensions are moved.
/// |---Grabbable left: Left grabbable extension with locked rotation and translation
/// | constrained to sliding it left-right.
/// |---Grabbable right: Right grabbable extension with locked rotation and translation
/// constrained to sliding it left-right.
/// </code>
/// All objects should use an axis system with x right, y up and z forward.
/// </summary>
public sealed partial class UxrGrabbableResizable : UxrComponent, IUxrGrabbable
{
#region Inspector Properties/Serialized Fields
[Header("General")] [SerializeField] private Transform _resizableRoot;
[SerializeField] private float _startScale = 1.0f;
[Header("Grabbing")] [SerializeField] private UxrGrabbableObject _grabbableRoot;
[SerializeField] private UxrGrabbableObject _grabbableExtendLeft;
[SerializeField] private UxrGrabbableObject _grabbableExtendRight;
[Header("Haptics")] [SerializeField] [Range(0.0f, 1.0f)] private float _hapticsIntensity = 0.1f;
#endregion
#region Public Types & Data
/// <summary>
/// Gets the <see cref="Transform" /> that is going to be scaled when the two grabbable objects are moved apart.
/// </summary>
public Transform ResizableRoot => _resizableRoot;
/// <summary>
/// Gets the root grabbable object.
/// </summary>
public UxrGrabbableObject GrabbableRoot => _grabbableRoot;
/// <summary>
/// Gets the left grabbable extension.
/// </summary>
public UxrGrabbableObject GrabbableExtendLeft => _grabbableExtendLeft;
/// <summary>
/// Gets the right grabbable extension.
/// </summary>
public UxrGrabbableObject GrabbableExtendRight => _grabbableExtendRight;
#endregion
#region Implicit IUxrGrabbable
/// <inheritdoc />
public bool IsBeingGrabbed => GrabbableRoot.IsBeingGrabbed || GrabbableExtendLeft.IsBeingGrabbed || GrabbableExtendRight.IsBeingGrabbed;
/// <inheritdoc />
public bool IsGrabbable
{
get => GrabbableRoot.IsGrabbable || GrabbableExtendLeft.IsGrabbable || GrabbableExtendRight.IsGrabbable;
set
{
BeginSync();
GrabbableRoot.IsGrabbable = value;
GrabbableExtendLeft.IsGrabbable = value;
GrabbableExtendRight.IsGrabbable = value;
EndSyncProperty(value);
}
}
/// <inheritdoc />
public bool IsKinematic
{
get => GrabbableRoot.IsKinematic || GrabbableExtendLeft.IsKinematic || GrabbableExtendRight.IsKinematic;
set
{
BeginSync();
GrabbableRoot.IsKinematic = value;
GrabbableExtendLeft.IsKinematic = value;
GrabbableExtendRight.IsKinematic = value;
EndSyncProperty(value);
}
}
/// <inheritdoc />
public event EventHandler<UxrManipulationEventArgs> Grabbing;
/// <inheritdoc />
public event EventHandler<UxrManipulationEventArgs> Grabbed;
/// <inheritdoc />
public event EventHandler<UxrManipulationEventArgs> Releasing;
/// <inheritdoc />
public event EventHandler<UxrManipulationEventArgs> Released;
/// <inheritdoc />
public event EventHandler<UxrManipulationEventArgs> Placing;
/// <inheritdoc />
public event EventHandler<UxrManipulationEventArgs> Placed;
/// <inheritdoc />
public void ResetPositionAndState(bool propagateEvents)
{
// This method will be synchronized through network
BeginSync();
ReleaseGrabs(true);
GrabbableRoot.ResetPositionAndState(propagateEvents);
GrabbableExtendLeft.ResetPositionAndState(propagateEvents);
GrabbableExtendRight.ResetPositionAndState(propagateEvents);
UpdateResizableScale();
EndSyncMethod(new object[] { propagateEvents });
}
/// <inheritdoc />
public void ReleaseGrabs(bool propagateEvents)
{
// This method will be synchronized through network
BeginSync();
GrabbableRoot.ReleaseGrabs(propagateEvents);
GrabbableExtendLeft.ReleaseGrabs(propagateEvents);
GrabbableExtendRight.ReleaseGrabs(propagateEvents);
_grabbingCount = 0;
_grabbedCount = 0;
EndSyncMethod(new object[] { propagateEvents });
}
#endregion
#region Unity
/// <summary>
/// Initializes the component.
/// </summary>
protected override void Awake()
{
base.Awake();
_initialGrabsSeparation = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position);
_initialResizableLocalScale = _resizableRoot.transform.localScale;
_separationToBoundsFactor = _initialGrabsSeparation / _resizableRoot.gameObject.GetLocalBounds(true).size.x;
}
/// <summary>
/// Subscribes to relevant events.
/// </summary>
protected override void OnEnable()
{
base.OnEnable();
UxrManager.AvatarsUpdated += UxrManager_AvatarsUpdated;
GrabbableRoot.Grabbing += Grabbable_Grabbing;
GrabbableRoot.Grabbed += Grabbable_Grabbed;
GrabbableRoot.Releasing += Grabbable_Releasing;
GrabbableRoot.Released += Grabbable_Released;
GrabbableExtendLeft.Grabbing += Grabbable_Grabbing;
GrabbableExtendLeft.Grabbed += Grabbable_Grabbed;
GrabbableExtendLeft.Releasing += Grabbable_Releasing;
GrabbableExtendLeft.Released += Grabbable_Released;
GrabbableExtendRight.Grabbed += Grabbable_Grabbed;
GrabbableExtendRight.Grabbing += Grabbable_Grabbing;
GrabbableExtendRight.Releasing += Grabbable_Releasing;
GrabbableExtendRight.Released += Grabbable_Released;
_hapticsCoroutine = StartCoroutine(HapticsCoroutine());
}
/// <summary>
/// Unsubscribes from relevant events.
/// </summary>
protected override void OnDisable()
{
base.OnDisable();
UxrManager.AvatarsUpdated -= UxrManager_AvatarsUpdated;
GrabbableRoot.Grabbing -= Grabbable_Grabbing;
GrabbableRoot.Grabbed -= Grabbable_Grabbed;
GrabbableRoot.Releasing -= Grabbable_Releasing;
GrabbableRoot.Released -= Grabbable_Released;
GrabbableExtendLeft.Grabbing -= Grabbable_Grabbing;
GrabbableExtendLeft.Grabbed -= Grabbable_Grabbed;
GrabbableExtendLeft.Releasing -= Grabbable_Releasing;
GrabbableExtendLeft.Released -= Grabbable_Released;
GrabbableExtendRight.Grabbed -= Grabbable_Grabbed;
GrabbableExtendRight.Grabbing -= Grabbable_Grabbing;
GrabbableExtendRight.Releasing -= Grabbable_Releasing;
GrabbableExtendRight.Released -= Grabbable_Released;
StopCoroutine(_hapticsCoroutine);
}
/// <summary>
/// Scales the resizable using the initial scale if it's different than 1.0
/// </summary>
protected override void Start()
{
base.Start();
if (!Mathf.Approximately(1.0f, _startScale))
{
float halfOffset = (_startScale * _initialGrabsSeparation - _initialGrabsSeparation) * 0.5f;
GrabbableExtendLeft.transform.localPosition -= Vector3.right * halfOffset;
GrabbableExtendRight.transform.localPosition += Vector3.right * halfOffset;
}
}
#endregion
#region Coroutines
/// <summary>
/// Coroutine that sends haptic feedback in case of scaling.
/// </summary>
/// <returns>Coroutine IEnumerator</returns>
private IEnumerator HapticsCoroutine()
{
void SendHapticClip(UxrGrabbableObject grabbableObject, UxrHandSide handSide, float speed)
{
if (_hapticsIntensity < 0.001f || !UxrGrabManager.Instance.GetGrabbingHand(grabbableObject, 0, out UxrGrabber grabber) || grabber.Avatar.AvatarMode != UxrAvatarMode.Local)
{
return;
}
float quantityPos = HapticsManipulationMaxSpeed - HapticsManipulationMinSpeed <= 0.0f ? 0.0f : (speed - HapticsManipulationMinSpeed) / (HapticsManipulationMaxSpeed - HapticsManipulationMinSpeed);
if (quantityPos > 0.0f)
{
float frequencyPos = Mathf.Lerp(HapticsManipulationMinFrequency, HapticsManipulationMaxFrequency, Mathf.Clamp01(quantityPos));
float amplitudePos = Mathf.Lerp(0.1f, 1.0f, Mathf.Clamp01(quantityPos)) * _hapticsIntensity;
UxrAvatar.LocalAvatarInput.SendHapticFeedback(handSide, frequencyPos, amplitudePos, UxrConstants.InputControllers.HapticSampleDurationSeconds);
}
}
float lastDistance = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position);
while (true)
{
if (_grabbableExtendLeft != null && _grabbableExtendRight != null && _grabbableExtendLeft.IsBeingGrabbed && _grabbableExtendRight.IsBeingGrabbed)
{
float currentDistance = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position);
float speed = Mathf.Abs(lastDistance - currentDistance) / UxrConstants.InputControllers.HapticSampleDurationSeconds;
SendHapticClip(_grabbableExtendLeft, UxrHandSide.Left, speed);
SendHapticClip(_grabbableExtendRight, UxrHandSide.Right, speed);
lastDistance = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position);
}
yield return new WaitForSeconds(UxrConstants.InputControllers.HapticSampleDurationSeconds);
}
}
#endregion
#region Event Handling Methods
/// <summary>
/// Called right after the avatars and manipulation update. Scale the object at this point.
/// </summary>
private void UxrManager_AvatarsUpdated()
{
UpdateResizableScale();
}
/// <summary>
/// Called when any grabbable is about to be grabbed. It is responsible for sending the appropriate
/// <see cref="UxrGrabbableResizable" /> manipulation events if necessary.
/// </summary>
/// <param name="sender">Event sender</param>
/// <param name="e">Event parameters</param>
private void Grabbable_Grabbing(object sender, UxrManipulationEventArgs e)
{
if (e.IsGrabbedStateChanged)
{
_grabbingCount++;
if (_grabbingCount == 1)
{
Grabbing?.Invoke(this, e);
}
}
}
/// <summary>
/// Called right after any grabbable was grabbed. It is responsible for sending the appropriate
/// <see cref="UxrGrabbableResizable" /> manipulation events if necessary.
/// </summary>
/// <param name="sender">Event sender</param>
/// <param name="e">Event parameters</param>
private void Grabbable_Grabbed(object sender, UxrManipulationEventArgs e)
{
if (e.IsGrabbedStateChanged)
{
_grabbedCount++;
if (_grabbedCount == 1)
{
Grabbed?.Invoke(this, e);
}
}
}
/// <summary>
/// Called when any grabbable is about to be released. It is responsible for sending the appropriate
/// <see cref="UxrGrabbableResizable" /> manipulation events if necessary.
/// </summary>
/// <param name="sender">Event sender</param>
/// <param name="e">Event parameters</param>
private void Grabbable_Releasing(object sender, UxrManipulationEventArgs e)
{
if (e.IsGrabbedStateChanged)
{
_grabbingCount--;
if (_grabbingCount == 0)
{
Releasing?.Invoke(this, e);
}
}
}
/// <summary>
/// Called right after any grabbable was released. It is responsible for sending the appropriate
/// <see cref="UxrGrabbableResizable" /> manipulation events if necessary.
/// </summary>
/// <param name="sender">Event sender</param>
/// <param name="e">Event parameters</param>
private void Grabbable_Released(object sender, UxrManipulationEventArgs e)
{
if (e.IsGrabbedStateChanged)
{
_grabbedCount--;
if (_grabbedCount == 0)
{
Released?.Invoke(this, e);
}
}
}
#endregion
#region Private Methods
/// <summary>
/// Updates the resizable scale based on the current separation between the left and right extensions.
/// </summary>
private void UpdateResizableScale()
{
float currentGrabSeparation = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position);
// Move the center in between the two extensions
Vector3 localCenter = transform.InverseTransformPoint((GrabbableExtendLeft.transform.position + GrabbableExtendRight.transform.position) * 0.5f);
Vector3 resizableLocalPos = _resizableRoot.transform.localPosition;
resizableLocalPos.x = localCenter.x;
_resizableRoot.transform.localPosition = resizableLocalPos;
// Scale the object
float localScaleZ = _resizableRoot.transform.localScale.z;
Vector3 resizableLocalScale = _initialResizableLocalScale * (currentGrabSeparation / _initialGrabsSeparation * _separationToBoundsFactor);
resizableLocalScale.z = localScaleZ;
_resizableRoot.transform.localScale = resizableLocalScale;
}
#endregion
#region Private Types & Data
private const float HapticsManipulationMinSpeed = 0.03f;
private const float HapticsManipulationMaxSpeed = 1.0f;
private const float HapticsManipulationMinFrequency = 10;
private const float HapticsManipulationMaxFrequency = 100;
private Vector3 _initialResizableLocalScale;
private float _initialGrabsSeparation;
private float _separationToBoundsFactor;
private int _grabbingCount;
private int _grabbedCount;
private Coroutine _hapticsCoroutine;
#endregion
}
}
#pragma warning restore 67

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6293fca64f4b03488a84eb8363cea23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrRestoreOnRelease.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UltimateXR.Animation.Interpolation;
using UltimateXR.Core.Components.Composite;
using UltimateXR.Core.Math;
using UnityEngine;
namespace UltimateXR.Manipulation.Helpers
{
/// <summary>
/// Component that will smoothly restore the original position and orientation of a <see cref="UxrGrabbableObject" />
/// when released.
/// </summary>
[RequireComponent(typeof(UxrGrabbableObject))]
public class UxrRestoreOnRelease : UxrGrabbableObjectComponent<UxrRestoreOnRelease>
{
#region Inspector Properties/Serialized Fields
[SerializeField] private UxrEasing _transitionType = UxrEasing.Linear;
[SerializeField] private float _transitionSeconds = 0.1f;
#endregion
#region Unity
/// <summary>
/// Updates the transition if it's active.
/// </summary>
private void Update()
{
if (_isTransitioning)
{
_transitionTimer -= Time.deltaTime;
float t = 1.0f;
if (_transitionTimer <= 0.0f)
{
_transitionTimer = 0.0f;
_isTransitioning = false;
}
else
{
t = UxrInterpolator.Interpolate(0.0f, 1.0f, 1.0f - _transitionTimer / _transitionSeconds, _transitionType);
}
GrabbableObject.transform.localPosition = Vector3.LerpUnclamped(_initialLocalPosition, GrabbableObject.InitialLocalPosition, t);
if (_singleRotationAxis != -1 && t >= 0.0f && t <= 1.0f)
{
// Do not rotate manually here to let UxrGrabbableObject keep track of single axis rotation.
// We allow using localRotation outside of the [0, 1] range to support overshooting since otherwise GrabbableObject.SingleRotationAxisDegrees
// will clamp the rotation.
GrabbableObject.SingleRotationAxisDegrees = Mathf.LerpUnclamped(_initialSingleAxisDegrees, 0.0f, t);
}
else
{
GrabbableObject.transform.localRotation = Quaternion.SlerpUnclamped(_initialLocalRotation, GrabbableObject.InitialLocalRotation, t);
}
}
}
#endregion
#region Event Trigger Methods
/// <summary>
/// Called by the base class whenever the object is grabbed.
/// </summary>
/// <param name="e">Contains all grab event parameters</param>
protected override void OnObjectGrabbed(UxrManipulationEventArgs e)
{
base.OnObjectGrabbed(e);
if (e.IsGrabbedStateChanged)
{
_isTransitioning = false;
}
}
/// <summary>
/// Called by the base class whenever the object is released.
/// </summary>
/// <param name="e">Contains all grab event parameters</param>
protected override void OnObjectReleased(UxrManipulationEventArgs e)
{
base.OnObjectReleased(e);
if (e.IsGrabbedStateChanged)
{
_isTransitioning = true;
_transitionTimer = _transitionSeconds;
_initialLocalPosition = e.GrabbableObject.transform.localPosition;
_initialLocalRotation = e.GrabbableObject.transform.localRotation;
_singleRotationAxis = e.GrabbableObject.SingleRotationAxisIndex;
if (_singleRotationAxis != -1)
{
_initialSingleAxisDegrees = e.GrabbableObject.SingleRotationAxisDegrees;
}
// Avoid transitions getting in the way or being executed after the restore ended.
e.GrabbableObject.FinishSmoothTransitions();
}
}
#endregion
#region Private Types & Data
private bool _isTransitioning;
private float _transitionTimer = -1.0f;
private Vector3 _initialLocalPosition;
private Quaternion _initialLocalRotation;
private int _singleRotationAxis;
private float _initialSingleAxisDegrees;
#endregion
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 9a39a5225324a9c44b5248d45300663a
timeCreated: 1532426377
licenseType: Pro
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrReturnGrabbableObject.ReturnPolicy.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace UltimateXR.Manipulation.Helpers
{
public partial class UxrReturnGrabbableObject
{
#region Public Types & Data
/// <summary>
/// Enumerates the different policies when returning an object to a previous anchor.
/// </summary>
public enum ReturnPolicy
{
/// <summary>
/// Return to last anchor where the object was placed.
/// </summary>
LastAnchor = 0,
/// <summary>
/// Return to the original anchor where the object was placed.
/// </summary>
OriginalAnchor,
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3fe66d825bdb4c3dba0d8252ba3cb221
timeCreated: 1693091097

View File

@@ -0,0 +1,157 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrReturnGrabbableObject.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System.Collections;
using UltimateXR.Avatar;
using UltimateXR.Core;
using UltimateXR.Core.Components.Composite;
using UnityEngine;
namespace UltimateXR.Manipulation.Helpers
{
/// <summary>
/// Component that will always return an <see cref="UxrGrabbableObject" /> to the
/// <see cref="UxrGrabbableObjectAnchor" /> it was grabbed from whenever it is released.
/// </summary>
[RequireComponent(typeof(UxrGrabbableObject))]
public partial class UxrReturnGrabbableObject : UxrGrabbableObjectComponent<UxrReturnGrabbableObject>
{
#region Inspector Properties/Serialized Fields
[SerializeField] private bool _smoothTransition = true;
[SerializeField] private float _returnDelaySeconds = -1.0f;
[SerializeField] private ReturnPolicy _returnPolicy = ReturnPolicy.LastAnchor;
#endregion
#region Public Methods
/// <summary>
/// Cancels a return if the given object has a <see cref="UxrReturnGrabbableObject" /> component and a return
/// programmed.
/// </summary>
/// <param name="grabbableObject">Object to try to cancel the return of</param>
public static void CancelReturn(UxrGrabbableObject grabbableObject)
{
if (grabbableObject.gameObject.TryGetComponent<UxrReturnGrabbableObject>(out var returnComponent))
{
returnComponent.CancelReturn();
}
}
#endregion
#region Coroutines
/// <summary>
/// Coroutine that returns the object to the original target after some time.
/// </summary>
/// <param name="grabbableObject">Object to return</param>
/// <param name="propagateEvents">Whether to propagate manipulation events</param>
/// <returns>Coroutine IEnumerator</returns>
private IEnumerator ReturnCoroutine(UxrGrabbableObject grabbableObject, bool propagateEvents)
{
yield return new WaitForSeconds(_returnDelaySeconds);
UxrGrabbableObjectAnchor anchor = GetReturnAnchor();
if (anchor != null && anchor.CurrentPlacedObject == null)
{
UxrGrabManager.Instance.PlaceObject(grabbableObject, anchor, _smoothTransition ? UxrPlacementOptions.Smooth : UxrPlacementOptions.None, propagateEvents);
}
_returnCoroutine = null;
}
#endregion
#region Event Trigger Methods
/// <summary>
/// Called by the base class whenever the object is grabbed.
/// </summary>
/// <param name="e">Contains all grab event parameters</param>
protected override void OnObjectGrabbed(UxrManipulationEventArgs e)
{
base.OnObjectGrabbed(e);
if (_returnCoroutine != null)
{
StopCoroutine(_returnCoroutine);
}
}
/// <summary>
/// Called by the base class whenever the object is released.
/// </summary>
/// <param name="e">Contains all grab event parameters</param>
protected override void OnObjectReleased(UxrManipulationEventArgs e)
{
base.OnObjectReleased(e);
if (e.IsGrabbedStateChanged)
{
if (e.GrabbableAnchor != null)
{
_lastObjectAnchor = e.GrabbableAnchor;
}
// Check also dependent grabs. We may be grabbing the object using another grip
if (!UxrGrabManager.Instance.IsHandGrabbing(UxrAvatar.LocalAvatar, GrabbableObject, UxrHandSide.Left, true) &&
!UxrGrabManager.Instance.IsHandGrabbing(UxrAvatar.LocalAvatar, GrabbableObject, UxrHandSide.Right, true))
{
if (_returnDelaySeconds <= 0.0f)
{
// Return to original place
UxrGrabManager.Instance.PlaceObject(e.GrabbableObject, GetReturnAnchor(), _smoothTransition ? UxrPlacementOptions.Smooth : UxrPlacementOptions.None, true);
}
else
{
_returnCoroutine = StartCoroutine(ReturnCoroutine(e.GrabbableObject, true));
}
}
}
}
#endregion
#region Private Methods
/// <summary>
/// Gets the anchor where the object should be returned.
/// </summary>
/// <returns>Destination anchor</returns>
private UxrGrabbableObjectAnchor GetReturnAnchor()
{
if (_returnPolicy == ReturnPolicy.LastAnchor && _lastObjectAnchor != null && _lastObjectAnchor.CurrentPlacedObject != null)
{
return _lastObjectAnchor;
}
return GrabbableObject.StartAnchor;
}
/// <summary>
/// Cancels a return if there was one programmed.
/// </summary>
private void CancelReturn()
{
if (_returnCoroutine != null)
{
StopCoroutine(_returnCoroutine);
_returnCoroutine = null;
}
}
#endregion
#region Private Types & Data
private UxrGrabbableObjectAnchor _lastObjectAnchor;
private Coroutine _returnCoroutine;
#endregion
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 6d08f3c02197a3f4ea71f397b2416ce4
timeCreated: 1532426377
licenseType: Pro
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: