Client-Side Prediction with Authoritative Ammo.js

Implementing client-side prediction with an authoritative ammo.js physics server is essential for creating responsive, lag-free multiplayer games. This guide explains how to run synchronized physics simulations on both the client and server, predict local player movement instantly, and reconcile state discrepancies when authoritative server updates arrive.

1. Synchronize the Physics Worlds

Both the client and the server must run the same ammo.js physics simulation. They need identical gravity settings, rigid body colliders (such as static level geometry), and fixed physics time steps.

To ensure deterministic behavior, always step the simulation using a fixed delta time on both sides:

// Use a fixed timestep, e.g., 1/60th of a second
const physicsStep = 1 / 60;
dynamicsWorld.stepSimulation(physicsStep, 1, physicsStep);

2. Implement Input Queuing and Ticking

The client must track time using discrete “ticks” (sequential sequence numbers).

  1. Capture Input: Every tick, record the player’s inputs (e.g., movement vectors, jump buttons).
  2. Buffer Inputs: Store these inputs in a local history buffer along with their corresponding tick number and the resulting physics state (position, rotation, linear velocity, and angular velocity).
  3. Send to Server: Package the inputs and the tick number into a network message and send it to the server.

3. Apply Local Prediction

Instead of waiting for the server to confirm movement, the client predicts the outcome immediately.

  1. Apply the input forces directly to the client’s local player rigid body in ammo.js.
  2. Step the local ammo.js physics world by physicsStep.
  3. Render the predicted position to the screen instantly.

4. Authoritative Server Processing

The server remains the source of truth.

  1. The server receives the client’s input messages.
  2. It applies these inputs to the server-side representation of the player’s rigid body.
  3. The server steps its own ammo.js simulation.
  4. Periodically, the server broadcasts the authoritative state (position, rotation, linear velocity, angular velocity) back to the client, along with the tick number of the last processed input.

5. Client-Side Reconciliation

When the client receives a state update from the server, it must reconcile its predicted state with the server’s authoritative state.

  1. Locate the Historic State: Find the cached local state in the history buffer that matches the tick number returned by the server.
  2. Compare States: Calculate the difference between the server’s position/velocity and the cached client position/velocity for that tick.
  3. Handle Discrepancies: If the difference exceeds a small tolerance threshold (due to collision with other dynamic objects or packet loss):
    • Snap back: Forcefully set the client’s player rigid body to the server’s authoritative position, rotation, and velocities using ammo.js functions:

      const transform = new Ammo.btTransform();
      transform.setIdentity();
      transform.setOrigin(new Ammo.btVector3(serverX, serverY, serverZ));
      transform.setRotation(new Ammo.btQuaternion(serverQX, serverQY, serverQZ, serverQW));
      
      rigidBody.setWorldTransform(transform);
      rigidBody.setLinearVelocity(new Ammo.btVector3(serverVx, serverVy, serverVz));
      rigidBody.setAngularVelocity(new Ammo.btVector3(serverWx, serverWy, serverWz));
      rigidBody.getMotionState().setWorldTransform(transform);
    • Replay inputs: Discard all cached inputs older than the reconciled tick. Then, re-apply all remaining inputs from the buffer in chronological order, stepping the ammo.js simulation once for each input. This recalculates the player’s current position starting from the corrected server state.

  4. Clean up: Delete inputs from the buffer that are older than the server’s last acknowledged tick.