Files
FORT-BO/Assets/CONTENT/Game/Scripts/Interaction/NetworkHeldInteractable.cs
T
2026-08-14 16:54:09 +03:00

404 lines
13 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.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<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 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;
}
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;
}
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<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();
}
}