commit428243f4a6Author: AsanoRema <asanorema2036@gmail.com> Date: Sat Aug 29 13:43:13 2026 +0300 Squashed commit of the following: commit68e78fa464Author: AsanoRema <asanorema2036@gmail.com> Date: Sat Aug 29 13:41:49 2026 +0300 Updated .gitignore commit9f87122a34Author: AsanoRema <asanorema2036@gmail.com> Date: Sat Aug 29 13:34:57 2026 +0300 Added Dohya commitf6a7547e84Author: AsanoRema <asanorema2036@gmail.com> Date: Fri Aug 21 17:59:13 2026 +0300 Fixed commitb98901c366Author: AsanoRema <asanorema2036@gmail.com> Date: Tue Aug 18 17:37:22 2026 +0300 Added new particle Package commitbb2f4a3b16Author: AsanoRema <asanorema2036@gmail.com> Date: Tue Aug 18 17:17:48 2026 +0300 Map mehanic ready to multiplayer commitc01ff800f6Author: AsanoRema <asanorema2036@gmail.com> Date: Tue Aug 18 16:16:19 2026 +0300 Squashed commit of the following: commit173bbb165cAuthor: henjkey <henjkey@gmail.com> Date: Fri Aug 14 18:51:23 2026 +0500 Mappp commitd559ab338aAuthor: henjkey <henjkey@gmail.com> Date: Thu Aug 13 18:42:41 2026 +0500 Map Controller Coordinates Target System Crosshair System Radar System
271 lines
8.8 KiB
C#
271 lines
8.8 KiB
C#
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);
|
|
}
|