Skip to content

five-on-it / packages/net-client/src/clock

packages/net-client/src/clock ​

Where is the server's clock, as far as this client can tell, and which tick should be rendered for remote entities. Remote entities are shown INTERP_DELAY_TICKS behind the server's estimated tick so there is always a pair of snapshots to interpolate between.

The server also tells the client how many of its inputs were still queued; the client nudges its own tick rate so that the queue stays short but never empty.

A frame runs the ticks its time owes, but never more than MAX_TICKS_PER_FRAME (frameTicks): a slow frame must not make the next one slower still by catching up all at once. What is still owed after them is kept up to MAX_BACKLOG_TICKS and caught up over the next frames; whole ticks beyond that are dropped (a stall, a tab in the background) and counted: their inputs are never sent, so the client's inputs stay numbered by its clock, and the server runs those ticks on its guess (apps/server/src/game-server.ts), as the prediction does (Predictor.guess).

The estimate. The clock keeps its own time in real ticks, advanced by every frame's elapsed time (ClientClock.advance), never by the simulation's tick count, whose rate the input queue nudges by 3 % (ClientClock.tickScale): the time the others are drawn at must not bend with it. Every snapshot is an observation of the server's offset from that time, snapshotTick − localTicks. A snapshot can arrive late (the network, the server's timer, a stall, the frame it waits for) but never early, so the largest offset seen is the nearest to the truth: the top of the window is the largest observation of the last CLOCK_WINDOW_TICKS (about forty snapshots). A snapshot that comes late gives a smaller offset and cannot raise it; a stall gives none and changes nothing; the six snapshots a 300 ms stall holds, arriving in one frame, raise nothing.

The estimate does not chase the top: it lives in a band CLOCK_SLACK wide under it, and starts at the band's floor. Above the top (every recent arrival says the path's delay grew: the render tick would outrun the snapshots) it comes down to the top at RENDER_DRIFT, the buffers extrapolating meanwhile; below the band (the earliest arrivals say the remotes could be drawn with less delay) it rises to the floor at CLOCK_SLEW, slowly, there being no hurry; inside the band it holds. The earliest arrival of a window is rare, so the top rises when one comes and falls back by the jitter when it ages out; an estimate that chased it at 5 % each way was a sawtooth of pace changes on everything on screen every few seconds (11.6 % of the frames of a Riverbend replay off their pace by 5 %), and the band is what keeps it still: the estimate ratchets up by half a per cent at most, and comes down only when the path got slower by more than the slack. ClientClock.serverTick is therefore a careful estimate, a slack under what the earliest arrival implies. A discontinuity (the estimate more than RENDER_SNAP_TICKS outside the band: a server restarted, its tick jumped) is taken at once only once it has persisted for a whole window, never on one observation; in the clock's first window, with no history to be fooled by, it is taken at once (the snapshots held while the level built come in the first frame, seconds ahead of the welcome) and the rest is slewed at RENDER_DRIFT either way. Until 0.67 the estimate started again from every snapshot as it arrived and the render tick was clamped to the newest snapshot: a stall of the snapshots halted every remote, and the burst of late snapshots that ended it leapt them forward by the stall (fifteen ticks in three frames measured, 1.6 and 1.9 m at 50 km/h).

The render tick is the estimated server tick less the delay, and nothing else: it keeps its pace through a silence (each remote's buffer extrapolates to its own limit and holds, remote-buffer.ts), and the caller draws the remotes at it alone, without the frame's alpha (the fraction of a simulation tick the controlled entity is drawn at), since real time is already in it. The clock is advanced by a frame's time before the frame's snapshots are handed to it, so that each observation is taken at the time it arrived.

Classes ​

Variables ​

Functions ​