Skip to main content

Multiplayer

Let several people share one scene at the same time. Send messages, share values, spawn objects for everyone and show other players moving smoothly.

These calls work only when multiplayer is turned on for your project and a room is open. The rest of the time they do nothing. So the same script also works in a single-player preview, and you do not need two versions.

Who is in the room​

ctx.net.id;        // this participant's id
ctx.net.peers(); // everyone currently in the room
ctx.net.onJoin((peer) => {});
ctx.net.onLeave((peer) => {});

What syncs by itself​

Only the positions of physics bodies set to shared. You send everything else yourself.

This is on purpose. A crate, a door or a ball should be one object that everyone sees in the same place. A player character must not be shared: otherwise, when one person walks, everyone's avatar moves. → ownership

Messages​

Use messages for things that happen: someone scored, a round started, a door opened.

ctx.net.send('score', { points: 10 });
ctx.net.on('score', ({ from, data }) => {
/* … */
});

A message goes to everyone except you. You do not get a confirmation back.

Shared state​

Use shared state for values everyone must agree on. It is a simple map, and everyone gets a copy. If two people write the same key, the last write wins.

ctx.net.state.set(ctx.net.id, { name, colour, ready: true });
ctx.net.state.get(somePeerId);
ctx.net.state.all();
ctx.net.state.on(ctx.net.id, (value) => {});

To keep values for each player, use ctx.net.id as the key. Each person writes their own entry and reads everyone else's.

Spawn for everyone​

const id = ctx.net.spawn(props.bullet, { position, rotation });
ctx.net.despawn(id);

You get the id right away, but the object appears a moment later. The id is the same for everyone.

Only the person who spawned an object can remove it. When someone leaves the room, the objects they spawned are removed too.

Move other players smoothly​

This is where things most often go wrong, so there is a special call for it.

ctx.net.follow(otherPlayerObject, { position, rotation });
ctx.net.unfollow(otherPlayerObject);

Call follow with each position you receive. Ten to twenty times a second is normal. The object then moves smoothly, a little behind the latest position.

Calling follow again with the same position costs nothing. So you can call it every frame with the latest position you have.

Do not write received positions straight into the transform

Updates arrive much less often than frames, so the object stands still and then jumps: other players seem to step instead of walk. Easing toward the newest position does not fix it either. Between updates there is nothing new to move toward, so the object stops and then lurches.

A setup that works​

If you are starting from scratch, this setup will take you a long way:

  • Each player controls their own character — a local body, moved by their input.
  • Their position is sent out through net.state or messages, ten to twenty times a second.
  • Everyone else's characters are moved with follow.
  • World objects are shared bodies, so physics keeps them in sync for you.
  • The score and the round state live in net.state, written by whoever is in charge of that event.

Next: Troubleshooting — what to do when something does not work.