Getting Started

Build a multiplayer game in under 5 minutes.

1. Set Up the HTML

Create an HTML file with a canvas and an import map pointing to the library CDN:

<canvas id="game" width="640" height="480"></canvas>

<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.168.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.168.0/examples/jsm/",
        "easy-multiplayer/": "https://libs.letsinspire.com/easy_multiplayer/dev/"
    }
}
</script>
Note The three import is currently required because some internal modules reference Three.js. This dependency will be made optional in a future release.

2. Create the Game

import { EasyMultiplayer } from 'easy-multiplayer/EasyMultiplayer.js';

const game = new EasyMultiplayer({
    room: 'my-game-room',    // Players in the same room see each other
    tickRate: 15,            // Game logic runs 15 times per second
    canvas: document.getElementById('game'),
    state: {                  // Your game state — any serializable object
        players: {},
        coins: [],
        scores: {}
    }
});

The state object is your entire game world. The library automatically snapshots it every tick, syncs hashes with other peers, and restores it during rollback. Keep it JSON-serializable.

3. Define Inputs

Register named inputs that the library will sample every tick and sync across the network:

const keys = {};
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);

game.defineInput('left',  () => keys['ArrowLeft'] || false);
game.defineInput('right', () => keys['ArrowRight'] || false);
game.defineInput('up',    () => keys['ArrowUp'] || false);
game.defineInput('down',  () => keys['ArrowDown'] || false);

Each input is a function that returns the current value. Return values must be JSON-serializable (strings, numbers, booleans).

4. Handle Players

game.on('playerJoined', ({ playerId, state, random }) => {
    state.players[playerId] = {
        x: Math.floor(random() * 20),
        y: Math.floor(random() * 15),
        score: 0
    };
});

game.on('playerLeft', ({ playerId, state }) => {
    delete state.players[playerId];
});
Important Player callbacks run during rollback resimulation too. Use only random() for randomness (not Math.random()), and don't do side effects like DOM changes or sounds here.

5. Write Game Logic

The tick callback runs at a fixed rate. Use query() to check inputs:

game.on('tick', ({ state, query, random, players }) => {
    for (const pid of players) {
        const p = state.players[pid];
        if (!p) continue;

        // Query-based input: predicate gets all inputs, rollback only if answer changes
        if (query(pid, i => i.left && !i.right)) p.x--;
        if (query(pid, i => i.right && !i.left)) p.x++;
        if (query(pid, i => i.up && !i.down))    p.y--;
        if (query(pid, i => i.down && !i.up))    p.y++;
    }

    // Spawn coins using deterministic random
    if (state.coins.length < 5 && random() < 0.05) {
        state.coins.push({
            x: Math.floor(random() * 20),
            y: Math.floor(random() * 15)
        });
    }
});
Tip: Context-Sensitive Queries Only query inputs when the answer matters. If there's a wall to the left, don't query for left — it can't change the outcome. Put compound logic in a single predicate: query(pid, i => i.left && !i.right) records one result, not two. See How It Works for details.

6. Render

The draw callback runs every animation frame (typically 60fps):

game.on('draw', ({ state, myId, canvas }) => {
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw coins
    ctx.fillStyle = 'gold';
    for (const coin of state.coins) {
        ctx.fillRect(coin.x * 32, coin.y * 32, 32, 32);
    }

    // Draw players
    for (const [id, p] of Object.entries(state.players)) {
        ctx.fillStyle = id === myId ? 'blue' : 'red';
        ctx.fillRect(p.x * 32, p.y * 32, 32, 32);
    }
});

myId is the local player's unique ID, so you can highlight "your" character.

7. Start

game.start();

That's it. Open the page in two browser tabs (or on two devices). Both join the same room and play together.

What Happens Under the Hood

  1. Peers discover each other via WebTorrent (no server needed)
  2. Clocks synchronize to agree on time
  3. Each tick: inputs are sampled, predicted for remote players, and the game state advances
  4. Inputs are exchanged over the network
  5. If a prediction was wrong and it changes a query result, the game rolls back and resimulates
  6. State hashes are compared periodically to detect desyncs

Read the full technical explanation →

Next Steps