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.

  • Game Development
  • $multiplayer-netcode
  • 134 lines
  • 3 files
  • ~1,900 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- multiplayer-netcode

Installs to ~/.agents/skills/multiplayer-netcode/ for every project on this machine.

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

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.

~/.agents/skills/multiplayer-netcode/SKILL.md

--- name: multiplayer-netcode description: Designs and debugs real-time multiplayer networking for games - choosing authoritative server, client prediction with server reconciliation, snapshot… license: MIT ---

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 / constraintModelWhy
Shooters, action, physics-light co-opAuthoritative server + client prediction + reconciliation + interpolation + lag compensationResponsive local feel, server decides hits
Fighting games, 2-4 player precise timingDeterministic rollback (GGPO-style)Inputs only, local-feeling at up to ~100+ ms with rollback frames
RTS, thousands of unitsDeterministic lockstep (inputs only)Bandwidth independent of unit count; needs full determinism
Turn-based, card, asyncRequest/response over reliable channel (HTTP/WebSocket)Real-time techniques are wasted here
Casual co-op, trusted friendsClient authority with server relaySimplest; 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

  1. Read AGENTS.md, identify the engine and networking library already present (Unity com.unity.netcode.gameobjects, Mirror, Fish-Net, Fusion; Godot MultiplayerAPI; Unreal replication; Colyseus; Nakama; custom).
  2. Write down: tick rate, max players, what the server simulates, what clients may decide.
  3. 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)

ProblemTechnique
Own movement feels laggyClient-side prediction: apply input locally right away, tagged with tick number
Prediction disagrees with serverReconciliation: on server state for tick N, reset to it and replay unacknowledged inputs N+1..now
Other players stutter or teleportSnapshot 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 serverLag compensation: server rewinds hit targets to the shooter’s view time (bounded, e.g. max 200 ms)
Bandwidth too highSend at lower rate than tick, quantise floats, delta-compress against last acked snapshot, interest management
Packet loss drops inputsSend 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

SettingTypicalNotes
Server simulation tick30-60 Hz (competitive shooters up to 128)Higher costs CPU per match
Snapshot send rate20-30 HzCan be lower than tick
Client input sendEvery tickSmall packets, redundant history
Interpolation delay2-3 snapshot intervalsAbsorbs jitter and one lost packet
Max rewind for lag compensation150-250 msBeyond 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

ConceptUnity NGOGodot 4Unreal
Server authority checkIsServermultiplayer.is_server() / is_multiplayer_authority()HasAuthority()
Client to server call[Rpc(SendTo.Server)]@rpc("any_peer") + rpc_id(1, ...)UFUNCTION(Server, Reliable/Unreliable)
Replicated valueNetworkVariable<T>MultiplayerSynchronizerUPROPERTY(Replicated) / ReplicatedUsing
SpawningNetworkObject.Spawn()MultiplayerSpawnerServer spawns replicated actor
Built-in predictionPartial (owner-authoritative transforms); custom for full predictionNone; implement yourselfCharacterMovementComponent 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:

ProfileLatencyJitterLoss
Good30 ms5 ms0%
Typical80 ms20 ms1%
Bad180 ms50 ms5%

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

SymptomLikely cause
Local player rubber-bands constantlyPrediction uses different code or inputs than server; non-deterministic movement (variable dt, physics engine state not rewound)
Snap-backs only on collisions with other playersPredicting against remote entities that are interpolated in the past; accept the correction and smooth it visually
Remote players jitterNo 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/rollbackNon-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.yamlgenerated from this page

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.md147 lines, 6.2 KB
# 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.

esc