Multiplayer Netcode skill for Codex
Instant local feel, server-decided outcomes. Codex picks a netcode model that fits the genre, from authoritative server with prediction to deterministic rollback or lockstep, then implements tick-tagged inputs, reconciliation by replay, buffered interpolation for remote players and bounded lag compensation. It validates every client message on the server and tests under simulated latency, jitter and loss instead of trusting localhost.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- multiplayer-netcode Installs to ~/.agents/skills/multiplayer-netcode/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install multiplayer-netcode The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project multiplayer-netcode Installs to .agents/skills/multiplayer-netcode/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/multiplayer-netcode.zipunzip -o multiplayer-netcode.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) multiplayer-netcode PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $multiplayer-netcode, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and debugs real-time multiplayer networking for games - choosing authoritative server, client prediction with server reconciliation, snapshot interpolation, lag compensation, deterministic lockstep or rollback (GGPO-style), tick rates, input and snapshot packet design, delta compression, and anti-cheat validation - on Unity Netcode for GameObjects, Mirror, Fish-Net, Photon Fusion, Godot high-level multiplayer, Unreal replication, Colyseus or custom UDP. Use when movement rubber-bands or jitters online, hits miss that looked like hits, adding multiplayer to a single-player game, choosing a netcode model, or simulating latency and packet loss.
Use it when
- Adding real-time multiplayer to a game
- Movement jitters, rubber-bands or hits feel unfair online
- Choosing between server authority, lockstep and rollback
- Clients can currently cheat by sending outcomes instead of intents
Use something else when
- The game is turn-based or async and needs a plain API: use websockets-realtime
- You need Unreal-specific replication setup only: use unreal-engine
How to check the result
Two clients hold under 80 ms/1% and 180 ms/5% loss profiles for ten minutes with prediction error near zero, and deterministic models produce matching state checksums on every peer.
The mistake it prevents
Testing only on localhost, where zero latency hides missing interpolation, broken reconciliation and unbounded lag compensation.
Done well means: the server is the authority for anything that decides outcomes, the local player’s own actions feel instant through prediction, remote entities move smoothly through interpolation, the model was chosen to fit the genre, and every change is tested under simulated latency, jitter and loss, not on localhost.
Pick the model by genre
| Genre / constraint | Model | Why |
|---|---|---|
| Shooters, action, physics-light co-op | Authoritative server + client prediction + reconciliation + interpolation + lag compensation | Responsive local feel, server decides hits |
| Fighting games, 2-4 player precise timing | Deterministic rollback (GGPO-style) | Inputs only, local-feeling at up to ~100+ ms with rollback frames |
| RTS, thousands of units | Deterministic lockstep (inputs only) | Bandwidth independent of unit count; needs full determinism |
| Turn-based, card, async | Request/response over reliable channel (HTTP/WebSocket) | Real-time techniques are wasted here |
| Casual co-op, trusted friends | Client authority with server relay | Simplest; cheating is possible, so not for competitive play |
Determinism (lockstep, rollback) requires identical results on every machine: fixed-point or carefully controlled floating point, seeded RNG in the simulation, no iteration over unordered containers, and the same build. Cross-platform float determinism is hard; budget for it or avoid these models.
Before writing code
- Read
AGENTS.md, identify the engine and networking library already present (Unitycom.unity.netcode.gameobjects, Mirror, Fish-Net, Fusion; GodotMultiplayerAPI; Unreal replication; Colyseus; Nakama; custom). - Write down: tick rate, max players, what the server simulates, what clients may decide.
- Find the simulation loop. Netcode depends on a fixed tick (see game-loop-architecture). Variable-timestep simulation cannot be predicted and reconciled cleanly.
Core techniques (authoritative server)
| Problem | Technique |
|---|---|
| Own movement feels laggy | Client-side prediction: apply input locally right away, tagged with tick number |
| Prediction disagrees with server | Reconciliation: on server state for tick N, reset to it and replay unacknowledged inputs N+1..now |
| Other players stutter or teleport | Snapshot interpolation: render remote entities ~2 snapshots behind (e.g. 100 ms at 20 Hz), lerp between buffered snapshots |
| Shots that hit on screen miss on server | Lag compensation: server rewinds hit targets to the shooter’s view time (bounded, e.g. max 200 ms) |
| Bandwidth too high | Send at lower rate than tick, quantise floats, delta-compress against last acked snapshot, interest management |
| Packet loss drops inputs | Send each input packet with the last few inputs redundantly |
For the full prediction and reconciliation loop with buffers and code, read references/prediction-loop.md when implementing or debugging it.
Rates and budgets
| Setting | Typical | Notes |
|---|---|---|
| Server simulation tick | 30-60 Hz (competitive shooters up to 128) | Higher costs CPU per match |
| Snapshot send rate | 20-30 Hz | Can be lower than tick |
| Client input send | Every tick | Small packets, redundant history |
| Interpolation delay | 2-3 snapshot intervals | Absorbs jitter and one lost packet |
| Max rewind for lag compensation | 150-250 ms | Beyond that, high-ping players get no favour |
Keep packets under ~1200 bytes to avoid IP fragmentation. Use unreliable channels for state and input streams; reliable ordered only for events that must arrive (chat, match results, item purchase), and never for per-tick data (head-of-line blocking).
Engine mapping
| Concept | Unity NGO | Godot 4 | Unreal |
|---|---|---|---|
| Server authority check | IsServer | multiplayer.is_server() / is_multiplayer_authority() | HasAuthority() |
| Client to server call | [Rpc(SendTo.Server)] | @rpc("any_peer") + rpc_id(1, ...) | UFUNCTION(Server, Reliable/Unreliable) |
| Replicated value | NetworkVariable<T> | MultiplayerSynchronizer | UPROPERTY(Replicated) / ReplicatedUsing |
| Spawning | NetworkObject.Spawn() | MultiplayerSpawner | Server spawns replicated actor |
| Built-in prediction | Partial (owner-authoritative transforms); custom for full prediction | None; implement yourself | CharacterMovementComponent predicts and corrects |
Libraries such as Photon Fusion and Fish-Net ship tick-based prediction; read their docs for the version installed rather than re-implementing on top.
Security rules
- Never trust the client for outcomes: damage, currency, inventory, cooldowns, positions
beyond a speed check. Clients send intent (
fire at tick N with aim vector), the server decides. - Validate every RPC argument: type, range, rate (max N calls per second), and that the
sender owns the entity it references (
multiplayer.get_remote_sender_id()in Godot). - Rate-limit and size-limit inbound packets before parsing.
- Do not ship server secrets or admin RPCs in client builds.
Testing under bad networks
Localhost hides every netcode bug. Always test with:
| Profile | Latency | Jitter | Loss |
|---|---|---|---|
| Good | 30 ms | 5 ms | 0% |
| Typical | 80 ms | 20 ms | 1% |
| Bad | 180 ms | 50 ms | 5% |
Tools: Unity Transport’s network simulator and the NGO Multiplayer Tools package, Unreal
Net PktLag=150, Net PktLoss=5 console commands (or PktLagVariance), clumsy on Windows,
tc netem on Linux (sudo tc qdisc add dev lo root netem delay 80ms 20ms loss 1%, needs root;
ask before running and remove it afterwards with sudo tc qdisc del dev lo root), Network Link
Conditioner on macOS.
Debugging symptoms
| Symptom | Likely cause |
|---|---|
| Local player rubber-bands constantly | Prediction uses different code or inputs than server; non-deterministic movement (variable dt, physics engine state not rewound) |
| Snap-backs only on collisions with other players | Predicting against remote entities that are interpolated in the past; accept the correction and smooth it visually |
| Remote players jitter | No interpolation buffer, buffer too small for jitter, or using arrival time instead of server tick |
| Hits register late or “around corners” | Lag compensation window too large; clamp rewind |
| Desync in lockstep/rollback | Non-determinism: float differences, unseeded RNG, dictionary iteration order, frame-time-based logic |
Smooth visible corrections: keep the corrected simulation state, but blend the rendered position toward it over ~100 ms instead of teleporting the mesh.
Verification
- Two clients plus a server (or host) under the “Typical” and “Bad” profiles for 10 minutes.
- Log per-tick prediction error magnitude on the client; it should be near zero in normal movement and corrections should converge within a few ticks.
- For deterministic models, compute a checksum of simulation state every N ticks on all peers and assert equality; log the first differing tick.
- Measure bandwidth per client (bytes/s up and down) against the budget.
Hand-off summary
**Model:** authoritative server, 60 Hz tick, 30 Hz snapshots, prediction + reconciliation for local player
**Remote entities:** interpolated 100 ms behind; lag compensation rewind capped at 200 ms
**Security:** server validates fire rate, ammo, movement speed; RPCs check sender ownership
**Tested:** 2 clients at 80 ms/20 ms/1% and 180 ms/50 ms/5% for 10 min; mean prediction error 0.02 m
**Bandwidth:** 14 KB/s down, 3 KB/s up per client at 8 players What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Multiplayer Netcode"
short_description: "Instant local feel, server-decided outcomes"
default_prompt: "$multiplayer-netcode Our online player rubber-bands every few seconds, figure out why" references/prediction-loop.md
# Prediction, reconciliation and interpolation in detail
Read this when implementing client prediction for an authoritative server, or when debugging
rubber-banding. Code is engine-neutral TypeScript; the structure maps directly to C#, GDScript,
C++ or Rust.
## Data on the wire
```ts
// client -> server, unreliable, every tick
interface InputMsg {
tick: number; // client's simulation tick this input applies to
inputs: PlayerInput[]; // newest last; includes the previous 2-4 for loss redundancy
}
// server -> client, unreliable, at snapshot rate
interface SnapshotMsg {
serverTick: number;
lastProcessedInputTick: number; // per receiving client: newest input the server applied
entities: EntityState[]; // quantised; delta-compressed against client's last ack
}
```
The server applies each client's inputs in tick order, ignoring duplicates (already-processed
ticks) and holding a small buffer (1-3 ticks) to absorb jitter. If an input for a tick never
arrives, repeat the last known input for that tick rather than stalling.
## Client side: predict and keep history
```ts
const HISTORY = 128; // ring buffer covers ~2 s at 60 Hz
const inputHistory: (PlayerInput | undefined)[] = new Array(HISTORY);
const stateHistory: (PlayerState | undefined)[] = new Array(HISTORY);
function clientTick(tick: number, input: PlayerInput) {
inputHistory[tick % HISTORY] = input;
local = simulatePlayer(local, input, DT); // SAME function the server runs
stateHistory[tick % HISTORY] = clone(local);
send({ tick, inputs: lastN(inputHistory, tick, 3) });
}
```
`simulatePlayer` must be a pure function of (state, input, DT): no frame time, no randomness
outside a seeded generator in state, no reads from interpolated remote entities unless you
accept corrections when they differ.
## Client side: reconcile
```ts
const EPSILON = 0.01; // metres; below this, ignore tiny float differences
function onSnapshot(snap: SnapshotMsg) {
const ackTick = snap.lastProcessedInputTick;
const serverState = findSelf(snap.entities);
const predicted = stateHistory[ackTick % HISTORY];
if (!predicted) return;
if (distance(predicted.position, serverState.position) < EPSILON &&
predicted.velocity.equals(serverState.velocity)) {
return; // prediction was right
}
// Rewind to authoritative state and replay every input after it.
const before = local.position.clone();
local = clone(serverState);
stateHistory[ackTick % HISTORY] = clone(local);
for (let t = ackTick + 1; t <= currentTick; t++) {
const input = inputHistory[t % HISTORY];
if (!input) continue;
local = simulatePlayer(local, input, DT);
stateHistory[t % HISTORY] = clone(local);
}
// Visual smoothing: offset the rendered mesh and decay it over ~100 ms.
renderOffset.add(before.sub(local.position));
}
```
Each frame, decay `renderOffset` toward zero (`renderOffset.scale(Math.exp(-dt * 20))`) and
draw the player at `local.position + renderOffset`. The simulation stays exact; only the
visual catches up smoothly.
If a physics engine moves the player, reconciliation must also restore the physics body state
and re-step it per replayed tick. Engines whose physics world cannot be stepped for one body
alone (most built-in ones) need a kinematic character controller for the predicted player.
## Remote entities: snapshot interpolation
```ts
const INTERP_DELAY_TICKS = 6; // e.g. 100 ms at 60 Hz tick, covers 2 snapshots at 20-30 Hz
const buffer = new Map<EntityId, { tick: number; state: EntityState }[]>();
function renderRemote(id: EntityId, renderServerTick: number) {
const snaps = buffer.get(id)!; // sorted by tick, trimmed to ~1 s
const target = renderServerTick - INTERP_DELAY_TICKS;
for (let i = snaps.length - 1; i > 0; i--) {
const a = snaps[i - 1], b = snaps[i];
if (a.tick <= target && target <= b.tick) {
const t = (target - a.tick) / (b.tick - a.tick);
return lerpState(a.state, b.state, t);
}
}
return snaps.at(-1)!.state; // buffer starved: hold last (or extrapolate briefly, max ~100 ms)
}
```
`renderServerTick` is the client's estimate of the current server tick (fractional), derived
from snapshot arrivals with smoothing. Adjust the client's clock slowly (speed up or slow
down by 1-2%) to keep the buffer at its target size instead of jumping.
## Server: lag compensation for hitscan
```ts
// Server keeps per-entity hitbox history for MAX_REWIND ticks.
function processFire(shooter: Player, msg: FireMsg) {
const viewTick = msg.clientTick - INTERP_DELAY_TICKS; // what the shooter saw
const rewind = Math.min(serverTick - viewTick, MAX_REWIND_TICKS);
const hit = raycastAgainstHistory(msg.origin, msg.dir, serverTick - rewind, shooter.id);
if (hit && validateFire(shooter, msg)) applyDamage(hit.target, weaponDamage(shooter));
}
```
Validate the fire message first: weapon owned, ammo available, rate of fire respected, origin
near the shooter's server position. Cap rewind so that high-ping players cannot shoot far into
the past.
## Rollback (peer-to-peer, deterministic)
Same shape as reconciliation, applied to the whole game state:
1. Each peer simulates every frame using local input plus predicted remote input (repeat last).
2. When real remote input for frame F arrives and differs from the prediction, load the saved
state for F, re-simulate F..now with corrected inputs, and continue.
3. Save state every frame (fast snapshot: plain arrays, no allocations) and keep a window of
roughly 8 frames; beyond the window, stall or add input delay.
4. Add 1-3 frames of input delay to reduce rollback frequency; let players see it in options.
Libraries: GGPO (C++), GGRS (Rust, Bevy via bevy_ggrs), Photon Fusion and Quantum (Unity,
commercial). Use one rather than writing the session protocol yourself.
## Debug overlay worth building
- Current RTT, jitter, packet loss (from sequence numbers).
- Interpolation buffer depth per remote entity (ticks).
- Prediction error per correction (metres) and correction count per second.
- Server tick vs estimated server tick drift.
- Toggle to draw server-authoritative positions as ghosts next to predicted ones.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.