This commit is contained in:
SepComet
2026-04-06 19:22:22 +08:00
parent 79474b53aa
commit 90f1832397
5 changed files with 76 additions and 73 deletions
+5 -17
View File
@@ -111,10 +111,6 @@ public class MovementComponent : MonoBehaviour
// This matches ServerAuthoritativeMovementConfiguration.SimulationInterval (50ms).
private const float kServerSimulationStepSeconds = 0.05f;
// Dead-band threshold for send-interval correction hysteresis.
// Prevents frame-to-frame send interval oscillation when server tick offset hovers near zero.
private const int kTickOffsetThreshold = 2;
private int _speed = 2;
[SerializeField] private Rigidbody _rigid;
private float _lastSendTime = 0;
@@ -211,7 +207,9 @@ public class MovementComponent : MonoBehaviour
}
Simulate(_cachedMoveInput);
_predictionBuffer.AccumulateLatest(kServerSimulationStepSeconds);
// Use actual elapsed wall-clock time since last authoritative state,
// decoupled from FixedUpdate cadence, to match server's 20Hz cadence.
_predictionBuffer.AccumulateWithElapsedTime(Time.time - _predictionBuffer.LastAuthoritativeStateTime);
}
else
{
@@ -228,7 +226,7 @@ public class MovementComponent : MonoBehaviour
private void Reconcile(ClientAuthoritativePlayerStateSnapshot snapshot)
{
_serverPosition = snapshot.Position;
if (!_predictionBuffer.TryApplyAuthoritativeState(snapshot.SourceState, out var replayInputs))
if (!_predictionBuffer.TryApplyAuthoritativeState(snapshot.SourceState, Time.time, out var replayInputs))
{
return;
}
@@ -240,7 +238,7 @@ public class MovementComponent : MonoBehaviour
predictedRotation,
snapshot.Position,
snapshot.RotationQuaternion,
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond),
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond, snapDistanceMultiplier: 5f),
_activeVisualCorrection);
_activeVisualCorrection = correction.NextState;
@@ -326,16 +324,6 @@ public class MovementComponent : MonoBehaviour
MainUI.Instance.OnServerTickChanged(serverTick);
}
}
if (_currentTickOffset < -kTickOffsetThreshold)
{
_sendInterval = 0.052f;
}
else if (_currentTickOffset > kTickOffsetThreshold)
{
_sendInterval = 0.048f;
}
// Within [-kTickOffsetThreshold, +kTickOffsetThreshold]: no correction, keep current interval
}
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
@@ -26,6 +26,18 @@ namespace Network.NetworkApplication
public long? LastAcknowledgedMoveTick { get; private set; }
/// <summary>
/// Time of the last received authoritative state, used to compute
/// actual elapsed wall-clock time for accumulation synchronization.
/// </summary>
private float _lastAuthoritativeStateTime = float.NegativeInfinity;
/// <summary>
/// Returns the wall-clock time of the last authoritative state arrival.
/// Valid only after TryApplyAuthoritativeState has been called at least once.
/// </summary>
public float LastAuthoritativeStateTime => _lastAuthoritativeStateTime;
public IReadOnlyList<PredictedMoveStep> PendingInputs => pendingInputs;
public void Record(MoveInput input)
@@ -54,7 +66,23 @@ namespace Network.NetworkApplication
pendingInputs[^1] = new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + simulatedDurationSeconds);
}
public bool TryApplyAuthoritativeState(PlayerState state, out IReadOnlyList<PredictedMoveStep> replayInputs)
/// <summary>
/// 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.
/// </summary>
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<PredictedMoveStep> replayInputs)
{
if (state == null)
{
@@ -71,6 +99,10 @@ namespace Network.NetworkApplication
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;
}
}