Map mehanic ready to multiplayer
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class NetworkPhysicalWheelInteractable :
|
||||
NetworkInteractable,
|
||||
IPlayerInteractionSession
|
||||
{
|
||||
private const ulong NoController = ulong.MaxValue;
|
||||
|
||||
[Header("Wheel visual")]
|
||||
[SerializeField] private Transform wheelVisual;
|
||||
[SerializeField] private Vector3 neutralLocalEulerAngles;
|
||||
[SerializeField] private Vector3 localRotationAxis = Vector3.right;
|
||||
[SerializeField] private Vector3 localReferenceDirection = Vector3.up;
|
||||
[SerializeField, Range(-720f, 0f)] private float minimumAngle = -180f;
|
||||
[SerializeField, Range(0f, 720f)] private float maximumAngle = 180f;
|
||||
[SerializeField, Range(0f, 1f)] private float initialNormalizedValue = 0.5f;
|
||||
[SerializeField, Min(1f)] private float maximumRotationSpeed = 360f;
|
||||
|
||||
[Header("Control")]
|
||||
[SerializeField, Min(0.5f)] private float maximumControlDistance = 4f;
|
||||
[SerializeField] private UnityEvent<float> normalizedValueChanged;
|
||||
|
||||
private readonly NetworkVariable<ulong> controllingClientId = new(
|
||||
NoController,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkVariable<float> synchronizedAngle = new(
|
||||
0f,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private float lastPointerAngle;
|
||||
private bool hasPointerAngle;
|
||||
|
||||
public float Angle => synchronizedAngle.Value;
|
||||
public float NormalizedValue => Mathf.InverseLerp(
|
||||
minimumAngle,
|
||||
maximumAngle,
|
||||
synchronizedAngle.Value);
|
||||
public bool IsControlled => controllingClientId.Value != NoController;
|
||||
public bool IsControlledByLocalPlayer =>
|
||||
NetworkManager != null && IsControlledBy(NetworkManager.LocalClientId);
|
||||
public string ReleasePrompt => "[Hold LMB] Turning wheel";
|
||||
|
||||
public override string InteractionPrompt => IsControlledByLocalPlayer
|
||||
? ReleasePrompt
|
||||
: IsControlled
|
||||
? "Wheel in use"
|
||||
: InteractionLabel;
|
||||
|
||||
public override bool IsInteractionAvailable =>
|
||||
base.IsInteractionAvailable && (!IsControlled || IsControlledByLocalPlayer);
|
||||
|
||||
public event Action<float> ValueChanged;
|
||||
|
||||
private Quaternion NeutralLocalRotation => Quaternion.Euler(neutralLocalEulerAngles);
|
||||
private Vector3 RotationAxis => localRotationAxis.sqrMagnitude > 0.0001f
|
||||
? localRotationAxis.normalized
|
||||
: Vector3.right;
|
||||
private Vector3 ReferenceDirection => localReferenceDirection.sqrMagnitude > 0.0001f
|
||||
? localReferenceDirection.normalized
|
||||
: Vector3.up;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
wheelVisual = transform;
|
||||
neutralLocalEulerAngles = wheelVisual.localEulerAngles;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (maximumAngle <= minimumAngle)
|
||||
maximumAngle = minimumAngle + 1f;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
PlayerInteractionSessionRegistry.Register(this);
|
||||
if (IsServer)
|
||||
{
|
||||
synchronizedAngle.Value = Mathf.Lerp(
|
||||
minimumAngle,
|
||||
maximumAngle,
|
||||
initialNormalizedValue);
|
||||
}
|
||||
|
||||
synchronizedAngle.OnValueChanged += HandleAngleChanged;
|
||||
ApplyVisual(synchronizedAngle.Value);
|
||||
InvokeValueChanged();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
PlayerInteractionSessionRegistry.Unregister(this);
|
||||
synchronizedAngle.OnValueChanged -= HandleAngleChanged;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (!IsServer || !IsSpawned || !IsControlled || wheelVisual == null)
|
||||
return;
|
||||
|
||||
if (!TryGetPlayerObject(controllingClientId.Value, out NetworkObject playerObject) ||
|
||||
!IsPlayerWithinControlDistance(playerObject))
|
||||
{
|
||||
EndControl();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryCalculatePointerAngle(playerObject, out float pointerAngle))
|
||||
{
|
||||
hasPointerAngle = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPointerAngle)
|
||||
{
|
||||
lastPointerAngle = pointerAngle;
|
||||
hasPointerAngle = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float pointerDelta = Mathf.DeltaAngle(lastPointerAngle, pointerAngle);
|
||||
lastPointerAngle = pointerAngle;
|
||||
float maximumStep = maximumRotationSpeed * Time.fixedDeltaTime;
|
||||
float nextAngle = Mathf.Clamp(
|
||||
synchronizedAngle.Value + Mathf.Clamp(pointerDelta, -maximumStep, maximumStep),
|
||||
minimumAngle,
|
||||
maximumAngle);
|
||||
if (!Mathf.Approximately(nextAngle, synchronizedAngle.Value))
|
||||
synchronizedAngle.Value = nextAngle;
|
||||
}
|
||||
|
||||
public bool IsControlledBy(ulong clientId) =>
|
||||
controllingClientId.Value == clientId;
|
||||
|
||||
public void RequestRelease()
|
||||
{
|
||||
if (IsSpawned && IsControlledByLocalPlayer)
|
||||
RequestReleaseServerRpc();
|
||||
}
|
||||
|
||||
protected override bool CanInteractOnServer(ulong senderClientId) =>
|
||||
!IsControlled || IsControlledBy(senderClientId);
|
||||
|
||||
protected override void InteractOnServer(
|
||||
ulong senderClientId,
|
||||
NetworkObject playerObject)
|
||||
{
|
||||
if (IsControlledBy(senderClientId))
|
||||
EndControl();
|
||||
else
|
||||
BeginControl(senderClientId, playerObject);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestReleaseServerRpc(RpcParams rpcParams = default)
|
||||
{
|
||||
if (IsControlledBy(rpcParams.Receive.SenderClientId))
|
||||
EndControl();
|
||||
}
|
||||
|
||||
private void BeginControl(ulong clientId, NetworkObject playerObject)
|
||||
{
|
||||
controllingClientId.Value = clientId;
|
||||
hasPointerAngle = TryCalculatePointerAngle(playerObject, out lastPointerAngle);
|
||||
}
|
||||
|
||||
private void EndControl()
|
||||
{
|
||||
controllingClientId.Value = NoController;
|
||||
hasPointerAngle = false;
|
||||
}
|
||||
|
||||
private bool IsPlayerWithinControlDistance(NetworkObject playerObject)
|
||||
{
|
||||
float maximumDistanceSquared = maximumControlDistance * maximumControlDistance;
|
||||
return (playerObject.transform.position - wheelVisual.position).sqrMagnitude <=
|
||||
maximumDistanceSquared;
|
||||
}
|
||||
|
||||
private bool TryCalculatePointerAngle(
|
||||
NetworkObject playerObject,
|
||||
out float pointerAngle)
|
||||
{
|
||||
pointerAngle = 0f;
|
||||
FirstPersonCameraController cameraController =
|
||||
playerObject.GetComponent<FirstPersonCameraController>();
|
||||
Transform aimTransform = cameraController != null
|
||||
? cameraController.AimTransform
|
||||
: playerObject.transform;
|
||||
|
||||
Transform wheelParent = wheelVisual.parent;
|
||||
Vector3 axisWorld = wheelParent != null
|
||||
? wheelParent.TransformDirection(NeutralLocalRotation * RotationAxis)
|
||||
: NeutralLocalRotation * RotationAxis;
|
||||
Vector3 referenceWorld = wheelParent != null
|
||||
? wheelParent.TransformDirection(NeutralLocalRotation * ReferenceDirection)
|
||||
: NeutralLocalRotation * ReferenceDirection;
|
||||
|
||||
Ray aimRay = new(aimTransform.position, aimTransform.forward);
|
||||
Plane wheelPlane = new(axisWorld, wheelVisual.position);
|
||||
Vector3 targetPoint;
|
||||
if (wheelPlane.Raycast(aimRay, out float enter) && enter >= 0f)
|
||||
{
|
||||
targetPoint = aimRay.GetPoint(enter);
|
||||
}
|
||||
else
|
||||
{
|
||||
float distanceAlongRay = Mathf.Max(
|
||||
0.25f,
|
||||
Vector3.Dot(wheelVisual.position - aimRay.origin, aimRay.direction));
|
||||
targetPoint = aimRay.GetPoint(distanceAlongRay);
|
||||
}
|
||||
|
||||
Vector3 pointerDirection = Vector3.ProjectOnPlane(
|
||||
targetPoint - wheelVisual.position,
|
||||
axisWorld);
|
||||
if (pointerDirection.sqrMagnitude < 0.0001f)
|
||||
return false;
|
||||
|
||||
pointerAngle = Vector3.SignedAngle(
|
||||
referenceWorld,
|
||||
pointerDirection.normalized,
|
||||
axisWorld);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleAngleChanged(float _, float current) =>
|
||||
ApplyNetworkValue(current);
|
||||
|
||||
private void ApplyNetworkValue(float angle)
|
||||
{
|
||||
ApplyVisual(angle);
|
||||
InvokeValueChanged();
|
||||
}
|
||||
|
||||
private void InvokeValueChanged()
|
||||
{
|
||||
float value = NormalizedValue;
|
||||
ValueChanged?.Invoke(value);
|
||||
normalizedValueChanged?.Invoke(value);
|
||||
}
|
||||
|
||||
private void ApplyVisual(float angle)
|
||||
{
|
||||
if (wheelVisual != null)
|
||||
{
|
||||
wheelVisual.localRotation = NeutralLocalRotation *
|
||||
Quaternion.AngleAxis(angle, RotationAxis);
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Minimum")]
|
||||
private void PreviewMinimum() => ApplyVisual(minimumAngle);
|
||||
|
||||
[ContextMenu("Preview/Center")]
|
||||
private void PreviewCenter() => ApplyVisual(Mathf.Lerp(
|
||||
minimumAngle,
|
||||
maximumAngle,
|
||||
0.5f));
|
||||
|
||||
[ContextMenu("Preview/Maximum")]
|
||||
private void PreviewMaximum() => ApplyVisual(maximumAngle);
|
||||
}
|
||||
Reference in New Issue
Block a user