Files
FORT-BO/Assets/CONTENT/Game/Scripts/Interaction/NetworkHeldInteractable.cs
T
2026-08-14 18:07:12 +03:00

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();
}
}