using System; using System.Collections.Generic; using System.Linq; using Network.Defines; namespace Network.NetworkApplication { public readonly struct PredictedMoveStep { public PredictedMoveStep(MoveInput input, float simulatedDurationSeconds) { Input = input ?? throw new ArgumentNullException(nameof(input)); SimulatedDurationSeconds = simulatedDurationSeconds < 0f ? 0f : simulatedDurationSeconds; } public MoveInput Input { get; } public float SimulatedDurationSeconds { get; } } public sealed class ClientPredictionBuffer { private readonly List pendingInputs = new(); public long? LastAuthoritativeTick { get; private set; } public long? LastAcknowledgedMoveTick { get; private set; } /// /// Time of the last received authoritative state, used to compute /// actual elapsed wall-clock time for accumulation synchronization. /// private float _lastAuthoritativeStateTime = float.NegativeInfinity; /// /// Returns the wall-clock time of the last authoritative state arrival. /// Valid only after TryApplyAuthoritativeState has been called at least once. /// public float LastAuthoritativeStateTime => _lastAuthoritativeStateTime; public IReadOnlyList PendingInputs => pendingInputs; /// /// 清空所有 pending inputs。 /// 用于 Reconcile 后清理已重放的输入,避免它们的时间被继续累积。 /// public void ClearPendingInputs() { pendingInputs.Clear(); } public void Record(MoveInput input) { if (input == null) { throw new ArgumentNullException(nameof(input)); } if (pendingInputs.Count > 0 && pendingInputs[^1].Input.Tick >= input.Tick) { return; } pendingInputs.Add(new PredictedMoveStep(input, 0f)); } public void AccumulateLatest(float simulatedDurationSeconds) { if (pendingInputs.Count == 0 || simulatedDurationSeconds <= 0f) { return; } var latest = pendingInputs[^1]; pendingInputs[^1] = new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + simulatedDurationSeconds); } /// /// Accumulate pending input duration using the actual elapsed wall-clock time /// since the last authoritative state, not the fixed simulation cadence. /// This synchronizes accumulation with the server's 20Hz authoritative cadence. /// public void AccumulateWithElapsedTime(float elapsedSinceLastState) { if (pendingInputs.Count == 0 || elapsedSinceLastState <= 0f || !float.IsFinite(elapsedSinceLastState)) { return; } var latest = pendingInputs[^1]; pendingInputs[^1] = new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + elapsedSinceLastState); } public bool TryApplyAuthoritativeState(PlayerState state, float currentTime, out IReadOnlyList replayInputs) { if (state == null) { throw new ArgumentNullException(nameof(state)); } if (LastAuthoritativeTick.HasValue && state.Tick <= LastAuthoritativeTick.Value) { replayInputs = Array.Empty(); return false; } LastAuthoritativeTick = state.Tick; LastAcknowledgedMoveTick = state.AcknowledgedMoveTick; pendingInputs.RemoveAll(input => input.Input.Tick <= state.AcknowledgedMoveTick); replayInputs = pendingInputs.ToArray(); // Reset the elapsed-time tracker so the next accumulation period // starts from this authoritative state's arrival time. _lastAuthoritativeStateTime = currentTime; return true; } /// /// 只清除已确认的旧输入,不触发 replay,不更新 LastAuthoritativeTick。 /// 用于在校正被禁用时,保持 predictionBuffer 的输入与服务端同步。 /// public void PruneAcknowledgedInputs(long acknowledgedMoveTick) { if (acknowledgedMoveTick <= 0) { return; } pendingInputs.RemoveAll(input => input.Input.Tick <= acknowledgedMoveTick); LastAcknowledgedMoveTick = acknowledgedMoveTick; } } }