Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

How prediction works

This page explains what happens between a key press and the room’s result. You can build a predicted mechanic without it, as Build a predicted mechanic shows. Read it to reason about timing, to understand a correction you see, or to judge what a player on a slow connection feels.

The model has three ideas:

  • One clock. The room counts ticks, 60 a second, and every input names the tick it is for.
  • The client runs ahead. The client steps the player’s character a few ticks before the room does, so each input reaches the room before the room needs it.
  • The room’s result settles each tick. The client keeps what it predicted for each tick, compares it with the room’s result, and corrects itself where they differ.

A tick is one step of the world, a sixtieth of a second. The room numbers its ticks from the moment it starts, and tick 1000 names the same moment for the room and for every client. The two reach that tick at different times on the wall clock; what they share is the number.

A rule counts time in ticks. Each step adds step.deltaSeconds, a sixtieth of a second, to whatever the rule times, and step.tick says which tick the step is.

The room steps every player’s character once on each tick, on the input that names that tick. It never waits for a player, and it runs no input on a tick other than the one the input names, with one exception for late predicted commands and jumps, described under When input arrives late.

The client steps the player’s character on a tick the room has not reached yet. The difference is the lead. An input for tick 1015 must reach the room before the room steps tick 1015, so the client has to run tick 1015 at least one trip to the room earlier.

Take a player whose client leads the room by 15 ticks, on a connection where a message takes about 75 ms, 4 to 5 ticks, each way:

  1. The room is at tick 1000, and the client at tick 1015. The player presses Dash.
  2. The client stamps the dash command with tick 1015, runs it on that step, and sends it. The player sees the dash now.
  3. The command reaches the room near tick 1005. The room holds it until its own clock reaches tick 1015.
  4. At tick 1015, the room runs the command, with the same system, from its own values.
  5. The room’s next send carries its result for that player: the command accepted, the values of the character’s predicted traits after the latest tick the room stepped, and the names of any predicted traits a rule on the room set with grantPredicted since the last send, so the client counts that correction as the grant it is.
  6. The result reaches the client about 75 ms later. The client compares the room’s values for that tick with the ones it kept for the same tick. They match, so nothing changes.
sequenceDiagram
    participant C as The player's client
    participant R as Room
    Note over C,R: the room at tick 1000, the client at 1015
    C->>C: tick 1015: Dash pressed, a charge spent, the push starts
    C->>R: the dash command, stamped tick 1015
    Note over R: arrives near tick 1005, held until tick 1015
    R->>R: tick 1015: the same system, the same result
    R-->>C: the character's values after the tick, and the command accepted
    C->>C: compare with what it kept for that tick: equal, nothing to do

Without a lead, the room would run each input when it arrived. The network delivers messages unevenly, so the room’s timing of a player’s inputs would follow the network’s jitter: other players would see the character stutter, and the client could not know which tick the room would use, so every action would need a correction.

The client sets the lead itself, from what the room reports. Once a second, the room tells each client how early its movement input arrived. The client aims for its latest-arriving input of the last eight seconds to land two ticks early:

  • Too early: the client runs its clock up to 5% slower until the lead has shrunk.
  • Too late: the client runs its clock up to 5% faster until the lead has grown.
  • More than 10 ticks off: after a stall, the client moves its clock in one jump.

The estimate is smoothed: one report lowers it by at most three ticks, so a single spike does not shrink the lead at once. The client keeps the lead between 2 and 45 ticks, and after a stall it moves back into that range over a few steps. The lead is the sum of four parts:

Part Ticks Why
The trip to the room Half the round trip An input has to arrive before its tick.
The tick’s phase 1 The client sends each tick’s input as the frame that stepped it ends: one message a frame, one tick in it at 60 frames a second, or several.
The margin 2 An input that arrives a little late still arrives in time.
The connection’s jitter What it measures The margin is kept for the latest arrival, so an uneven connection leads by more.

On a simulated connection, the lead settled at the following values:

Round trip Lead
30 ms About 6 ticks, 100 ms
150 ms, with jitter About 13 ticks, 220 ms
300 ms About 15 ticks, 260 ms
150 ms, with jitter and a 300 ms stall every 4 s About 28 ticks, 470 ms

Other engines keep a client ahead of the server the same way. Blizzard’s 2017 talk on Overwatch describes a client half a round trip plus one buffered frame ahead, held there by clock changes of about 5%. Unity’s Netcode for Entities aims for an arrival margin of two ticks by default. Both send input every tick, as Counter-Strike 2 and Rocket League do. Over UDP, where a packet can be lost, they repeat the last few inputs in each packet. The room’s WebSocket delivers every message in order, and a repeated input would wait behind the same delayed message, so the client sends each tick once.

