Download Game! Currently 71 players and visitors. Last logged in:BeermaestroSolvikPotionOmnipotent

Blitzer's Blog >> 72179

Back to blogs index
Posted: 07 Sep 2026 13:52 [ permalink ]
John Carmacks architecture for QuakeWorld in 1996 fundamentally changed how
multiplayer games were built. Prior to QuakeWorld, games used a synchronous
"lockstep" model or a dumb-client model where pressing "Forward" sent a packet
to the server, and you didn't see your character move until the server's
acknowledgment came back. Over a 300ms dial-up modem, this felt like swimming
in molasses.

To solve this, Carmack and subsequent engine developers (like Valve with the
Source Engine) introduced a trinity of networking concepts.

Here is how they achieved that seamless "playback" feel, and how you can adapt
these principles to your decentralized MeshCRDT.

---

## 1. Client-Side Prediction (CSP)

In QuakeWorld, the client stopped waiting for permission to move. When a
player pressed a key, the local client instantly ran the physics simulation
and moved the camera.

Because the client was predicting the outcome of its inputs before the server
saw them, the game felt completely responsive. However, the client kept a
local buffer of every input it applied, tagged with a sequence number.

## 2. Server Reconciliation

Because of network latency and mid-air collisions, the client's prediction
will inevitably be wrong sometimes. The server remained the absolute
authority.

When the server finally processed the inputs, it sent back the *true* state of
the player, along with the sequence number of the last input it processed.
The client then:

1. Receives the authoritative state.
2. Discards any local inputs up to the server's acknowledged sequence number.
3. Instantly rewinds the player to the server's exact position.
4. **Re-applies** the remaining unacknowledged inputs in a single frame to
calculate the new predicted position.

---

## 3. Entity Interpolation (Handling "The Others")

While CSP handles *your* movement, how do you handle the other players? If you
just snap them to their absolute coordinates whenever a packet arrives, they
will visually stutter and jump around the screen due to packet jitter.

Instead of rendering other players at their *latest* network state, the engine
renders them slightly in the *past*. The client maintains a buffer of incoming
states. If the interpolation delay is 100ms, the client looks at the state
from 100ms ago, and smoothly interpolates the character's transform between
two known network snapshots.

## 4. Lag Compensation (The Modern Addition)

Because clients are rendering other players in the past, a massive problem
arises: if you shoot a player directly in the head on your screen, they are
actually entirely somewhere else on the authoritative server.

Modern engines solve this by keeping a historical buffer of hitboxes on the
server. When a client fires, the server calculates: `Time = CurrentServerTime
- ClientPing - InterpolationDelay`. The server then "rewinds" the world to
that exact millisecond, checks for the collision, and then fast-forwards back
to the present.

> **Key insight:** Lag compensation favors the shooter. It is why you
sometimes feel like you were "shot around a corner"on the shooter's screen,
you hadn't reached the corner yet.

---

## Applying this to your MeshCRDT

Your CRDT architecture is brilliant for a decentralized mesh because it
prevents race conditions without a central server. Your decision to use damage
claims (`damage_taken_from_player1`) instead of absolute HP is exactly the
right paradigm for decentralized authority.

However, your Movement implementation (`x = 25`) will suffer from severe
visual stuttering. If a packet drops, the LWW register simply waits for the
next packet, causing the character to snap instantly to the new coordinates.

To fix this while keeping your CRDT intact, you must separate **Network
State** from **Render State**.

### 1. Buffer the CRDT State

Instead of instantly moving the character when `processNetworkEvent` fires,
push the new value into a timeline buffer for that entity.

```javascript
/ Inside your MeshCRDT or rendering loop
const entityBuffer = [
    { x: 10, y: 0, timestamp: 1725540001000 },
    { x: 25, y: 0, timestamp: 1725540001200 } // Packet arrived out of order,
sorted by timestamp
];

```

### 2. Interpolate the Render State

In your game loop, calculate the render position by looking slightly in the
past (e.g., 100ms behind `Date.now()`). Find the two CRDT snapshots that
surround that past timestamp, and `lerp()` (linear interpolate) between them.

```javascript
/ Conceptual Render Loop
const renderTime = Date.now() - 100; // 100ms interpolation delay

/ Find the snapshot just before and just after renderTime
/ lerp(snapshotA, snapshotB, interpolationFactor)

```

### 3. The Decentralized Collision Problem

QuakeWorld relies on a server to reconcile physics disagreements. In your mesh
network, if Player A and Player B collide, who is right?
Since you are using LWW with absolute states, whoever has the highest
timestamp "wins" the position, but they might visually overlap or clip through
geometry. In a purely decentralized mesh, you will eventually need a consensus
mechanism (like designating one peer as the "host" for physics authority in a
specific zone) if you want rigid, server-like collisions.