Multiplayer
Everything below is live only when your project has multiplayer switched on and the player opened a room.
The rest of the time these calls quietly do nothing, which is deliberate: a script written for multiplayer still runs perfectly well in a single-player preview. You do not need two versions.
ctx.net.id; // this participant's id
ctx.net.peers(); // everyone currently in the room
ctx.net.onJoin((peer) => {});
ctx.net.onLeave((peer) => {});
What replicates by itself
Only the positions of physics bodies marked shared. Everything else you send yourself.
That sounds like a small amount, and it is the right small amount: a crate, a door or a ball should be one object everybody agrees on, while a player character must not be — otherwise one person walking moves everybody's avatar. → ownership
Messages
For events: someone scored, a round started, a door was opened.
ctx.net.send('score', { points: 10 });
ctx.net.on('score', ({ from, data }) => {
/* … */
});
Fire-and-forget, to everyone except you.
Shared state
For values everyone needs to agree on. It is a simple map where the last write wins, replicated to everybody.
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) => {});
Keying by ctx.net.id is the standard pattern for per-player state: everyone writes their own
entry and reads everyone else's.
Spawning for everyone
const id = ctx.net.spawn(props.bullet, { position, rotation });
ctx.net.despawn(id);
The id comes back immediately, but the object appears once the room confirms it. That round trip is what keeps the id identical on every client, which is what lets physics agree about it afterwards.
Only whoever spawned something may despawn it, and leaving the room takes your spawns with you.
Drawing other people smoothly
This is the part that most often goes wrong, so it has a dedicated call.
ctx.net.follow(otherPlayerObject, { position, rotation });
ctx.net.unfollow(otherPlayerObject);
Call it with each position you receive, ten to twenty times a second is normal, and the runtime writes the transform every frame instead, playing the buffer back slightly behind so it always has two known positions to move between.
Packets arrive far less often than frames, so the object stands still and then jumps: remote players appear to step rather than walk. Easing toward the newest packet does not fix it either — between packets there is nothing ahead of the current moment to ease toward, so it stalls and then lurches.
Re-feeding the same position costs nothing, so calling follow every frame with whatever you
currently hold is the intended use.
A shape that works
If you are starting from scratch, this division of labour will get you a long way:
- each player owns their own character — a local body, driven by their input;
- their position goes out through
net.stateor a channel, ten to twenty times a second; - everyone else's characters are drawn with
follow; - the world's objects are shared bodies, so physics keeps them consistent for free;
- the score and round state live in
net.state, written by whoever is authoritative for that event.
Next: Troubleshooting — when something is not behaving.