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.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 float nextCollisionRefreshTime; private Vector3 followVelocity; private Vector3 previousHolderPosition; private bool hasPreviousHolderPosition; 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); } public override void OnNetworkSpawn() { SpawnedHeldObjects.Add(this); holderClientId.OnValueChanged += HandleHolderChanged; isExternallyControlled.OnValueChanged += HandleExternalControlChanged; ApplyHoldingState(IsHeld || IsExternallyControlled); } public override void OnNetworkDespawn() { holderClientId.OnValueChanged -= HandleHolderChanged; isExternallyControlled.OnValueChanged -= HandleExternalControlChanged; SpawnedHeldObjects.Remove(this); ApplyHoldingState(false); } private void LateUpdate() { if (!IsSpawned || !IsHeld) return; if (!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder)) { if (IsServer) StopHolding(); return; } FollowHolderTranslation(holder.transform.position); Transform anchor = ResolveHoldingAnchor(holder); FollowHoldingAnchor(anchor); if (Time.time >= nextCollisionRefreshTime) { nextCollisionRefreshTime = Time.time + collisionRefreshInterval; IgnoreCollisionsWithPlayers(); } } 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(true); return true; } public bool TryReleaseExternalControl() { if (!IsServer || !IsSpawned || !IsExternallyControlled) return false; isExternallyControlled.Value = false; ApplyHoldingState(IsHeld); 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(true); } private void StopHolding() { holderClientId.Value = NoHolder; ApplyHoldingState(IsExternallyControlled); } private Transform ResolveHoldingAnchor(NetworkObject holder) { FirstPersonCameraController cameraController = holder.GetComponent(); return cameraController != null ? cameraController.AimTransform : holder.transform; } private void FollowHoldingAnchor(Transform anchor) { Vector3 targetPosition = anchor.TransformPoint(HoldingOffset); Vector3 targetDelta = targetPosition - transform.position; if (targetDelta.sqrMagnitude > maximumFollowDistance * maximumFollowDistance) { transform.position = targetPosition - targetDelta.normalized * maximumFollowDistance; followVelocity = Vector3.zero; } transform.position = Vector3.SmoothDamp( transform.position, targetPosition, ref followVelocity, positionSmoothTime, maximumFollowSpeed, Time.deltaTime); float rotationBlend = 1f - Mathf.Exp(-rotationFollowSpeed * Time.deltaTime); transform.rotation = Quaternion.Slerp( transform.rotation, anchor.rotation, rotationBlend); } private void FollowHolderTranslation(Vector3 holderPosition) { if (!hasPreviousHolderPosition) { previousHolderPosition = holderPosition; hasPreviousHolderPosition = true; return; } Vector3 holderDelta = holderPosition - previousHolderPosition; previousHolderPosition = holderPosition; transform.position += holderDelta; } private void HandleHolderChanged(ulong _, ulong currentHolder) => ApplyHoldingState(currentHolder != NoHolder || IsExternallyControlled); private void HandleExternalControlChanged(bool _, bool isControlled) => ApplyHoldingState(IsHeld || isControlled); private void ApplyHoldingState(bool isHeld) { followVelocity = Vector3.zero; hasPreviousHolderPosition = false; if (physicsBody != null) { bool shouldBeKinematic = isHeld || !IsServer; if (physicsBody.isKinematic && !shouldBeKinematic) physicsBody.isKinematic = false; if (!physicsBody.isKinematic) { physicsBody.linearVelocity = Vector3.zero; physicsBody.angularVelocity = Vector3.zero; } physicsBody.isKinematic = shouldBeKinematic; } if (isHeld) IgnoreCollisionsWithPlayers(); else RestorePlayerCollisions(); } private void IgnoreCollisionsWithPlayers() { foreach (CharacterController playerCollider in FindObjectsByType( FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { foreach (Collider objectCollider in objectColliders) { if (objectCollider != null && playerCollider != null) Physics.IgnoreCollision(objectCollider, playerCollider, true); } ignoredPlayerColliders.Add(playerCollider); } } private void RestorePlayerCollisions() { foreach (Collider playerCollider in ignoredPlayerColliders) { if (playerCollider == null) continue; foreach (Collider objectCollider in objectColliders) { if (objectCollider != null) Physics.IgnoreCollision(objectCollider, playerCollider, false); } } ignoredPlayerColliders.Clear(); } }