This commit is contained in:
SepComet
2026-04-06 16:19:44 +08:00
parent a1ede230bb
commit 79474b53aa
42 changed files with 1264 additions and 18 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-06
@@ -0,0 +1,51 @@
## Context
`MovementComponent.SetServerTick(long serverTick)` drives input-send cadence by comparing server tick to local client tick. When `_currentTickOffset = serverTick - Tick - _startTickOffset` is negative, it sets `_sendInterval = 0.052f`; when positive, `_sendInterval = 0.048f`. When the offset hovers near zero (e.g., due to minor clock drift or network jitter), the sign flips each call, causing `_sendInterval` to toggle every frame between 0.048 and 0.052. This send-rate oscillation adds jitter to the input cadence.
## Goals / Non-Goals
**Goals:**
- Prevent send interval oscillation when server tick offset is near zero.
- Preserve meaningful clock correction when real drift exists (offset is consistently positive or negative).
**Non-Goals:**
- This is not a full clock synchronization protocol — only a local oscillation guard.
- Does not change the underlying tick offset computation.
## Decisions
### Decision: Dead-band hysteresis for send interval correction
Instead of toggling `_sendInterval` on every sign change of `_currentTickOffset`, apply a dead-band threshold. Only correct the send interval when the absolute offset exceeds a meaningful threshold (e.g., 1-2 ticks = 50-100ms of drift).
**Current code (problematic):**
```csharp
if (_currentTickOffset < 0)
_sendInterval = 0.052f;
if (_currentTickOffset > 0)
_sendInterval = 0.048f;
```
**Proposed replacement:**
```csharp
private const float kTickOffsetThreshold = 2; // ticks
if (_currentTickOffset < -kTickOffsetThreshold)
_sendInterval = 0.052f;
else if (_currentTickOffset > kTickOffsetThreshold)
_sendInterval = 0.048f;
// else: keep current interval (no correction within dead band)
```
**Alternatives considered:**
1. **Exponential moving average of offset** — smooths jitter but adds complexity and latency to correction.
2. **Remove correction entirely, use fixed 0.05s** — simpler but loses adaptive behavior when real drift exists.
The dead-band approach is the simplest that directly solves oscillation without adding state complexity.
## Risks / Trade-offs
- **Risk**: If `kTickOffsetThreshold` is too large, real drift may not be corrected fast enough.
- **Mitigation**: Start with a conservative threshold (1-2 ticks). Adjust after measuring.
- **Risk**: The hysteresis introduces a zone where no correction is applied even when offset is slightly non-zero.
- **Accepted**: This is the intended behavior — minor fluctuations near zero should not disturb steady-rate sending.
@@ -0,0 +1,22 @@
## Why
`MovementComponent.SetServerTick(...)` toggles `_sendInterval` between 0.052f and 0.048f whenever `_currentTickOffset` crosses zero. When the offset hovers near zero due to minor clock drift, this causes frame-to-frame send-cadilla oscillation, which disrupts steady-rate input submission and adds unnecessary jitter to the prediction/reconciliation loop.
## What Changes
- Add hysteresis to the send interval adjustment so it does not flip-flop when `_currentTickOffset` oscillates around zero.
- The correction logic will use a dead-band threshold — only adjust `_sendInterval` when the absolute offset exceeds a meaningful threshold, not on every sign change.
- A small nominal send interval (50ms) remains the baseline; clock correction only applies when drift is substantial.
## Capabilities
### New Capabilities
- `client-send-interval-stabilization`: A contract specifying that the client's send interval does not oscillate due to minor server tick offset fluctuations near zero.
### Modified Capabilities
- `client-prediction-cadence`: Extend to explicitly cover that send interval correction is also bounded by hysteresis and does not toggle at near-zero offset.
## Impact
- `MovementComponent.SetServerTick(...)` — threshold-based hysteresis added to send interval correction logic
- No changes to network message formats, delivery policies, or prediction buffer behavior
@@ -0,0 +1,36 @@
# client-send-interval-stabilization Specification
## Purpose
Define that the client send interval is protected from oscillation when the server tick offset hovers near zero, ensuring steady-rate input submission without frame-to-frame cadence jitter.
## Requirements
### Requirement: Send interval correction uses hysteresis dead-band
The controlled-client send interval corrector SHALL apply a dead-band threshold before adjusting `_sendInterval`, so that minor server tick offset fluctuations near zero do not cause the send cadence to toggle between values.
#### Scenario: No correction within dead-band
- **WHEN** `_currentTickOffset` is between -2 and +2 ticks (inclusive)
- **THEN** `_sendInterval` is not changed
- **THEN** the previously active send interval is preserved
#### Scenario: Slow drift correction below threshold
- **WHEN** `_currentTickOffset` stays within the dead-band for an extended period
- **THEN** `_sendInterval` remains stable at its current value
- **THEN** no oscillation occurs regardless of offset sign changes within the band
#### Scenario: Correction applies outside dead-band
- **WHEN** `_currentTickOffset` exceeds +2 (client ahead of server)
- **THEN** `_sendInterval` is set to 0.048f to send slightly faster
- **WHEN** `_currentTickOffset` is below -2 (client behind server)
- **THEN** `_sendInterval` is set to 0.052f to send slightly slower
### Requirement: Send interval stabilizes after offset crosses threshold
Once the offset exits the dead-band and triggers a correction, subsequent corrections SHALL only occur when the offset crosses the threshold again in the opposite direction, preventing rapid re-correction.
#### Scenario: Correction latches until opposite threshold
- **WHEN** offset triggers a correction to 0.048f (offset > +2)
- **THEN** further offset increases within the same sign do not re-trigger correction
- **THEN** the send interval stays at 0.048f until offset crosses back below +2 then exceeds -2
@@ -0,0 +1,12 @@
## 1. Implement hysteresis dead-band in SetServerTick
- [x] 1.1 Add `private const int kTickOffsetThreshold = 2;` to MovementComponent
- [x] 1.2 Replace the dual `if (_currentTickOffset < 0 / > 0)` sign checks with a threshold-based dead-band: only adjust `_sendInterval` when `Mathf.Abs(_currentTickOffset) > kTickOffsetThreshold`
## 2. Add regression test for send interval stability
- [x] 2.1 Add a test in `ServerRuntimeEntryPointTests.cs` or a new test file verifying that `SetServerTick` does not oscillate `_sendInterval` when offset hovers near zero
## 3. Update TODO.md
- [x] 3.1 Mark TODO.md Step 3 as complete