# The shared simulation: ECS, Rapier physics and a fixed tick

> Net Forge runs one simulation module, @netforge/engine-sim, on the server for authority and in the browser for prediction. It is a bitECS world with deterministic Rapier physics, stepped at a fixed tick, with integer state and bit-exact snapshots.

Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under `@netforge` with source at https://github.com/netnetdev/net-forge.

Source: https://forge.netnet.capital/docs/concepts/simulation-ecs  
Last updated: 2026-10-23. Checked against @netforge/engine 0.2.0.

Net Forge runs one simulation module, `@netforge/engine-sim`, in two places: on the server, where it is the authority, and in the browser, where it predicts the local player. Because both sides run the same code on the same quantized inputs, there is no second copy of the game rules to keep in sync.

```sh
npm install @netforge/engine-sim
```

```ts title=sim.ts
import { initSim, createWorld, addPlayer, step, captureState, encodeSnapshot, Button } from "@netforge/engine-sim";

await initSim(); // once per process: loads the Rapier WebAssembly build
const world = createWorld({ tickRate: 30, seed: 7, enemies: { target: 2000 } });
addPlayer(world, 1);

// One fixed tick, with every player's input for that tick.
const { tick, events } = step(world, new Map([[1, { seq: 1, tick: 0, moveX: 0, moveZ: 1, yaw: 0, buttons: Button.Fire }]]));
const snapshot = encodeSnapshot(captureState(world)); // a full snapshot as bytes
console.log(tick, events.length, snapshot.byteLength);
```

## What is in it

| Part | What it does |
|---|---|
| ECS world | bitECS entities and components, iterated in `netId` order so both sides agree. |
| Physics | Rapier's deterministic WebAssembly build (`@dimforge/rapier3d-deterministic-compat`): bodies, triggers, joints, raycasts and a kinematic character controller. |
| Fixed tick | `step(world, inputs)` advances exactly one tick of `1 / tickRate` seconds and never reads a clock. Templates run at 30 Hz. |
| Inputs | `InputCommand { seq, tick, moveX, moveZ, yaw, buttons }`, quantized on entry (moves to 1/127, yaw to a 16-bit turn) and 15 bytes encoded. |
| State | `captureState` returns integers only (positions in 1/1024 m). The sim snaps itself to that grid every tick. |
| Snapshots | `encodeSnapshot(state, baseline?)` writes a full or delta snapshot: zigzag varints with a changed-field mask, about 3 bytes for a resting entity and 8 for a moving one. |

There is no DOM, timer, network or renderer in the package. It compiles with `lib: ES2022` only, so the same file runs in Node and in a browser worker.

## Why one simulation

A multiplayer game usually needs the rules twice: once on the server that decides what happened, and again on the client so the player's own movement feels instant. When they drift apart, players see rubber-banding and wrong hits. Net Forge removes the second copy. The browser imports the same `step` the server runs, applies its own inputs ahead of the server, and when the server's snapshot arrives it restores that state and replays the inputs the server has not seen yet. See [Netcode and prediction](https://forge.netnet.capital/docs/concepts/netcode-prediction.md).

## Determinism, honestly

On one JavaScript engine the same inputs give the same snapshot bytes; the package tests this over 1,000 ticks. Across engines, `Math.sin` and `Math.cos` can differ in the last bit. That is acceptable because the server is authoritative and clients reconcile to it, rather than the other way round. Randomness comes from a seeded generator stored in the state, never from `Math.random`.

## Fixed tick and rendering

`FixedStepper` turns frame deltas into a whole number of ticks plus a render `alpha`. The browser feeds it from `requestAnimationFrame` and the server from its own loop. `renderView(world)` returns typed arrays of positions with the previous tick's values, so the renderer interpolates with `alpha` without touching the ECS.

```ts title=loop.ts
import { FixedStepper } from "@netforge/engine-sim";

const stepper = new FixedStepper(30);
let last = performance.now();
function frame(now: number): void {
  const { ticks, alpha } = stepper.advance(now - last);
  last = now;
  for (let i = 0; i < ticks; i++) {
    // step(world, inputs) once per tick
  }
  // draw, interpolating by alpha
  void alpha;
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
```

## How much it costs

Enemies steer inside the ECS on a uniform grid rather than as Rapier bodies, which is what lets a survivor-like keep thousands alive. Measured with the survivor template's playtest bots on an Apple M2 Pro under Node 22: 2,000 enemies and 4 players cost 2.22 ms per tick on average and 5.21 ms at the 99th percentile, against a 33 ms budget at 30 Hz.

## Game code never touches this layer

A game's `src/game.ts` does not import `@netforge/engine-sim`. It declares prefabs, weapons and rules with [the typed game API](https://forge.netnet.capital/docs/concepts/game-api.md), and the template compiles those declarations into sim content. You use engine-sim directly when you write a template or a tool, or when you want the physics world on its own: see [How to add deterministic Rapier physics to a three.js game](https://forge.netnet.capital/docs/guides/threejs-rapier-physics.md).

Full API: [@netforge/engine-sim reference](https://forge.netnet.capital/docs/reference/engine-sim.md).

## FAQ

### Is the Net Forge simulation deterministic?

Yes on one JavaScript engine: the same inputs produce the same snapshot bytes, and the package tests this. Across different engines trigonometry can differ in the last bit, so the server stays authoritative and clients reconcile to it.

### What ECS does Net Forge use?

bitECS, with entities iterated in `netId` order so the server and every client process them in the same sequence.

### What physics engine does Net Forge use?

Rapier, in its deterministic cross-platform WebAssembly build, version 0.21. It provides rigid bodies, triggers, joints, raycasts, shape casts and a kinematic character controller with coyote time, jump buffering and dash.

### What tick rate does Net Forge run at?

The templates run the simulation at a fixed tick: 30 Hz by default, 60 Hz in the arena shooter, kart racer and platformer. The local room host sends snapshots to clients 20 times a second.

### Can I run the simulation in a Web Worker or on a server?

Yes. The package has no DOM or timer dependency and compiles against `lib: ES2022`, so it runs in Node, a Web Worker or on the main thread.
