553 lines
20 KiB
C#
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}");
|
|
}
|
|
}
|
|
} |