Files
FORT-BO/Assets/CONTENT/Game/Scripts/Interaction/NetworkHeldInteractable.cs

291 lines
9.0 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
{
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.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 HashSet<Collider> 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 IsHeldByLocalPlayer =>
NetworkManager != null && holderClientId.Value == NetworkManager.LocalClientId;
public string HeldObjectName => InteractionLabel;
public override string InteractionPrompt => !IsHeld
? InteractionLabel
: IsHeldByLocalPlayer
? DropPrompt
: "In use";
public override bool IsInteractionAvailable => base.IsInteractionAvailable &&
(!IsHeld || IsHeldByLocalPlayer);
private void Awake()
{
if (physicsBody == null)
physicsBody = GetComponent<Rigidbody>();
if (objectColliders == null || objectColliders.Length == 0)
objectColliders = GetComponentsInChildren<Collider>(true);
}
public override void OnNetworkSpawn()
{
SpawnedHeldObjects.Add(this);
holderClientId.OnValueChanged += HandleHolderChanged;
ApplyHoldingState(IsHeld);
}
public override void OnNetworkDespawn()
{
holderClientId.OnValueChanged -= HandleHolderChanged;
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 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 (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)
{
holderClientId.Value = clientId;
ApplyHoldingState(true);
}
private void StopHolding()
{
holderClientId.Value = NoHolder;
ApplyHoldingState(false);
}
private Transform ResolveHoldingAnchor(NetworkObject holder)
{
FirstPersonCameraController cameraController =
holder.GetComponent<FirstPersonCameraController>();
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);
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<CharacterController>(
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();
}
}