Files
2026-06-07 16:58:24 +01:00

553 lines
20 KiB
C#

using System;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// This system is responsible for handling pickups.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimOnPickupEvent
/// - ClientSimOnPickupDropEvent
/// - ClientSimOnPickupUseDownEvent
/// - ClientSimOnPickupUseUpEvent
/// Listens to Input Events:
/// - Grab
/// - Use
/// - Drop
/// </remarks>
[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<FixedJoint>();
_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<IClientSimPickupHandler>())
{
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<IClientSimPickupHandler>())
{
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<IClientSimPickupHandler>())
{
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<IClientSimPickupHandler>())
{
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}");
}
}
}