Map mehanic ready to multiplayer
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@@ -13,6 +14,9 @@ public sealed class NetworkButtonInteractable : NetworkInteractable
|
||||
|
||||
public bool IsPressed => isPressed.Value;
|
||||
|
||||
public event Action Pressed;
|
||||
public event Action<bool> StateChanged;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
isPressed.OnValueChanged += HandleStateChanged;
|
||||
@@ -28,11 +32,15 @@ public sealed class NetworkButtonInteractable : NetworkInteractable
|
||||
|
||||
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
|
||||
{
|
||||
Pressed?.Invoke();
|
||||
isPressed.Value = !isPressed.Value;
|
||||
ApplyState(isPressed.Value);
|
||||
}
|
||||
|
||||
private void HandleStateChanged(bool _, bool currentValue) => ApplyState(currentValue);
|
||||
|
||||
private void ApplyState(bool currentValue) => buttonStateChanged?.Invoke(currentValue);
|
||||
private void ApplyState(bool currentValue)
|
||||
{
|
||||
StateChanged?.Invoke(currentValue);
|
||||
buttonStateChanged?.Invoke(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@@ -52,7 +53,10 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
public bool IsControlled => controllingClientId.Value != NoController;
|
||||
public bool IsControlledByLocalPlayer =>
|
||||
NetworkManager != null && IsControlledBy(NetworkManager.LocalClientId);
|
||||
public string ReleasePrompt => "[Hold E] Pulling lever";
|
||||
public string ReleasePrompt => "[Hold LMB] Pulling lever";
|
||||
|
||||
public event Action<float> ValueChanged;
|
||||
public event Action<bool> StateChanged;
|
||||
|
||||
public override string InteractionPrompt => IsControlledByLocalPlayer
|
||||
? ReleasePrompt
|
||||
@@ -304,11 +308,15 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
private void HandleValueChanged(float _, float currentValue)
|
||||
{
|
||||
ApplyVisual(currentValue);
|
||||
ValueChanged?.Invoke(currentValue);
|
||||
leverValueChanged?.Invoke(currentValue);
|
||||
}
|
||||
|
||||
private void HandleActivationChanged(bool _, bool currentState) =>
|
||||
private void HandleActivationChanged(bool _, bool currentState)
|
||||
{
|
||||
StateChanged?.Invoke(currentState);
|
||||
leverStateChanged?.Invoke(currentState);
|
||||
}
|
||||
|
||||
private void ApplyVisual(float value)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c645663bb9dcbaf43991bd38665923da
|
||||
@@ -22,7 +22,7 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isInitialized || !IsOwner || Keyboard.current == null)
|
||||
if (!isInitialized || !IsOwner)
|
||||
return;
|
||||
|
||||
if (PlayerInteractionSessionRegistry.TryGetControlledSession(
|
||||
@@ -31,7 +31,9 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
{
|
||||
focusedInteractable = null;
|
||||
|
||||
if (!Keyboard.current.eKey.isPressed && !activeSessionReleaseRequested)
|
||||
bool isHoldingControl = Mouse.current != null &&
|
||||
Mouse.current.leftButton.isPressed;
|
||||
if (!isHoldingControl && !activeSessionReleaseRequested)
|
||||
{
|
||||
activeSessionReleaseRequested = true;
|
||||
activeSession.RequestRelease();
|
||||
@@ -44,9 +46,14 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
activeSessionReleaseRequested = false;
|
||||
focusedInteractable = FindFocusedInteractable();
|
||||
|
||||
if (Keyboard.current.eKey.wasPressedThisFrame &&
|
||||
focusedInteractable != null &&
|
||||
focusedInteractable.IsInteractionAvailable)
|
||||
if (focusedInteractable == null || !focusedInteractable.IsInteractionAvailable)
|
||||
return;
|
||||
|
||||
bool startsHoldSession = focusedInteractable is IPlayerInteractionSession &&
|
||||
Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
|
||||
bool performsRegularInteraction = focusedInteractable is not IPlayerInteractionSession &&
|
||||
Keyboard.current != null && Keyboard.current.eKey.wasPressedThisFrame;
|
||||
if (startsHoldSession || performsRegularInteraction)
|
||||
{
|
||||
focusedInteractable.RequestInteraction();
|
||||
}
|
||||
@@ -128,8 +135,11 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
if (focusedInteractable == null)
|
||||
return;
|
||||
|
||||
string inputPrompt = focusedInteractable is IPlayerInteractionSession
|
||||
? "[Hold LMB] "
|
||||
: "[E] ";
|
||||
string prompt = focusedInteractable.IsInteractionAvailable
|
||||
? "[E] " + focusedInteractable.InteractionPrompt
|
||||
? inputPrompt + focusedInteractable.InteractionPrompt
|
||||
: "In use";
|
||||
DrawPrompt(prompt);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user