From a key press to another player’s screen

Section titled “From a key press to another player’s screen”

The player’s own action shows at once. Another player sees it later, after four stages:

Stage Time
The room reaches the tick the command names The lead: 100 to 470 ms in the table above
The room’s next send goes out Up to 17 ms at the default 60 sends a second, up to 33 ms at 30
The send reaches the other player’s client That player’s trip, half a round trip
The other client draws toward the new state One send interval, 17 or 33 ms, plus the spread of that connection’s round trips, at most 200 ms

Two players on 30 ms connections see each other’s actions about 155 ms after the key press at the default 60 sends a second, and about 190 ms at 30. Two on 150 ms connections see them about 360 ms after at 60, and about 400 ms at 30. These totals are estimates: the sum of the stages, with the lead from the table above and a spread of 5 ms and 35 ms. The last stage is measured: in a 9-player room over a 30 ms link, a page drew the other players about 43 ms behind the room’s last send at 60, and 58 ms at 30. A game sets its send rate with defineRoom. A client draws every other player from the room’s stream, so on any one screen, the player’s own character is shown ahead of the room and everyone else behind it. Instant-hit weapons account for the difference: the room judges what the player saw.

The room never waits for a player. Where a player’s input for a tick has not arrived:

  • Movement. The room steps the character on the last input it has, without the jump, for up to 15 ticks, a quarter of a second, and then stands the character still. A late input does not run for its tick: the room keeps its newest input as the one it repeats, and corrects the client to where it stepped the character. A held key repeats correctly, so a short delay usually costs nothing.
  • A hitch of the client’s own. When a long frame leaves the client behind its clock, the ticks whose input would reach the room late are not skipped: the client steps them on the input the room steps them on, the last input for 15 ticks and then a still one, up to 12 of them in the next frame, before that frame’s own ticks, and sends none of them. Its history has no hole, so the room’s values for those ticks match and correct nothing; the rest of a hitch longer than a fifth of a second stays a hole, which corrects once. A command or a jump sent during the hitch runs on the client’s next tick of its own.
  • A held input. A game’s held input, such as a sprint’s Shift, is part of the input the room repeats: held for up to 15 silent ticks, then at rest. A release that arrives late takes effect on the next tick the room steps the character without an input, and her next input says released anyway, so a sprint never stays on.
  • A jump. A jump that arrives up to 17 ticks late runs on the room’s next tick, so the jump still happens, a moment after the client showed it.
  • A predicted command. A command that arrives up to its lateTicks late, 17 ticks by default and 60 for the walk, runs once, on the room’s next tick. The room’s result names the tick it ran on as ranTick, the action stands, and the client gets an ordinary correction that re-runs the command on that tick. A command later than that is refused late: the client learns it from the room’s result, and its correction re-runs the ticks without the command, which undoes it.

A correction starts when the room’s values for a past tick differ from what the client kept for that tick, and also when the room refused a command the client ran, or ran it on another tick, whatever the values. The client then does three things:

  1. Restores. It sets every predicted trait of every entity the player predicts, her character and anything else she controls in Simulation.Predicted, such as a kart, to the room’s value for that tick, and stands each body there: a body the client’s own run of that tick left in the same place goes back as that run left it, and a character standing on a floor reads the floor once every body stands, bases before riders. A character in the air stays in the air.
  2. Re-runs. It runs every predicted system again over that whole set at once, once for each tick since, with the inputs the player gave on those ticks and each command on the tick the room ran it, and keeps each tick’s values again, so the room’s next word is compared against the corrected history and the same bump corrects once.
  3. Shows the result. Each entity ends where the room’s rules put it. Where that differs from where the client drew it, the client slides the drawn entity to the new position and turns it to the new facing over 0.1 to 0.4 s, and jumps there at once past 2 m, or past the distance the entity covers in 0.4 s at the speed the room’s word gives it where that is farther, so a character a kart throws slides across while one moved far as she stood jumps. A shove the client did not know of closes over 0.1 s whatever its size, so a hit reads as a hit, and a teleport on either side jumps with no slide. A control’s smoothing changes the jump distance, the seconds of speed it grows by and the longest slide, and characters({ smoothing }) sets it for her character.
flowchart TD
    A["The room's values for tick T arrive"] --> B{"Equal to what the client kept for T, and each command of hers run as she ran it?"}
    B -- yes --> C[Nothing changes]
    B -- no --> D["Restore every predicted trait to the room's value at T"]
    D --> E["Re-run T + 1 to now: the predicted systems over the whole set, the player's inputs and commands"]
    E --> F["Slide each drawn entity to the result, or jump across a teleport"]

