using System.Collections.Generic; using Unity.Netcode; using Unity.Netcode.Components; using UnityEngine; [RequireComponent(typeof(Rigidbody), typeof(NetworkRigidbody))] public sealed class NetworkDraggableInteractable : NetworkInteractable { private const ulong NoDragger = ulong.MaxValue; [Header("Weight")] [SerializeField, Min(0.1f)] private float weight = 35f; [SerializeField, Min(0.1f)] private float pullStrengthPerPlayer = 50f; [SerializeField, Range(1, 2)] private int maximumDraggers = 2; [Header("Dragging")] [SerializeField] private Vector3 dragOffset = new(0f, 1f, 1.8f); [SerializeField, Min(0.1f)] private float maximumDragSpeed = 3f; [SerializeField, Range(0.05f, 1f)] private float minimumSpeedMultiplier = 0.35f; [SerializeField, Min(0.5f)] private float maximumDraggerDistance = 4.5f; [SerializeField] private bool keepCurrentHeight; [Header("Physics")] [SerializeField] private Rigidbody physicsBody; [SerializeField] private Collider[] objectColliders; [SerializeField, Min(0.05f)] private float collisionRefreshInterval = 0.25f; private readonly NetworkVariable firstDraggerClientId = new( NoDragger, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable secondDraggerClientId = new( NoDragger, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly HashSet ignoredPlayerColliders = new(); private float nextCollisionRefreshTime; public float Weight => weight; public int ActiveDraggerCount { get { int count = firstDraggerClientId.Value == NoDragger ? 0 : 1; if (secondDraggerClientId.Value != NoDragger) count++; return count; } } public int RequiredDraggerCount => Mathf.Max(1, Mathf.CeilToInt(weight / pullStrengthPerPlayer)); public bool HasEnoughPullStrength => ActiveDraggerCount >= RequiredDraggerCount; private bool HasAnyDragger => ActiveDraggerCount > 0; private bool IsDraggedByLocalPlayer => NetworkManager != null && IsDragger(NetworkManager.LocalClientId); public override string InteractionPrompt { get { if (IsDraggedByLocalPlayer) return "Release"; int activeDraggers = ActiveDraggerCount; if (activeDraggers == 0) return $"{InteractionLabel} ({weight:0.#} kg)"; if (activeDraggers < maximumDraggers) return !HasEnoughPullStrength ? $"Help drag ({activeDraggers}/{RequiredDraggerCount})" : $"Join drag ({activeDraggers}/{maximumDraggers})"; return HasEnoughPullStrength ? "In use" : "Too heavy"; } } public override bool IsInteractionAvailable => base.IsInteractionAvailable && (IsDraggedByLocalPlayer || ActiveDraggerCount < maximumDraggers); private void Awake() { ResolvePhysicsReferences(); ApplyWeight(); } private void OnValidate() { ResolvePhysicsReferences(); ApplyWeight(); } public override void OnNetworkSpawn() { firstDraggerClientId.OnValueChanged += HandleDraggerChanged; secondDraggerClientId.OnValueChanged += HandleDraggerChanged; ApplyDraggingState(); } public override void OnNetworkDespawn() { firstDraggerClientId.OnValueChanged -= HandleDraggerChanged; secondDraggerClientId.OnValueChanged -= HandleDraggerChanged; RestorePlayerCollisions(); } private void Update() { if (!HasAnyDragger || Time.time < nextCollisionRefreshTime) return; nextCollisionRefreshTime = Time.time + collisionRefreshInterval; IgnoreCollisionsWithPlayers(); } private void FixedUpdate() { if (!IsServer || !IsSpawned || physicsBody == null) return; RemoveInvalidDraggers(); if (!HasEnoughPullStrength || !TryGetCombinedDragTarget(out Vector3 target)) return; if (keepCurrentHeight) target.y = physicsBody.position.y; float totalStrength = ActiveDraggerCount * pullStrengthPerPlayer; float loadRatio = Mathf.Clamp01(weight / totalStrength); float speedMultiplier = Mathf.Lerp(1f, minimumSpeedMultiplier, loadRatio); float movementStep = maximumDragSpeed * speedMultiplier * Time.fixedDeltaTime; Vector3 nextPosition = Vector3.MoveTowards(physicsBody.position, target, movementStep); physicsBody.MovePosition(nextPosition); } protected override bool CanInteractOnServer(ulong senderClientId) => IsDragger(senderClientId) || ActiveDraggerCount < maximumDraggers; protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject) { if (IsDragger(senderClientId)) RemoveDragger(senderClientId); else AddDragger(senderClientId); } private void AddDragger(ulong clientId) { if (firstDraggerClientId.Value == NoDragger) firstDraggerClientId.Value = clientId; else if (maximumDraggers > 1 && secondDraggerClientId.Value == NoDragger) secondDraggerClientId.Value = clientId; } private void RemoveDragger(ulong clientId) { if (firstDraggerClientId.Value == clientId) { firstDraggerClientId.Value = secondDraggerClientId.Value; secondDraggerClientId.Value = NoDragger; } else if (secondDraggerClientId.Value == clientId) { secondDraggerClientId.Value = NoDragger; } } private void RemoveInvalidDraggers() { ulong first = firstDraggerClientId.Value; ulong second = secondDraggerClientId.Value; if (first != NoDragger && !IsDraggerValid(first)) RemoveDragger(first); if (second != NoDragger && !IsDraggerValid(second)) RemoveDragger(second); } private bool IsDraggerValid(ulong clientId) { if (!TryGetPlayerObject(clientId, out NetworkObject playerObject)) return false; float maximumDistanceSquared = maximumDraggerDistance * maximumDraggerDistance; return (playerObject.transform.position - physicsBody.position).sqrMagnitude <= maximumDistanceSquared; } private bool TryGetCombinedDragTarget(out Vector3 target) { target = Vector3.zero; int validTargets = 0; AddDragTarget(firstDraggerClientId.Value, ref target, ref validTargets); AddDragTarget(secondDraggerClientId.Value, ref target, ref validTargets); if (validTargets == 0) return false; target /= validTargets; return true; } private void AddDragTarget(ulong clientId, ref Vector3 target, ref int validTargets) { if (clientId == NoDragger || !TryGetPlayerObject(clientId, out NetworkObject playerObject)) return; target += playerObject.transform.TransformPoint(dragOffset); validTargets++; } private bool IsDragger(ulong clientId) => firstDraggerClientId.Value == clientId || secondDraggerClientId.Value == clientId; private void HandleDraggerChanged(ulong _, ulong __) => ApplyDraggingState(); private void ApplyDraggingState() { if (physicsBody != null) { physicsBody.isKinematic = HasAnyDragger || !IsServer; physicsBody.linearVelocity = Vector3.zero; physicsBody.angularVelocity = Vector3.zero; } if (HasAnyDragger) IgnoreCollisionsWithPlayers(); else RestorePlayerCollisions(); } private void ResolvePhysicsReferences() { if (physicsBody == null) physicsBody = GetComponent(); if (objectColliders == null || objectColliders.Length == 0) objectColliders = GetComponentsInChildren(true); } private void ApplyWeight() { if (physicsBody != null) physicsBody.mass = Mathf.Max(0.1f, weight); } 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(); } }