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 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.01f)] private float positionSmoothTime = 0.12f; [SerializeField, Min(0.1f)] private float maximumFollowSpeed = 12f; [SerializeField, Min(0.1f)] private float maximumFollowDistance = 1.25f; [SerializeField, Min(0.5f)] private float maximumHoldDistance = 4.5f; [SerializeField, Min(0.1f)] private float rotationFollowSpeed = 14f; [Header("Throw")] [SerializeField, Min(0f)] private float throwForce = 6f; [SerializeField, Min(0f)] private float throwUpwardForce = 0.5f; private readonly NetworkVariable holderClientId = new( NoHolder, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable isExternallyControlled = new( false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly HashSet ignoredPlayerColliders = new(); private readonly HashSet desiredIgnoredColliders = new(); private readonly List collidersToRestore = new(); 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(); if (objectColliders == null || objectColliders.Length == 0) objectColliders = GetComponentsInChildren(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; } 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(); 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(); 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; } float distance = targetDelta.magnitude; float speedDistance = Mathf.Max(maximumFollowDistance, 0.01f); float desiredSpeed = maximumFollowSpeed * Mathf.Clamp01(distance / speedDistance); Vector3 desiredVelocity = distance > 0.0001f ? targetDelta / distance * desiredSpeed : Vector3.zero; float maximumAcceleration = maximumFollowSpeed / Mathf.Max(positionSmoothTime, 0.01f); Vector3 velocityChange = Vector3.ClampMagnitude( desiredVelocity - physicsBody.linearVelocity, maximumAcceleration * Time.fixedDeltaTime); physicsBody.AddForce(velocityChange, ForceMode.VelocityChange); 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 desiredAngularVelocity = axis.normalized * (angle * Mathf.Deg2Rad * rotationFollowSpeed); desiredAngularVelocity = Vector3.ClampMagnitude( desiredAngularVelocity, rotationFollowSpeed); Vector3 angularVelocityChange = Vector3.ClampMagnitude( desiredAngularVelocity - physicsBody.angularVelocity, rotationFollowSpeed * 4f * Time.fixedDeltaTime); physicsBody.AddTorque(angularVelocityChange, ForceMode.VelocityChange); } 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(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(); } }