using System.Collections.Generic; using Network.Defines; using Network.NetworkApplication; using UnityEngine; using Vector3 = UnityEngine.Vector3; public static class ClientGameplayInputFlow { public static bool HasPlanarInput(Vector3 input) { return new Vector2(input.x, input.z).sqrMagnitude > 0f; } public static bool TryCreateMoveInput(string playerId, long tick, Vector3 input, bool stopMessagePending, out MoveInput message) { if (!HasPlanarInput(input) && !stopMessagePending) { message = null; return false; } message = new MoveInput { PlayerId = playerId, Tick = tick, MoveX = input.x, MoveY = input.z }; return true; } public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "") { var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z); if (planarDirection.sqrMagnitude <= 0f) { planarDirection = Vector3.forward; } else { planarDirection.Normalize(); } return new ShootInput { PlayerId = playerId, Tick = tick, DirX = planarDirection.x, DirY = planarDirection.z, TargetId = targetId ?? string.Empty }; } public static void SendShootInput( MessageManager messageManager, string playerId, long tick, Vector3 aimDirection, string targetId = "") { if (messageManager == null) { throw new System.ArgumentNullException(nameof(messageManager)); } SendShootInput(messageManager, CreateShootInput(playerId, tick, aimDirection, targetId)); } public static void SendShootInput(MessageManager messageManager, ShootInput message) { if (messageManager == null) { throw new System.ArgumentNullException(nameof(messageManager)); } if (message == null) { throw new System.ArgumentNullException(nameof(message)); } messageManager.SendMessage(message, MessageType.ShootInput); } } public class MovementComponent : MonoBehaviour { [SerializeField] private float _sendInterval = 0.05f; private Player _master; private int _speed = 2; [SerializeField] private Rigidbody _rigid; private float _lastSendTime = 0; private bool _isControlled = false; private Vector3 _serverPosition; private bool _hasServerState = false; private ClientAuthoritativePlayerStateSnapshot _lastAuthoritativeState; public long Tick { get; private set; } = 0; private long _startTickOffset = 0; private long _currentTickOffset = 0; private readonly ClientPredictionBuffer _predictionBuffer = new ClientPredictionBuffer(); private readonly RemotePlayerSnapshotInterpolator _remoteSnapshotInterpolator = new(); [SerializeField] private float _lerpRate = 0.1f; private Vector3 _cachedMoveInput; private Vector3 _lastAimDirection = Vector3.forward; private bool _wasMovingLastFrame; private bool _stopMessagePending; public void Init(bool isControlled, Player master, int speed = 0, long serverTick = 0) { _master = master; _isControlled = isControlled; _speed = speed; _startTickOffset = serverTick; _rigid.interpolation = RigidbodyInterpolation.Interpolate; _rigid.isKinematic = !isControlled; _rigid.velocity = Vector3.zero; if (serverTick != 0 && _isControlled && MainUI.Instance != null) MainUI.Instance.OnStartTickOffsetChanged(serverTick); } private void Update() { if (_isControlled) { _cachedMoveInput = CaptureMovement(); var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput); if (hasMovement) { _lastAimDirection = _cachedMoveInput; _stopMessagePending = false; } else if (_wasMovingLastFrame) { _stopMessagePending = true; } _wasMovingLastFrame = hasMovement; var shootInput = CaptureShootInput(); if (shootInput != null) { NetworkManager.Instance.SendShootInput(shootInput); } if (Time.time - _lastSendTime > _sendInterval) { if (ClientGameplayInputFlow.TryCreateMoveInput(_master.PlayerId, Tick, _cachedMoveInput, _stopMessagePending, out var moveInput)) { NetworkManager.Instance.SendMoveInput(moveInput); _predictionBuffer.Record(moveInput); _stopMessagePending = false; } _lastSendTime = Time.time; Tick++; MainUI.Instance.OnClientTickChanged(Tick); } } } private void FixedUpdate() { if (_isControlled) { if (_hasServerState) { if (MainUI.Instance != null) { MainUI.Instance.OnServerPosChanged(_serverPosition); } Reconcile(_lastAuthoritativeState); _hasServerState = false; } Simulate(_cachedMoveInput); } else { var sample = _remoteSnapshotInterpolator.Sample(Time.time); if (sample.HasValue) { _rigid.MovePosition(sample.Position); _rigid.MoveRotation(sample.Rotation); _rigid.velocity = sample.Velocity; } } } private void Reconcile(ClientAuthoritativePlayerStateSnapshot snapshot) { if (!_predictionBuffer.TryApplyAuthoritativeState(snapshot.SourceState, out var replayInputs)) { return; } _serverPosition = snapshot.Position; _rigid.position = Vector3.Lerp(_rigid.position, _serverPosition, _lerpRate); _rigid.rotation = Quaternion.Slerp(_rigid.rotation, snapshot.RotationQuaternion, _lerpRate); _rigid.velocity = snapshot.Velocity; ReplayPendingInputs(replayInputs); } private Vector3 CaptureMovement() { return new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical")); } private ShootInput CaptureShootInput() { if (!Input.GetMouseButtonDown(0)) { return null; } return ClientGameplayInputFlow.CreateShootInput(_master.PlayerId, Tick, ResolveAimDirection()); } private Vector3 ResolveAimDirection() { if (ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection)) { return _lastAimDirection; } var forward = _master != null ? _master.transform.forward : transform.forward; var planarForward = new Vector3(forward.x, 0f, forward.z); return ClientGameplayInputFlow.HasPlanarInput(planarForward) ? planarForward : Vector3.forward; } private void Simulate(Vector3 input) { _rigid.velocity = _speed * input; if (_isControlled) { if (MainUI.Instance != null) { MainUI.Instance.OnClientPosChanged(_rigid.position); } } } public void OnAuthoritativeState(ClientAuthoritativePlayerStateSnapshot snapshot) { if (_isControlled) { _lastAuthoritativeState = snapshot; _hasServerState = true; } else { _lastAuthoritativeState = snapshot; _remoteSnapshotInterpolator.TryAddSnapshot(snapshot, Time.time); } } public void SetServerTick(long serverTick) { _currentTickOffset = serverTick - Tick - _startTickOffset; if (_isControlled) { if (MainUI.Instance != null) { MainUI.Instance.OnServerTickChanged(serverTick); } } if (_currentTickOffset < 0) { _sendInterval = 0.052f; } if (_currentTickOffset > 0) { _sendInterval = 0.048f; } } private void ReplayPendingInputs(IReadOnlyList replayInputs) { foreach (var replayInput in replayInputs) { _rigid.position += _speed * new Vector3(replayInput.MoveX, 0f, replayInput.MoveY) * _sendInterval; } if (_isControlled) { if (MainUI.Instance != null) { MainUI.Instance.OnClientPosChanged(_rigid.position); } } } }