A correction leaves the rest of the world alone. Other players, monsters and projectiles are not re-run: the client keeps drawing them from the room’s stream. A predicted system that reads their state during a re-run reads what the stream holds now. Health is never predicted, so no correction touches it.

The client compares within a tolerance, so two browsers whose arithmetic differs in the last digit never correct each other:

  • Position: within 5 cm of each other, unless the control names another poseTolerance.
  • Rotation: within 0.0002 radians, about a hundredth of a degree, of each other.
  • Any other number: within a ten-thousandth; a vector by the distance between the two, a quaternion by the angle. A trait sets a field’s own with predicted: { tolerance }, as A predicted mechanic shows. A field the character’s stats set is not compared.
  • Text, a true or false, an entity, and whether the entity holds the trait: exactly equal.

The player runs toward a doorway. Another player closes the door at tick 1010. Only the room knows it yet.

  1. The client, at tick 1025, has not heard of the door. It runs the character through the doorway.
  2. On the room, the character stops at the closed door on tick 1012.
  3. The room’s send arrives: the door is closed, and the character’s position after tick 1013 is at the door.
  4. The client kept a position past the doorway for tick 1013. The two differ.
  5. The client restores the character to the room’s position at tick 1013 and re-runs the ticks since, with the player’s inputs, against the closed door.
  6. The character ends at the door. The player sees the character slide back a step.

Most corrections have this cause: the room knew something the client had not heard of yet. The others come from input that reached the room late, and from a mistake in a mechanic, which Debug prediction covers. On open ground with nothing in the way, the two run the same code on the same input and agree on every tick.

The player presses Dash during a network stall, and the dash command reaches the room 20 ticks after its tick:

sequenceDiagram
    participant C as The player's client
    participant R as Room
    C->>C: tick 1015: Dash pressed, a charge spent, the push starts
    C->>R: the command, stamped tick 1015, held up by a stall
    R->>R: tick 1015: no command arrived, the character keeps both charges
    Note over R: the command arrives at tick 1035, 20 ticks late: refused late
    R-->>C: the character's values, and the command refused late
    C->>C: differs: restore the room's values and re-run the ticks since
    C->>C: the charge is back, the push is undone, useLatestCommandResult holds the refusal

The game shows the late refusal with a line, as it shows any refusal: commandRefusalLines reads “Too late”.

A correction re-runs the predicted systems for about as many ticks as the lead plus the trip back, 10 to 30 ticks on the connections above. Each moveCharacter and moveAndSlide call of a re-run tick whose inputs and physics body are unchanged, with nothing that moves within its reach, reuses the result its first run kept, so a correction of a trait that is not movement costs little: re-running 18 ticks of her character took 1.3 ms in the engine’s measurements, on a processor slowed to a quarter of its speed, and 18 ticks of her character and a kart that slides twice a tick took 0.7 ms rather than 2.6 ms on a desktop. Near other bodies that move, and on a moving platform, the calls sweep again and a correction costs the full price. The first four mover calls of an entity in a tick keep a result; a fifth sweeps on every re-run. The client keeps the last 3.2 s of the player’s inputs to re-run; a result older than that is not re-run.

A correction re-runs every entity her page predicts, so its cost grows with the set, and the engine caps none. A development page warns of a re-run past a quarter of a frame, about 4.2 ms at 60 frames a second, naming the entities it re-ran and the ticks, and spawnite play state --sample reads the slowest re-run of each second as slowest re-run, ms. A game that passes it predicts fewer entities, or controls some with Simulation.Server.

The following table lists the constants these pages quote, from the engine’s code:

What Value
Ticks the room runs 60 a second
Movement input the client sends Each tick, one message a frame
A predicted command Sent on the tick the player sends it
The room’s sends and its results 60 a second, or 30 where the game sets sendRate
The margin the lead aims for 2 ticks
The lead’s range 2 to 45 ticks
How far the client’s clock speeds or slows 5%
A late predicted command or jump still runs within 17 ticks, or the command’s own lateTicks
The room repeats the last movement input for 15 ticks
A predicted command’s default rate 20 a second, burst of 2, 4 on a tick
Position tolerance 5 cm
Rotation tolerance 0.0002 radians
Other numbers’ tolerance 0.0001
A corrected character slides to its new position over 0.1 to 0.4 s, and jumps past 2 m or 0.4 s of its speed, the farther
A shove the client did not know of closes over 0.1 s
Ticks a hitch left that the next frame fills 12
Other players are drawn toward each send over 50 to 200 ms
Inputs the client keeps to re-run 3.2 s
Press results a client keeps for a message 16