3.0 KiB
3.0 KiB
Context
Local loopback testing shows controlled-player jitter. One root cause is ReplayPendingInputs() applying each PredictedMoveStep as a single accumulated-duration integration, while live prediction uses FixedUpdate with fixed substeps. This mismatch in integration shape causes trajectory divergence even for identical input sequences.
Tank movement kinematics: heading(t+dt) = heading(t) + turnInput * turnSpeed * dt, position(t+dt) = position(t) + forward(heading(t+dt)) * throttleSpeed * dt. Step-by-step and one-shot integration diverge at larger dt values because each step's heading affects the next step's forward direction.
Goals / Non-Goals
Goals:
ReplayPendingInputs()uses fixed-step accumulation matching server authoritative cadence- Replay produces identical trajectory to live prediction for the same input sequence
- No external API changes, only internal integration method modification
- Add regression test for replay vs live prediction parity
- Add diagnostics for acknowledged move tick, predicted pose, authoritative pose, and correction magnitude
Non-Goals:
- Do not modify server 50ms cadence
- Do not fix send-interval oscillation (TODO Step 3)
- Do not modify visual correction logic (TODO Step 4)
Decisions
Decision: Use server SimulationInterval (50ms) as replay substep size
Choice: Replay in 50ms fixed substeps.
Rationale:
- Server integrates at 50ms cadence to produce authoritative state; client replay must match to eliminate偏差
- Client FixedUpdate at 20ms is render/physics step, not server simulation granularity
- Each
PredictedMoveStep.SimulatedDurationSecondsmay be 50ms, 100ms, etc.; stepping at 50ms handles all cases
Alternatives:
- 20ms step: matches client FixedUpdate but not server, still causes偏差
- Use
SimulatedDurationSecondsas single step: current behavior, causes non-linear divergence
Decision: Substep within ReplayPendingInputs loop without new state
Implementation:
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
{
const float serverStepSeconds = 0.05f; // 50ms server SimulationInterval
foreach (var replayInput in replayInputs)
{
var remaining = replayInput.SimulatedDurationSeconds;
while (remaining > 0f)
{
var step = Mathf.Min(remaining, serverStepSeconds);
ApplyTankMovementToPredictedState(
replayInput.Input.TurnInput,
replayInput.Input.ThrottleInput,
step);
remaining -= step;
}
}
}
Rationale:
- Does not change
PredictedMoveStepstruct interface - No new temporary state variables needed
- Integration shape identical to live prediction path
Risks / Trade-offs
- [Risk] Floating-point accumulation error could cause loop to run one step too many or too few
- Mitigation: Use
Mathf.Min(remaining, serverStepSeconds)guard; final step naturally truncates
- Mitigation: Use
- [Risk] 50ms step adds one extra function call for very short inputs
- Acceptable: Negligible overhead