using System; using UnityEngine; using VRC.SDKBase; using VRC.Udon.Common; namespace VRC.SDK3.ClientSim { /// /// This system is responsible for handling pickups. /// /// /// Sends Events: /// - ClientSimOnPickupEvent /// - ClientSimOnPickupDropEvent /// - ClientSimOnPickupUseDownEvent /// - ClientSimOnPickupUseUpEvent /// Listens to Input Events: /// - Grab /// - Use /// - Drop /// [AddComponentMenu("")] public class ClientSimPlayerHand : ClientSimBehaviour, IDisposable { // The duration after picking up an object to start sending pickup UseDown and UseUp events. private const float INITIAL_PICKUP_DURATION = 0.5f; // The distance at which a pickup will force snap to the hand. private const float MAX_PICKUP_DISTANCE = 0.25f; // How many units will the object rotate during manipulation. private const float DESKTOP_ROTATION_MULTIPLIER = 2f; // How many units will an object be moved forward or backwards during manipulation. private const float DESKTOP_MANIPULATE_MULTIPLIER = 0.01f; // How far can the object move away from the hand during manipulation. private const float DESKTOP_MANIPULATION_MAX_DISTANCE = 0.64f; private static readonly Quaternion _gripOffsetRotation = Quaternion.Euler(0, 35, 0); private static readonly Quaternion _gunOffsetRotation = Quaternion.Euler(0, 305, 0); private static readonly Quaternion _desktopManipulationRotation = Quaternion.Euler(180, 35, 90); [SerializeField] private HandType handType; [SerializeField] private Transform handTransform; [SerializeField] private ClientSimPlayerHand otherHand; private IClientSimEventDispatcher _eventDispatcher; private IClientSimInput _input; private IClientSimTrackingProvider _trackingProvider; private IClientSimPlayerApiProvider _player; private IClientSimPlayerPickupData _pickupData; private VRC_Pickup.PickupHand _pickupHandType; // The object this hand hovering to know if it should pickup an object. private IClientSimPickupable _hoverPickupable; // The object currently held by this hand. private IClientSimPickupable _heldPickupable; private Rigidbody _heldPickupRigidbody; private Transform _heldPickupTransform; private GameObject _heldPickupGameObject; private FixedJoint _heldPickupJoint; // Used for determining pickup throw private Vector3 _previousHandPosition; private Vector3 _previousHandRotation; // Check if the use input is down (true) or up (false) private bool _useInputHeldDown; // Has this pickup fired the UseDown event, to know if we need to fire the UseUp event. private bool _isUseDown; private bool _initialGrab; private float _grabActionStartTime; private float _dropActionStartTime; public void Initialize( IClientSimEventDispatcher eventDispatcher, IClientSimInput input, IClientSimTrackingProvider trackingProvider, IClientSimPlayerApiProvider player, IClientSimPlayerPickupData pickupData) { _eventDispatcher = eventDispatcher; _input = input; _trackingProvider = trackingProvider; _player = player; _pickupData = pickupData; // Too many hand enums... _pickupHandType = (handType == HandType.LEFT ? VRC_Pickup.PickupHand.Left : VRC_Pickup.PickupHand.Right); enabled = false; // Only enabled while holding something to reduce Update checks. // Subscribe to input events // Input will be null with incorrect Unity input project settings. _input?.SubscribeGrab(GrabInput); _input?.SubscribeUse(UseInput); _input?.SubscribeDrop(DropInput); } private void OnDestroy() { Dispose(); } public void Dispose() { // Unsubscribe _input?.UnsubscribeGrab(GrabInput); _input?.UnsubscribeUse(UseInput); _input?.UnsubscribeDrop(DropInput); } private void Update() { if (!IsHolding()) { return; } if (_initialGrab && _useInputHeldDown) { HandleUseInput(); } UpdateManipulation(); UpdatePosition(); _previousHandPosition = handTransform.position; _previousHandRotation = handTransform.rotation.eulerAngles; } #region ClientSim Input private void GrabInput(bool value, HandType hand) { if (hand != handType) { return; } if (value) { // Try to grab hover object if (!IsHolding() && _hoverPickupable != null) { Pickup(_hoverPickupable); } } else { // If releasing grab input and holding a pickup that is not auto hold, drop the pickup. if (IsHolding() && !ShouldAutoHoldPickupable(_heldPickupable)) { ForceDrop(_heldPickupable); } } } private void UseInput(bool value, HandType hand) { if (hand != handType) { return; } // Save input state to know when we can fire the first UseDown event after being picked up. See Update. _useInputHeldDown = value; if (!IsHolding()) { return; } HandleUseInput(); } private void DropInput(bool value, HandType hand) { if (hand != handType) { return; } // Do not try to drop anything if not holding a pickup. if (!IsHolding()) { return; } if (value) { // Button was just pressed. Start a timer to know how much "throw" charge should be added. _dropActionStartTime = Time.time; } else { Drop(_heldPickupable, Time.time - _dropActionStartTime); } } #endregion public void SetHoverPickupable(IClientSimPickupable pickupable) { _hoverPickupable = pickupable; } public bool IsHolding() { return _heldPickupable != null; } private bool ShouldAutoHoldPickupable(IClientSimPickupable pickupable) { // Some VR controllers do not support auto hold. return pickupable.AutoHold() && _trackingProvider.SupportsPickupAutoHold(); } private void Pickup(IClientSimPickupable pickupable) { if (IsHolding()) { LogErrorMessage("Cannot pickup a pickup while holding another."); return; } if (pickupable.IsHeld()) { // Allow yourself to grab a pickup from your other hand. if (otherHand != null && otherHand._heldPickupable == pickupable) { otherHand.ForceDrop(pickupable); } else { LogErrorMessage("Cannot pickup a pickup someone else is holding."); return; } } handTransform.localPosition = Vector3.zero; handTransform.localRotation = Quaternion.identity; _heldPickupable = pickupable; _heldPickupTransform = pickupable.GetTransform(); _heldPickupGameObject = pickupable.GetGameObject(); _heldPickupRigidbody = pickupable.GetRigidbody(); LogMessage($"Picking up object {_heldPickupGameObject.name}"); VRC_Pickup pickup = pickupable.GetPickup(); _pickupData.SetPickupInHand(_pickupHandType, pickup); pickupable.Pickup(_player.Player, _pickupHandType, ForceDrop); // Set the grab time to know if the player has held long enough to send Use events _grabActionStartTime = Time.time; _initialGrab = true; // Set self enabled to allow for pickup manipulation enabled = true; VRC_Pickup.PickupOrientation pickupOrientation = pickupable.GetOrientation(); Transform pickupExactGrip = pickupable.GetGripLocation(); Transform pickupExactGun = pickupable.GetGunLocation(); // Calculate offset Transform pickupHoldPoint = null; Quaternion offsetRotation = Quaternion.identity; if (pickupOrientation == VRC_Pickup.PickupOrientation.Grip && pickupExactGrip != null) { pickupHoldPoint = pickupExactGrip; offsetRotation = _gripOffsetRotation; } else if (pickupOrientation == VRC_Pickup.PickupOrientation.Gun && pickupExactGun != null) { pickupHoldPoint = pickupExactGun; offsetRotation = _gunOffsetRotation; } Vector3 positionOffset; Quaternion rotationOffset; // Grab as if no pickup point if (pickupHoldPoint == null) { rotationOffset = Quaternion.Inverse(handTransform.rotation) * _heldPickupTransform.rotation; positionOffset = handTransform.InverseTransformDirection(_heldPickupTransform.position - handTransform.position); if (positionOffset.magnitude > MAX_PICKUP_DISTANCE && pickupOrientation == VRC_Pickup.PickupOrientation.Any) { positionOffset = positionOffset.normalized * MAX_PICKUP_DISTANCE; } } else { rotationOffset = offsetRotation * Quaternion.Inverse(Quaternion.Inverse(_heldPickupTransform.rotation) * pickupHoldPoint.rotation); positionOffset = rotationOffset * _heldPickupTransform.InverseTransformDirection(_heldPickupTransform.position - pickupHoldPoint.position); } Vector3 position = handTransform.position + handTransform.TransformDirection(positionOffset); Quaternion rotation = handTransform.rotation * rotationOffset; // Move hand and pickup to the same location handTransform.position = _heldPickupTransform.position = position; handTransform.rotation = _heldPickupTransform.rotation = rotation; // Link with hand rigidbody _heldPickupJoint = handTransform.gameObject.AddComponent(); _heldPickupJoint.connectedBody = _heldPickupRigidbody; // Set the owner of this object to the player picking it up. Networking.SetOwner(_player.Player, _heldPickupGameObject); _eventDispatcher.SendEvent(new ClientSimOnPickupEvent { player = _player.Player, handType = handType, pickup = pickupable, }); // Notify pickup handlers of object pickup. foreach (var pickupHandler in _heldPickupGameObject.GetComponents()) { pickupHandler.OnPickup(); } } public void ForceDrop() { if (_heldPickupable != null) { ForceDrop(_heldPickupable); } } private void ForceDrop(IClientSimPickupable pickupable) { Drop(pickupable, 0); } private void Drop(IClientSimPickupable pickupable, float throwHoldDuration) { if (_heldPickupable != pickupable || !pickupable.IsHeld() || pickupable.GetHoldingPlayer() != _player.Player) { LogErrorMessage("Cannot drop a pickup that you aren't holding."); return; } // Ensure that UseUp is called before the drop event finishes. OnPickupUseUp(); // Check to return early and ensure no errors if OnPickupUseUp calls Drop. if (_heldPickupable != pickupable || !pickupable.IsHeld() || pickupable.GetHoldingPlayer() != _player.Player) { return; } LogMessage($"Dropping object {_heldPickupGameObject.name}"); // Unlink from arm rigidbody if (_heldPickupJoint != null) { Destroy(_heldPickupJoint); } // When exiting playmode while holding an object, Drop will be called and Time.deltaTime will be 0. // This check prevents setting the velocity to NaN due to divide by zero. if (Time.deltaTime > 0) { _heldPickupRigidbody.velocity = (handTransform.position - _previousHandPosition) * (0.5f / Time.deltaTime); _heldPickupRigidbody.angularVelocity = (handTransform.rotation.eulerAngles - _previousHandRotation); } // Calculate throw velocity // TODO Verify how VR handles throwing pickups if (!_heldPickupRigidbody.isKinematic) { float holdDuration = Mathf.Clamp(throwHoldDuration, 0, 3); if (holdDuration > 0.2f) { float power = holdDuration * 500 * pickupable.GetThrowVelocityBoostScale(); Vector3 throwForce = power * transform.TransformDirection(_gripOffsetRotation * Vector3.forward); _heldPickupRigidbody.AddForce(throwForce); LogMessage($"Adding throw force: {throwForce}"); } } pickupable.Drop(_player.Player); _pickupData.SetPickupInHand(_pickupHandType, null); _eventDispatcher.SendEvent(new ClientSimOnPickupDropEvent { player = _player.Player, handType = handType, pickup = pickupable, }); // Notify pickup handlers that the object has been dropped. foreach (var pickupHandler in _heldPickupGameObject.GetComponents()) { pickupHandler.OnDrop(); } _heldPickupable = null; _heldPickupTransform = null; _heldPickupGameObject = null; _heldPickupRigidbody = null; // Prevent throwing an exception when exiting playmode due to this object being destroyed. if (this != null) { enabled = false; } handTransform.localPosition = Vector3.zero; handTransform.localRotation = Quaternion.identity; } private bool HeldLongEnoughForUseEvents() { float grabDuration = Time.time - _grabActionStartTime; return grabDuration >= INITIAL_PICKUP_DURATION; } private void HandleUseInput() { // Grab time has not been long enough to send use events if (!HeldLongEnoughForUseEvents()) { return; } // Only auto hold pickups can be used. if (!_heldPickupable.AutoHold()) { return; } if (_useInputHeldDown) { OnPickupUseDown(); } else { OnPickupUseUp(); } } private void OnPickupUseDown() { LogMessage($"Pickup Use Down {_heldPickupGameObject.name}"); _initialGrab = false; _isUseDown = true; _eventDispatcher.SendEvent(new ClientSimOnPickupUseDownEvent { player = _player.Player, handType = handType, pickup = _heldPickupable, }); // Notify pickup handlers that the object has Use Down. foreach (var pickupHandler in _heldPickupGameObject.GetComponents()) { pickupHandler.OnPickupUseDown(); } } private void OnPickupUseUp() { // Prevent calling UseUp if UseDown was never called. if (!_isUseDown) { return; } LogMessage($"Pickup Use Up {_heldPickupGameObject.name}"); _isUseDown = false; _eventDispatcher.SendEvent(new ClientSimOnPickupUseUpEvent { player = _player.Player, handType = handType, pickup = _heldPickupable, }); // Notify pickup handlers that the object has Use Up. foreach (var pickupHandler in _heldPickupGameObject.GetComponents()) { pickupHandler.OnPickupUseUp(); } } // Apply desktop hand rotations private void UpdateManipulation() { if (handType != HandType.RIGHT || !_heldPickupable.AllowManipulation()) { return; } // Get the input for rotating the pickup. Vector3 angles = new Vector3( _input.GetPickupRotateUpDown(), _input.GetPickupRotateLeftRight(), _input.GetPickupRotateCwCcw()); // Only apply rotation if some input has been detected. if (angles.sqrMagnitude > 0) { // Rotate the input angles to match rotation based on desktop view. angles = transform.rotation * _desktopManipulationRotation * angles; // Apply rotation to hand. handTransform.Rotate(angles, DESKTOP_ROTATION_MULTIPLIER,Space.World); } // Move pickup forward and back. float manipulateForwardBack = _input.GetPickupManipulateDistance(); if (!Mathf.Approximately(manipulateForwardBack, 0)) { Vector3 forward = _gripOffsetRotation * Vector3.forward; Vector3 offset = forward * Mathf.Sign(manipulateForwardBack) * DESKTOP_MANIPULATE_MULTIPLIER; Vector3 handLocal = handTransform.localPosition + offset; handLocal = Vector3.ClampMagnitude(handLocal, DESKTOP_MANIPULATION_MAX_DISTANCE); handTransform.localPosition = handLocal; } } public void UpdatePosition(bool force = false) { if ((_heldPickupRigidbody != null && _heldPickupRigidbody.isKinematic) || force) { _heldPickupTransform.SetPositionAndRotation(handTransform.position, handTransform.rotation); } } private void LogMessage(string message) { this.Log($"[{handType}] {message}"); } private void LogErrorMessage(string message) { this.LogError($"[{handType}] {message}"); } } }