All guides

How to sync player positions between browsers

Write your own position to the room on a fixed tick, ten to twenty times a second, and never once per frame. Then draw other players by moving them toward their last received position rather than snapping to it. Those two rules are most of what makes network movement look right.

01Send on a tick, not on a frame

A game loop runs about sixty times a second. Sending a position that often spends bandwidth on differences no player can see, and on a busy room it is the fastest way to hit a rate limit.

Ten to twenty updates a second is enough. Keep updating your local player every frame as you already do, and publish on a timer.

let last = 0;
function loop(now) {
  updateLocalPlayer();          // every frame, as before

  if (now - last > 60) {        // about 16 times a second
    mp.me.set({ x: me.x, y: me.y });
    last = now;
  }

  draw();
  requestAnimationFrame(loop);
}

02Draw toward the target, never at it

Updates arrive in steps, so drawing each one as it lands makes every other player jump. Keep the received value as a target and move a fraction of the remaining distance each frame.

This one change is the difference between a game that feels broken on ordinary wifi and one that feels fine.

drawing the last packetjumps between updatesmoving toward it each framesmooth on the same connection
The same packets, drawn two ways. Snapping to each update produces visible jumps; moving a fraction of the way toward it each frame produces smooth motion with no extra data.
const view = {};   // what we draw, per player id

mp.onState((state) => {
  for (const [id, p] of Object.entries(state.entities.players)) {
    view[id] ??= { x: Number(p.x), y: Number(p.y) };
    view[id].tx = Number(p.x);   // target
    view[id].ty = Number(p.y);
  }
});

function draw() {
  for (const v of Object.values(view)) {
    v.x += (v.tx - v.x) * 0.2;   // 0.15 to 0.25 feels right
    v.y += (v.ty - v.y) * 0.2;
    drawPlayer(v);
  }
}

03Only your own player, by design

You can write your own entity and nothing else. There is no message that addresses another player's position, so the most common cheat in a browser game is impossible rather than merely blocked.

The trade is that movement is client-authoritative: a player can lie about where they are. That is fine for co-op and sandbox games. When a position decides a score or a hit, move that decision into server logic and send inputs rather than coordinates.

04Two things that will bite you

  • Leave the room on unload. A refresh that did not leave cleanly holds the seat for about thirty seconds, so you appear twice to everyone. Wire window.addEventListener("pagehide", () => mp.leave()).
  • Do not send a field per frame per property. Batch into one call with an object, and prefer fewer, larger fields to many small ones.

Common questions

How many players can move in one room at once?

Up to 256, bounded by your plan's per-game concurrent limit. Because all state replicates to everyone, traffic grows with players; test at your target size rather than assuming.

Do I need client-side prediction?

Not for casual and co-op games, where interpolation alone is enough. It matters when the server decides movement and you want your own input to feel instant.

Try it on your own game

The free tier does not expire and asks for no card. Add one script tag and have two people playing in about a minute.