460 lines
15 KiB
C#
460 lines
15 KiB
C#
using System.Collections.Generic;
|
|
using Unity.Netcode;
|
|
using Unity.Netcode.Components;
|
|
using UnityEngine;
|
|
|
|
[RequireComponent(typeof(Rigidbody), typeof(NetworkRigidbody))]
|
|
public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstationItemControl
|
|
{
|
|
protected const ulong NoHolder = ulong.MaxValue;
|
|
private static readonly HashSet<NetworkHeldInteractable> SpawnedHeldObjects = new();
|
|
|
|
[Header("Physics")]
|
|
[SerializeField] private Rigidbody physicsBody;
|
|
[SerializeField] private Collider[] objectColliders;
|
|
[SerializeField, Min(0.05f)] private float collisionRefreshInterval = 0.25f;
|
|
|
|
[Header("Holding Follow")]
|
|
[SerializeField, Min(0.1f)] private float maximumFollowSpeed = 12f;
|
|
[SerializeField, Min(0.5f)] private float maximumHoldDistance = 4.5f;
|
|
[SerializeField, Min(0.1f)] private float rotationFollowSpeed = 14f;
|
|
|
|
[Header("Local Presentation")]
|
|
[Tooltip("Removes the server round-trip delay for the player currently carrying the item.")]
|
|
[SerializeField] private bool correctLocalHolderPresentation = true;
|
|
[SerializeField, Min(0.1f)] private float maximumLocalCorrectionDistance = 2f;
|
|
|
|
[Header("Throw")]
|
|
[SerializeField, Min(0f)] private float throwForce = 6f;
|
|
[SerializeField, Min(0f)] private float throwUpwardForce = 0.5f;
|
|
|
|
private readonly NetworkVariable<ulong> holderClientId = new(
|
|
NoHolder,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
|
|
private readonly NetworkVariable<bool> isExternallyControlled = new(
|
|
false,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
|
|
private readonly HashSet<Collider> ignoredPlayerColliders = new();
|
|
private readonly HashSet<Collider> desiredIgnoredColliders = new();
|
|
private readonly List<Collider> collidersToRestore = new();
|
|
private readonly RaycastHit[] localCorrectionHits = new RaycastHit[24];
|
|
private float nextCollisionRefreshTime;
|
|
private bool initialUseGravity;
|
|
private CollisionDetectionMode initialCollisionDetectionMode;
|
|
|
|
protected abstract Vector3 HoldingOffset { get; }
|
|
protected abstract string DropPrompt { get; }
|
|
|
|
public bool IsHeld => holderClientId.Value != NoHolder;
|
|
public bool IsExternallyControlled => isExternallyControlled.Value;
|
|
public bool IsHeldByLocalPlayer =>
|
|
NetworkManager != null && holderClientId.Value == NetworkManager.LocalClientId;
|
|
public string HeldObjectName => InteractionLabel;
|
|
|
|
public override string InteractionPrompt => IsExternallyControlled
|
|
? "In machine"
|
|
: !IsHeld
|
|
? InteractionLabel
|
|
: IsHeldByLocalPlayer
|
|
? DropPrompt
|
|
: "In use";
|
|
|
|
public override bool IsInteractionAvailable => base.IsInteractionAvailable &&
|
|
!IsExternallyControlled &&
|
|
(!IsHeld || IsHeldByLocalPlayer);
|
|
|
|
private void Awake()
|
|
{
|
|
if (physicsBody == null)
|
|
physicsBody = GetComponent<Rigidbody>();
|
|
|
|
if (objectColliders == null || objectColliders.Length == 0)
|
|
objectColliders = GetComponentsInChildren<Collider>(true);
|
|
|
|
if (physicsBody != null)
|
|
{
|
|
initialUseGravity = physicsBody.useGravity;
|
|
initialCollisionDetectionMode = physicsBody.collisionDetectionMode;
|
|
}
|
|
}
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
SpawnedHeldObjects.Add(this);
|
|
holderClientId.OnValueChanged += HandleHolderChanged;
|
|
isExternallyControlled.OnValueChanged += HandleExternalControlChanged;
|
|
ApplyHoldingState();
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
holderClientId.OnValueChanged -= HandleHolderChanged;
|
|
isExternallyControlled.OnValueChanged -= HandleExternalControlChanged;
|
|
SpawnedHeldObjects.Remove(this);
|
|
RestorePlayerCollisions();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!IsSpawned || !IsHeld || IsExternallyControlled ||
|
|
Time.time < nextCollisionRefreshTime)
|
|
{
|
|
return;
|
|
}
|
|
|
|
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
|
|
RefreshHolderCollisions();
|
|
}
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
if (!IsServer || !IsSpawned || !IsHeld || IsExternallyControlled || physicsBody == null)
|
|
return;
|
|
|
|
if (!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
|
|
{
|
|
if (IsServer)
|
|
StopHolding();
|
|
|
|
return;
|
|
}
|
|
|
|
Transform anchor = ResolveHoldingAnchor(holder);
|
|
if (!DriveTowardAnchor(anchor))
|
|
return;
|
|
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if (!correctLocalHolderPresentation || IsServer || !IsSpawned ||
|
|
!IsHeldByLocalPlayer || IsExternallyControlled || physicsBody == null ||
|
|
!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Transform anchor = ResolveHoldingAnchor(holder);
|
|
Vector3 targetPosition = anchor.TransformPoint(HoldingOffset);
|
|
Vector3 targetDelta = targetPosition - physicsBody.position;
|
|
if (targetDelta.sqrMagnitude >
|
|
maximumLocalCorrectionDistance * maximumLocalCorrectionDistance ||
|
|
HasBlockingColliderForLocalCorrection(targetDelta))
|
|
{
|
|
return;
|
|
}
|
|
|
|
transform.SetPositionAndRotation(targetPosition, anchor.rotation);
|
|
}
|
|
|
|
public void RequestThrow()
|
|
{
|
|
if (IsSpawned && IsHeldByLocalPlayer)
|
|
RequestThrowServerRpc();
|
|
}
|
|
|
|
public bool TryAcquireExternalControl()
|
|
{
|
|
if (!IsServer || !IsSpawned || IsExternallyControlled)
|
|
return false;
|
|
|
|
isExternallyControlled.Value = true;
|
|
|
|
if (IsHeld)
|
|
holderClientId.Value = NoHolder;
|
|
|
|
ApplyHoldingState();
|
|
return true;
|
|
}
|
|
|
|
public bool TryReleaseExternalControl()
|
|
{
|
|
if (!IsServer || !IsSpawned || !IsExternallyControlled)
|
|
return false;
|
|
|
|
isExternallyControlled.Value = false;
|
|
ApplyHoldingState();
|
|
return true;
|
|
}
|
|
|
|
public bool TryReleaseExternalControlToPlayer(ulong clientId)
|
|
{
|
|
if (!IsServer || !IsSpawned || !IsExternallyControlled ||
|
|
TryGetHeldObject(clientId, out _) ||
|
|
NetworkHandItemInteractable.TryGetHeldItem(clientId, out _) ||
|
|
!TryGetPlayerObject(clientId, out _))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
isExternallyControlled.Value = false;
|
|
holderClientId.Value = clientId;
|
|
ApplyHoldingState();
|
|
return true;
|
|
}
|
|
|
|
public static bool TryGetHeldObject(
|
|
ulong clientId,
|
|
out NetworkHeldInteractable heldObject)
|
|
{
|
|
foreach (NetworkHeldInteractable candidate in SpawnedHeldObjects)
|
|
{
|
|
if (candidate != null && candidate.holderClientId.Value == clientId)
|
|
{
|
|
heldObject = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
heldObject = null;
|
|
return false;
|
|
}
|
|
|
|
protected override bool CanInteractOnServer(ulong senderClientId)
|
|
{
|
|
if (IsExternallyControlled)
|
|
return false;
|
|
|
|
if (holderClientId.Value == senderClientId)
|
|
return true;
|
|
|
|
return !IsHeld &&
|
|
!TryGetHeldObject(senderClientId, out _) &&
|
|
!NetworkHandItemInteractable.TryGetHeldItem(senderClientId, out _);
|
|
}
|
|
|
|
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
|
|
{
|
|
if (holderClientId.Value == senderClientId)
|
|
StopHolding();
|
|
else if (!IsHeld)
|
|
StartHolding(senderClientId);
|
|
}
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void RequestThrowServerRpc(RpcParams rpcParams = default)
|
|
{
|
|
ulong senderClientId = rpcParams.Receive.SenderClientId;
|
|
if (holderClientId.Value != senderClientId ||
|
|
!TryGetPlayerObject(senderClientId, out NetworkObject playerObject))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FirstPersonCameraController cameraController =
|
|
playerObject.GetComponent<FirstPersonCameraController>();
|
|
Vector3 throwDirection = cameraController != null
|
|
? cameraController.AimForward
|
|
: playerObject.transform.forward;
|
|
Vector3 throwVelocity = throwDirection * throwForce +
|
|
Vector3.up * throwUpwardForce;
|
|
|
|
StopHolding();
|
|
|
|
if (physicsBody != null)
|
|
physicsBody.linearVelocity = throwVelocity;
|
|
}
|
|
|
|
private void StartHolding(ulong clientId)
|
|
{
|
|
if (IsExternallyControlled)
|
|
return;
|
|
|
|
holderClientId.Value = clientId;
|
|
ApplyHoldingState();
|
|
}
|
|
|
|
private void StopHolding()
|
|
{
|
|
holderClientId.Value = NoHolder;
|
|
ApplyHoldingState();
|
|
}
|
|
|
|
private Transform ResolveHoldingAnchor(NetworkObject holder)
|
|
{
|
|
FirstPersonCameraController cameraController =
|
|
holder.GetComponent<FirstPersonCameraController>();
|
|
return cameraController != null ? cameraController.AimTransform : holder.transform;
|
|
}
|
|
|
|
private bool DriveTowardAnchor(Transform anchor)
|
|
{
|
|
Vector3 targetPosition = anchor.TransformPoint(HoldingOffset);
|
|
Vector3 targetDelta = targetPosition - physicsBody.position;
|
|
|
|
if (targetDelta.sqrMagnitude > maximumHoldDistance * maximumHoldDistance)
|
|
{
|
|
StopHolding();
|
|
return false;
|
|
}
|
|
|
|
Vector3 targetVelocity = targetDelta / Mathf.Max(Time.fixedDeltaTime, 0.001f);
|
|
physicsBody.linearVelocity = Vector3.ClampMagnitude(
|
|
targetVelocity,
|
|
maximumFollowSpeed);
|
|
|
|
DriveRotation(anchor.rotation);
|
|
return true;
|
|
}
|
|
|
|
private void DriveRotation(Quaternion targetRotation)
|
|
{
|
|
Quaternion rotationDelta = targetRotation * Quaternion.Inverse(physicsBody.rotation);
|
|
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
|
|
if (axis.sqrMagnitude < 0.0001f || float.IsNaN(axis.x))
|
|
return;
|
|
|
|
if (angle > 180f)
|
|
angle -= 360f;
|
|
|
|
Vector3 targetAngularVelocity = axis.normalized *
|
|
(angle * Mathf.Deg2Rad / Mathf.Max(Time.fixedDeltaTime, 0.001f));
|
|
physicsBody.angularVelocity = Vector3.ClampMagnitude(
|
|
targetAngularVelocity,
|
|
rotationFollowSpeed);
|
|
}
|
|
|
|
private bool HasBlockingColliderForLocalCorrection(Vector3 targetDelta)
|
|
{
|
|
float distance = targetDelta.magnitude;
|
|
if (distance <= 0.0001f)
|
|
return false;
|
|
|
|
int hitCount = Physics.RaycastNonAlloc(
|
|
physicsBody.position,
|
|
targetDelta / distance,
|
|
localCorrectionHits,
|
|
distance,
|
|
~0,
|
|
QueryTriggerInteraction.Ignore);
|
|
|
|
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
|
|
{
|
|
Collider hitCollider = localCorrectionHits[hitIndex].collider;
|
|
if (hitCollider == null ||
|
|
ignoredPlayerColliders.Contains(hitCollider) ||
|
|
IsObjectCollider(hitCollider))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool IsObjectCollider(Collider candidate)
|
|
{
|
|
foreach (Collider objectCollider in objectColliders)
|
|
{
|
|
if (objectCollider == candidate)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void HandleHolderChanged(ulong _, ulong __) => ApplyHoldingState();
|
|
|
|
private void HandleExternalControlChanged(bool _, bool __) => ApplyHoldingState();
|
|
|
|
private void ApplyHoldingState()
|
|
{
|
|
if (physicsBody != null)
|
|
{
|
|
bool isPhysicallyHeld = IsHeld && !IsExternallyControlled;
|
|
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
|
|
|
|
if (shouldBeKinematic)
|
|
{
|
|
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
|
|
physicsBody.isKinematic = true;
|
|
physicsBody.useGravity = false;
|
|
}
|
|
else
|
|
{
|
|
physicsBody.isKinematic = false;
|
|
physicsBody.useGravity = isPhysicallyHeld ? false : initialUseGravity;
|
|
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
|
|
}
|
|
|
|
if (!isPhysicallyHeld)
|
|
{
|
|
physicsBody.linearVelocity = Vector3.zero;
|
|
physicsBody.angularVelocity = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
if (IsHeld && !IsExternallyControlled)
|
|
RefreshHolderCollisions();
|
|
else
|
|
RestorePlayerCollisions();
|
|
}
|
|
|
|
private CollisionDetectionMode ResolveDynamicCollisionDetectionMode() =>
|
|
initialCollisionDetectionMode == CollisionDetectionMode.Discrete
|
|
? CollisionDetectionMode.ContinuousDynamic
|
|
: initialCollisionDetectionMode;
|
|
|
|
private void RefreshHolderCollisions()
|
|
{
|
|
desiredIgnoredColliders.Clear();
|
|
if (TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
|
|
{
|
|
foreach (Collider holderCollider in holder.GetComponentsInChildren<Collider>(true))
|
|
desiredIgnoredColliders.Add(holderCollider);
|
|
}
|
|
|
|
collidersToRestore.Clear();
|
|
foreach (Collider ignoredCollider in ignoredPlayerColliders)
|
|
{
|
|
if (ignoredCollider == null || !desiredIgnoredColliders.Contains(ignoredCollider))
|
|
collidersToRestore.Add(ignoredCollider);
|
|
}
|
|
|
|
foreach (Collider colliderToRestore in collidersToRestore)
|
|
{
|
|
SetCollisionIgnored(colliderToRestore, false);
|
|
ignoredPlayerColliders.Remove(colliderToRestore);
|
|
}
|
|
|
|
foreach (Collider desiredCollider in desiredIgnoredColliders)
|
|
{
|
|
if (desiredCollider != null && ignoredPlayerColliders.Add(desiredCollider))
|
|
SetCollisionIgnored(desiredCollider, true);
|
|
}
|
|
}
|
|
|
|
private void SetCollisionIgnored(Collider playerCollider, bool ignored)
|
|
{
|
|
if (playerCollider == null)
|
|
return;
|
|
|
|
foreach (Collider objectCollider in objectColliders)
|
|
{
|
|
if (objectCollider != null)
|
|
Physics.IgnoreCollision(objectCollider, playerCollider, ignored);
|
|
}
|
|
}
|
|
|
|
private void RestorePlayerCollisions()
|
|
{
|
|
foreach (Collider playerCollider in ignoredPlayerColliders)
|
|
{
|
|
if (playerCollider == null)
|
|
continue;
|
|
|
|
SetCollisionIgnored(playerCollider, false);
|
|
}
|
|
|
|
ignoredPlayerColliders.Clear();
|
|
desiredIgnoredColliders.Clear();
|
|
collidersToRestore.Clear();
|
|
}
|
|
}
|